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.
Files changed (57) hide show
  1. package/.env.example +7 -0
  2. package/build-info.json +2 -2
  3. package/docs/architecture/04-configuration-reference.md +1 -0
  4. package/docs/architecture/05-config-governance.md +1 -1
  5. package/driver/CHANGELOG.md +11 -0
  6. package/driver/band-shape.mjs +4 -4
  7. package/driver/contract-vocabulary.mjs +3 -3
  8. package/driver/coverage-form-io.mjs +3 -1
  9. package/driver/coverage-form.mjs +21 -2
  10. package/driver/coverage-ledger.mjs +7 -1
  11. package/driver/declination-call.mjs +33 -16
  12. package/driver/degraded-parts.mjs +6 -0
  13. package/driver/engine/mcp/clarivate-server.mjs +2 -0
  14. package/driver/engine/mcp/corsearch-server.mjs +2 -0
  15. package/driver/engine/mcp/declination-server.mjs +4 -5
  16. package/driver/engine/mcp/euipo-server.mjs +2 -0
  17. package/driver/engine/mcp/free-tier-server.mjs +2 -0
  18. package/driver/engine/mcp/gather-config.mjs +15 -0
  19. package/driver/engine/mcp/held-answers.mjs +11 -3
  20. package/driver/engine/mcp/signa-server.mjs +2 -0
  21. package/driver/engine/mcp/supplemental.mjs +5 -2
  22. package/driver/engine/mcp/uspto-local-server.mjs +2 -0
  23. package/driver/envelope-settle.mjs +13 -3
  24. package/driver/gateway.mjs +31 -10
  25. package/driver/package.json +1 -1
  26. package/driver/parts-not-completed.mjs +52 -11
  27. package/driver/pipeline-knockout.mjs +5 -4
  28. package/driver/pipeline.mjs +436 -72
  29. package/driver/placement-decisions.mjs +4 -4
  30. package/driver/placement-form-io.mjs +21 -1
  31. package/driver/placement-form.mjs +25 -15
  32. package/driver/placement-union.mjs +53 -8
  33. package/driver/publish/index.mjs +7 -3
  34. package/driver/runner.mjs +4 -4
  35. package/driver/skills/clearance-register/digest.md +1 -1
  36. package/driver/skills/clearance-search/phase2-execution.md +3 -3
  37. package/driver/skills/clearance-search/synthesis-rules.md +3 -3
  38. package/driver/skills/placement-inquiry/SKILL.md +3 -3
  39. package/driver/stages.mjs +5 -5
  40. package/driver/suite-census.json +84 -12
  41. package/driver/verify.mjs +6 -6
  42. package/mcp-server/CHANGELOG.md +4 -0
  43. package/mcp-server/package.json +1 -1
  44. package/package.json +1 -1
  45. package/portal-ui/package.json +1 -1
  46. package/providers/_shared/README.md +1 -0
  47. package/providers/_shared/enumerate.mjs +4 -3
  48. package/providers/_shared/execute-plan.mjs +24 -7
  49. package/providers/_shared/send-until.mjs +25 -0
  50. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  51. package/providers/oauth-mcp-bridge/package.json +1 -1
  52. package/scripts/band-shape-probe.mjs +1 -1
  53. package/scripts/e2e-first-time.mjs +2 -1
  54. package/shared/names-in-force.mjs +1 -0
  55. package/shared/slices-taken-out.mjs +65 -0
  56. package/skills/clearotron-account/SKILL.md +1 -1
  57. package/skills/clearotron-ops/SKILL.md +1 -1
@@ -81,6 +81,12 @@
81
81
  "skips": 0,
82
82
  "todos": 0
83
83
  },
84
+ "a-blocking-email-keeps-the-reports-client-actions.test.mjs": {
85
+ "tests": 3,
86
+ "asserts": 7,
87
+ "skips": 0,
88
+ "todos": 0
89
+ },
84
90
  "a-borrowed-node-modules-is-not-a-licence-defect.test.mjs": {
85
91
  "tests": 8,
86
92
  "asserts": 28,
@@ -669,6 +675,12 @@
669
675
  "skips": 0,
670
676
  "todos": 0
671
677
  },
678
+ "a-failure-notice-goes-to-whoever-asked.test.mjs": {
679
+ "tests": 3,
680
+ "asserts": 10,
681
+ "skips": 0,
682
+ "todos": 0
683
+ },
672
684
  "a-family-withheld-by-judgment-is-not-read-as-awaiting.test.mjs": {
673
685
  "tests": 6,
674
686
  "asserts": 15,
@@ -705,6 +717,24 @@
705
717
  "skips": 0,
706
718
  "todos": 0
707
719
  },
720
+ "a-findings-record-broken-after-synthesis-is-restored-at-delivery.pipeline.test.mjs": {
721
+ "tests": 6,
722
+ "asserts": 38,
723
+ "skips": 0,
724
+ "todos": 0
725
+ },
726
+ "a-finished-turn-is-not-a-timeout-for-a-request-inside-it.test.mjs": {
727
+ "tests": 5,
728
+ "asserts": 23,
729
+ "skips": 0,
730
+ "todos": 0
731
+ },
732
+ "a-floor-is-set-aside-on-its-goods-or-mark-on-the-evidence.test.mjs": {
733
+ "tests": 6,
734
+ "asserts": 19,
735
+ "skips": 0,
736
+ "todos": 0
737
+ },
708
738
  "a-floor-row-whose-owner-was-set-aside-is-answered.test.mjs": {
709
739
  "tests": 2,
710
740
  "asserts": 11,
@@ -803,7 +833,7 @@
803
833
  },
804
834
  "a-goods-test-miss-is-not-called-other-goods.test.mjs": {
805
835
  "tests": 2,
806
- "asserts": 6,
836
+ "asserts": 7,
807
837
  "skips": 0,
808
838
  "todos": 0
809
839
  },
@@ -827,7 +857,7 @@
827
857
  },
828
858
  "a-group-row-gives-every-owner-in-it-a-fate.test.mjs": {
829
859
  "tests": 7,
830
- "asserts": 36,
860
+ "asserts": 37,
831
861
  "skips": 0,
832
862
  "todos": 0
833
863
  },
@@ -1557,6 +1587,12 @@
1557
1587
  "skips": 0,
1558
1588
  "todos": 0
1559
1589
  },
1590
+ "a-register-search-that-cannot-finish-is-delivered-as-not-searched.pipeline.test.mjs": {
1591
+ "tests": 3,
1592
+ "asserts": 22,
1593
+ "skips": 0,
1594
+ "todos": 0
1595
+ },
1560
1596
  "a-register-unit-follow-up-carries-the-one-market-list-and-the-classes.test.mjs": {
1561
1597
  "tests": 4,
1562
1598
  "asserts": 10,
@@ -1815,12 +1851,24 @@
1815
1851
  "skips": 0,
1816
1852
  "todos": 0
1817
1853
  },
1854
+ "a-search-a-check-cannot-prove-is-delivered-as-not-searched.pipeline.test.mjs": {
1855
+ "tests": 11,
1856
+ "asserts": 66,
1857
+ "skips": 0,
1858
+ "todos": 0
1859
+ },
1818
1860
  "a-search-asks-the-web-on-the-settings-its-report-type-pins.test.mjs": {
1819
1861
  "tests": 6,
1820
1862
  "asserts": 22,
1821
1863
  "skips": 0,
1822
1864
  "todos": 0
1823
1865
  },
1866
+ "a-search-taken-out-of-the-band-is-never-read-as-answered.test.mjs": {
1867
+ "tests": 5,
1868
+ "asserts": 23,
1869
+ "skips": 0,
1870
+ "todos": 0
1871
+ },
1824
1872
  "a-search-the-engine-chose-not-to-run-is-not-reported-as-unfinished.test.mjs": {
1825
1873
  "tests": 5,
1826
1874
  "asserts": 13,
@@ -1905,6 +1953,12 @@
1905
1953
  "skips": 0,
1906
1954
  "todos": 0
1907
1955
  },
1956
+ "a-slice-taken-out-of-the-band-is-open-for-its-own-reason.test.mjs": {
1957
+ "tests": 4,
1958
+ "asserts": 12,
1959
+ "skips": 0,
1960
+ "todos": 0
1961
+ },
1908
1962
  "a-slice-the-fold-refused-as-already-asked-is-not-a-dispatch-failure.test.mjs": {
1909
1963
  "tests": 10,
1910
1964
  "asserts": 32,
@@ -2067,6 +2121,12 @@
2067
2121
  "skips": 0,
2068
2122
  "todos": 0
2069
2123
  },
2124
+ "a-turns-tool-servers-are-told-when-to-stop-sending.test.mjs": {
2125
+ "tests": 3,
2126
+ "asserts": 12,
2127
+ "skips": 0,
2128
+ "todos": 0
2129
+ },
2070
2130
  "a-two-form-matter-owes-a-read-per-form.test.mjs": {
2071
2131
  "tests": 8,
2072
2132
  "asserts": 25,
@@ -2403,6 +2463,12 @@
2403
2463
  "skips": 0,
2404
2464
  "todos": 0
2405
2465
  },
2466
+ "an-identical-mark-is-declined-on-its-goods-only-on-goods-held-complete.test.mjs": {
2467
+ "tests": 6,
2468
+ "asserts": 19,
2469
+ "skips": 0,
2470
+ "todos": 0
2471
+ },
2406
2472
  "an-incumbent-alert-adds-no-class.test.mjs": {
2407
2473
  "tests": 2,
2408
2474
  "asserts": 3,
@@ -2597,7 +2663,7 @@
2597
2663
  },
2598
2664
  "band-truth-gate.pipeline.test.mjs": {
2599
2665
  "tests": 3,
2600
- "asserts": 15,
2666
+ "asserts": 16,
2601
2667
  "skips": 0,
2602
2668
  "todos": 0
2603
2669
  },
@@ -3653,7 +3719,7 @@
3653
3719
  },
3654
3720
  "engine.anthropic.test.mjs": {
3655
3721
  "tests": 70,
3656
- "asserts": 262,
3722
+ "asserts": 263,
3657
3723
  "skips": 2,
3658
3724
  "todos": 0
3659
3725
  },
@@ -4385,7 +4451,7 @@
4385
4451
  },
4386
4452
  "named-band-missing.test.mjs": {
4387
4453
  "tests": 7,
4388
- "asserts": 22,
4454
+ "asserts": 24,
4389
4455
  "skips": 0,
4390
4456
  "todos": 0
4391
4457
  },
@@ -7103,7 +7169,7 @@
7103
7169
  },
7104
7170
  "the-review-after-a-repair-reads-only-what-the-repair-changed.pipeline.test.mjs": {
7105
7171
  "tests": 24,
7106
- "asserts": 190,
7172
+ "asserts": 193,
7107
7173
  "skips": 0,
7108
7174
  "todos": 0
7109
7175
  },
@@ -7120,8 +7186,8 @@
7120
7186
  "todos": 0
7121
7187
  },
7122
7188
  "the-review-is-told-the-floors-placement-set-aside.test.mjs": {
7123
- "tests": 9,
7124
- "asserts": 25,
7189
+ "tests": 11,
7190
+ "asserts": 27,
7125
7191
  "skips": 0,
7126
7192
  "todos": 0
7127
7193
  },
@@ -7391,7 +7457,7 @@
7391
7457
  },
7392
7458
  "the-words-the-step-reads-match-what-the-driver-checks.test.mjs": {
7393
7459
  "tests": 9,
7394
- "asserts": 44,
7460
+ "asserts": 45,
7395
7461
  "skips": 0,
7396
7462
  "todos": 0
7397
7463
  },
@@ -7427,7 +7493,7 @@
7427
7493
  },
7428
7494
  "the-writer-owes-positions-not-records.test.mjs": {
7429
7495
  "tests": 6,
7430
- "asserts": 20,
7496
+ "asserts": 21,
7431
7497
  "skips": 0,
7432
7498
  "todos": 0
7433
7499
  },
@@ -7953,8 +8019,8 @@
7953
8019
  "todos": 0
7954
8020
  },
7955
8021
  "connector-skills-are-one-copy.test.mjs": {
7956
- "tests": 5,
7957
- "asserts": 30,
8022
+ "tests": 6,
8023
+ "asserts": 31,
7958
8024
  "skips": 0,
7959
8025
  "todos": 0
7960
8026
  },
@@ -8681,6 +8747,12 @@
8681
8747
  "skips": 0,
8682
8748
  "todos": 0
8683
8749
  },
8750
+ "providers/_shared/test/a-plan-call-past-its-time-to-stop-sending-returns-what-it-holds.test.mjs": {
8751
+ "tests": 5,
8752
+ "asserts": 19,
8753
+ "skips": 0,
8754
+ "todos": 0
8755
+ },
8684
8756
  "providers/_shared/test/a-question-with-no-regions-is-asked-to-the-orders-scope.test.mjs": {
8685
8757
  "tests": 3,
8686
8758
  "asserts": 6,
package/driver/verify.mjs CHANGED
@@ -2689,12 +2689,12 @@ function placementAccountVerdict(dir) {
2689
2689
  const rid = rowOf.get(id), cause = rid ? causeOf.get(rid) : null;
2690
2690
  return `${id}${group ? ` in (${group})` : ""}${cause ? `, its row ${rid} (${cause})` : ""}`;
2691
2691
  });
2692
- const finishRow = floors.some((f) => f.includes(", its row ")) ? " Where a floor's row is named, finish that row: a floor's row owes a tier, "
2693
- + "sheet-2 at the least, and a reason stating the ground." : "";
2692
+ const finishRow = floors.some((f) => f.includes(", its row ")) ? " Where a floor's row is named, finish that row: give it "
2693
+ + "headline-candidate or sheet-2, and a reason stating the ground." : "";
2694
2694
  if (unjudged.length) {
2695
2695
  const named = unjudged.slice(0, 8).map((u) => `${u.row_id ?? u.select ?? "?"} (${u.cause})`).join(", ");
2696
- const floorsToo = floors.length ? ` The form gives these floors no fate either: ${capped(floors)}. Place each by `
2697
- + `selecting that record, sheet-2 at the least, or set its line aside by name on its status or its territory.${finishRow}` : "";
2696
+ const floorsToo = floors.length ? ` The form gives these floors no fate either: ${capped(floors)}. Select each at `
2697
+ + `headline-candidate or sheet-2, or set its line aside by name.${finishRow}` : "";
2698
2698
  return fail(`placement_unjudged=${unjudged.length}: ${named}${unjudged.length > 8 ? ", …" : ""} — each of these rows is `
2699
2699
  + "in placement-form.json and its own judgement will not render: a selected row owes a `tier` from the closed set and a `reason` "
2700
2700
  + "stating the ground; a row you wrote in full owes mark, owner and jurisdiction too. Finish these rows in the form; "
@@ -2755,8 +2755,8 @@ function placementSaidBackVerdict({ refused, floors, also, finishRow = "" }) {
2755
2755
  if (floors.length)
2756
2756
  return fail(`placement_floors_without_fate=${floors.length}: ${capped(floors)} — each is a floor, a live identical or `
2757
2757
  + "near-identical mark in an ordered class, and the form gives it no fate. "
2758
- + "Place it by selecting that record, sheet-2 at the least, or set its line aside by name on its status or its "
2759
- + `territory.${finishRow} Only these are owed; every other row stands.${also}`);
2758
+ + "Select it at headline-candidate or sheet-2, or set its line aside by name."
2759
+ + `${finishRow} Only these are owed; every other row stands.${also}`);
2760
2760
  if (refused.length)
2761
2761
  return fail(`placement_refused=${refused.length}: ${capped(refused)} — the form refused each of these for the reason given `
2762
2762
  + "with it, and nothing else on the form moved. Send each again corrected, or leave it out; every other row stands.");
@@ -1,5 +1,9 @@
1
1
  # trademark-artifacts-mcp
2
2
 
3
+ ## 0.5.0-beta.3
4
+
5
+ No changes in this release.
6
+
3
7
  ## 0.5.0-beta.2
4
8
 
5
9
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-artifacts-mcp",
3
- "version": "0.5.0-beta.2",
3
+ "version": "0.5.0-beta.3",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron",
3
3
  "mcpName": "io.github.CordilleraSarl/clearotron",
4
4
  "type": "module",
5
- "version": "0.5.0-beta.2",
5
+ "version": "0.5.0-beta.3",
6
6
  "license": "AGPL-3.0-only",
7
7
  "repository": {
8
8
  "type": "git",
@@ -2,7 +2,7 @@
2
2
  "name": "portal-ui",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.5.0-beta.2",
5
+ "version": "0.5.0-beta.3",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
8
8
  "engines": {
@@ -24,6 +24,7 @@ documents both: `countProbe` (`"endpoint"` / `"cheap"` / `"none"`) says where a
24
24
  | `term-shape.mjs` | Whether a string is a searchable mark term and agrees with its predicate (`{ predicate: "exact", term: "WAVO*" }` is a silent clean). |
25
25
  | `territory-codes.mjs` | Display name → ISO 3166 / WIPO ST.3 code. `""` means worldwide, `null` means unknown — and never goes to the wire. |
26
26
  | `provider-text.mjs` | Clips a vendor message from the MIDDLE, because the word that classifies a refusal sits at the end of the sentence. |
27
+ | `send-until.mjs` | The time after which a plan call sends no further question, handed to each turn's tool servers ahead of the turn's deadline, so a call cut short returns what it holds. |
27
28
  | `test/` | Twelve files, provider-agnostic by design — several drive every adapter's real code rather than fixtures of what we believe it returns. |
28
29
 
29
30
  ## What reads it
@@ -57,6 +57,7 @@ import {
57
57
  import { makeCountProbe, parseToolText, isToolError } from "./count.mjs"; import { floorOf, moreThan } from "../../shared/register-floor.mjs";
58
58
  import { guardCountCall, guardToolCall } from "./transport-guard.mjs";
59
59
  import { clipProviderText } from "./provider-text.mjs"; // — keep the discriminator
60
+ import { pastSendUntil } from "./send-until.mjs"; // past a call's time to stop sending, a timed-out question is not split again
60
61
 
61
62
  // ── — HOW MUCH OF A PROVIDER ERROR SURVIVES INTO THE BAND BLOCK: ALL OF IT ───────────────────────────
62
63
  //
@@ -448,7 +449,7 @@ export function makeEnumerate(deps) {
448
449
  }
449
450
  // A half that timed out while its sibling answered is the one place fewer regions may help: it is
450
451
  // halved in turn, under the same rule.
451
- const r = timedOut({ r: first })
452
+ const r = timedOut({ r: first }) && !pastSendUntil(tctx)
452
453
  ? await regionHalves(auth, { ...params, regions: half }, tctx, { ceiling, incomplete, cause: parseToolText(first)?.reason ?? cause, hear })
453
454
  : first;
454
455
  const parsed = isToolError(r) ? null : parseToolText(r);
@@ -637,7 +638,7 @@ export function makeEnumerate(deps) {
637
638
  + `Provider's own words: ${clipProviderText(c?.reason ?? "", COUNT_PROBE_BUDGET)}`,
638
639
  { crowd_basis: "provider-refused-count", count_unavailable: true });
639
640
  }
640
- if (splittable && isGatewayTimeout(c?.reason)) return regionHalves(auth, params, tctx, { ceiling, incomplete, cause: String(c.reason), hear });
641
+ if (splittable && isGatewayTimeout(c?.reason) && !pastSendUntil(tctx)) return regionHalves(auth, params, tctx, { ceiling, incomplete, cause: String(c.reason), hear });
641
642
  return incomplete(0, 0, [],
642
643
  `provider error on the count probe before enumeration: ${String(c?.reason ?? "count unavailable")}`, timeoutMark(c?.reason));
643
644
  }
@@ -670,7 +671,7 @@ export function makeEnumerate(deps) {
670
671
  if (page === 0 && allNames && allNames.length > 1 && listRefusal?.test(String(r.text ?? ""))) {
671
672
  return enumerateQuestion(auth, params, tctx, { split, width: Math.ceil(allNames.length / 2) }, said);
672
673
  }
673
- if (splittable && isGatewayTimeout(r.text)) return regionHalves(auth, params, tctx, { ceiling, incomplete, cause: String(r.text), hear });
674
+ if (splittable && isGatewayTimeout(r.text) && !pastSendUntil(tctx)) return regionHalves(auth, params, tctx, { ceiling, incomplete, cause: String(r.text), hear });
674
675
  return incomplete(total, results.length, results, `provider error during enumeration (page ${page}): ${String(r.text ?? "")}`, timeoutMark(r.text));
675
676
  }
676
677
  const parsed = parseToolText(r);
@@ -20,6 +20,8 @@ import { nativeScriptIndexGap } from "./script-form.mjs"; import { floorOf, more
20
20
  import { entryTermIssues, goodsTermsList } from "./term-shape.mjs";
21
21
  import { awaitsReadingTurn, releasedFamiliesFile } from "./plan-guards.mjs";
22
22
  import { FOLLOW_UP_SEARCH_LIMIT, runDriverDirOf, followUpRoom, followUpClass, identicalMarkTerms, recordFollowUpSearch, recordOutstanding } from "./follow-up-searches.mjs";
23
+ import { bandWithSlicesOut, slicesTakenOutByAxis } from "../../shared/slices-taken-out.mjs";
24
+ import { pastSendUntil } from "./send-until.mjs"; // a call past its time to stop sending sends no new question // a band read as answers, with the slices a check could not prove ran taken out
23
25
 
24
26
  /** How soon after an answer lands it is written to the band, while the call goes on (see `keepAnswers` below). */
25
27
  export const KEEP_ANSWERS_AFTER_MS = 2000;
@@ -375,10 +377,20 @@ export function makeExecutePlan(deps) {
375
377
  try { identicalTerms = identicalMarkTerms(JSON.parse(readFileSync(join(driverDir, "register-plan.json"), "utf8"))); } catch { /* no frozen plan: no identical terms known */ }
376
378
  }
377
379
  const refused = [];
380
+ // THE BAND AS ANSWERS, for the checks below that read it so: a slice the run's searches record took out of the band,
381
+ // because a check could not prove it ran, is read as the open slice it was delivered as, whatever wrote the band
382
+ // since (shared/slices-taken-out.mjs). The run is the band's own, as the propose tool reads it. The writes below
383
+ // still merge the file as it stands.
384
+ const asAnswers = (file) => {
385
+ const prior = JSON.parse(readFileSync(file, "utf8"));
386
+ return Array.isArray(prior) ? bandWithSlicesOut(prior, slicesTakenOutByAxis(dirname(dirname(outPath))).get(axis)).blocks : prior;
387
+ };
388
+ const bandAsAnswers = () => asAnswers(outPath);
378
389
  // The follow-ups this axis's band already answers, read once and only when the net meets a question sent before:
379
- // an answered one adds nothing when it is sent again, and a failed one counts again. A unit whose pass was killed at
380
- // its time limit is parked: its band is renamed (`<band>.tainted-<n>`) so the resume runs the unit again, and the
381
- // re-run sends the same follow-ups. Their answers are in the renamed band, so they count nothing either.
390
+ // an answered one adds nothing when it is sent again, and a failed one counts again, as does one taken out. A unit
391
+ // whose pass was killed at its time limit is parked: its band is renamed (`<band>.tainted-<n>`) so the resume runs the
392
+ // unit again, and the re-run sends the same follow-ups. Their answers are in the renamed band, so they count nothing
393
+ // either. Every band on record is read as answers, so a slice taken out counts again whichever band holds it.
382
394
  let answeredQids = null;
383
395
  // This axis's band, and the bands a parked pass left (`<band>.tainted-<n>`), oldest first and the band's own last.
384
396
  const bandsOnRecord = () => {
@@ -393,7 +405,7 @@ export function makeExecutePlan(deps) {
393
405
  answeredQids = new Set();
394
406
  for (const band of bandsOnRecord()) {
395
407
  try {
396
- const prior = JSON.parse(readFileSync(band, "utf8"));
408
+ const prior = asAnswers(band);
397
409
  for (const b of Array.isArray(prior) ? prior : []) if (b?.qid && b.error !== true) answeredQids.add(String(b.qid));
398
410
  } catch { /* no band yet: nothing answered */ }
399
411
  }
@@ -426,7 +438,7 @@ export function makeExecutePlan(deps) {
426
438
  const seeded = new Set();
427
439
  if (qidsFilter) {
428
440
  try {
429
- const existing = JSON.parse(readFileSync(outPath, "utf8"));
441
+ const existing = bandAsAnswers(); // a parent taken out of the band releases no child
430
442
  if (Array.isArray(existing)) {
431
443
  for (const b of existing) {
432
444
  if (b && typeof b === "object" && b.qid && typeof b.state === "string" && !qidsFilter.has(b.qid)) {
@@ -695,7 +707,11 @@ export function makeExecutePlan(deps) {
695
707
  // change the answer. An unhandled fault here is an unknown of unknown durability, so it keeps the
696
708
  // transient reading. The reason names it an EXECUTOR fault so it is never read as something the
697
709
  // register said — a block that fabricated a provider response would be the worse defect.
710
+ // PAST ITS TIME TO STOP SENDING, A CALL SENDS NO NEW QUESTION (send-until.mjs). The question is not reached by
711
+ // this call and keeps whatever block the band holds for it; the call returns what it has, naming it.
712
+ const notReached = [];
698
713
  const runEntry = async (e) => {
714
+ if (pastSendUntil(tctx)) { notReached.push(e.qid); return; }
699
715
  if (driverDir && followUpClass(e, { identicalTerms }) === "counted") {
700
716
  const room = followUpRoom(driverDir, FOLLOW_UP_SEARCH_LIMIT, e, { answered: answeredBefore });
701
717
  if (!room.open) {
@@ -754,8 +770,8 @@ export function makeExecutePlan(deps) {
754
770
  // fringe) gets no block and any stale one is dropped (a fringe slice must never outlive its
755
771
  // parent's crowd verdict). This makes the initial call and every warm followup the SAME call.
756
772
  // A follow-up the net did not send is not owned by this call: whatever answer it already had stands.
757
- const refusedSet = new Set(refused);
758
- const owned = new Set(targeted.map((e) => e.qid).filter((q) => !refusedSet.has(q)));
773
+ const refusedSet = new Set(refused), notReachedSet = new Set(notReached);
774
+ const owned = new Set(targeted.map((e) => e.qid).filter((q) => !refusedSet.has(q) && !notReachedSet.has(q)));
759
775
  let preserved = [];
760
776
  try {
761
777
  const existing = JSON.parse(readFileSync(outPath, "utf8"));
@@ -793,6 +809,7 @@ export function makeExecutePlan(deps) {
793
809
  states: Object.fromEntries([...stateByQid].filter(([q]) => !seeded.has(q))),
794
810
  ...(Object.keys(spellingCounts).length ? { spelling_counts: spellingCounts } : {}),
795
811
  ...(refused.length ? { outstanding: refused } : {}),
812
+ ...(notReached.length ? { not_reached: notReached } : {}),
796
813
  }, null, 2) };
797
814
  };
798
815
  }
@@ -0,0 +1,25 @@
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
+ //
4
+ // send-until.mjs — the time after which a plan call sends no further question to the register.
5
+ //
6
+ // A plan call runs inside a model's turn, and the turn has a deadline. When the call outlasted it, the call and the
7
+ // turn ended together, so the unit could not write what the call had answered: on a production run on 2026-10-21 a
8
+ // unit's plan search ran into the register's gateway timeouts on every attempt and never wrote its result. The
9
+ // driver now hands each turn's tool servers a time to stop sending (CLEAROTRON_GATHER_SEND_UNTIL_MS, set per turn by
10
+ // the gateway, ahead of the turn's own deadline by a reserve). Past it, the executor sends no new question and a
11
+ // timed-out question is not split again; the call returns what it holds and the turn has the time to write it.
12
+ //
13
+ // A pure leaf. A call with no time to stop sending, the driver's own dispatches among them, runs as it always did.
14
+
15
+ /** The gateway's reserve for a turn of `timeoutSec`: a quarter of the turn, and never more than five minutes. */
16
+ export const sendReserveMs = (timeoutSec) => Math.min(300_000, Math.max(0, Number(timeoutSec) || 0) * 250);
17
+
18
+ /** Whether a call given `tctx` is past its time to stop sending. False when it has none. */
19
+ export function pastSendUntil(tctx, now = Date.now()) {
20
+ const until = Number(tctx?.sendUntilMs);
21
+ return Number.isFinite(until) && until > 0 && now >= until;
22
+ }
23
+
24
+ /** The call context's time to stop sending, read from the environment a turn's tool server was started with. */
25
+ export const sendUntilFromEnv = (env = process.env) => Number(env.CLEAROTRON_GATHER_SEND_UNTIL_MS) || null;
@@ -1,5 +1,9 @@
1
1
  # trademark-oauth-mcp-bridge
2
2
 
3
+ ## 0.5.0-beta.3
4
+
5
+ No changes in this release.
6
+
3
7
  ## 0.5.0-beta.2
4
8
 
5
9
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-oauth-mcp-bridge",
3
- "version": "0.5.0-beta.2",
3
+ "version": "0.5.0-beta.3",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
@@ -65,7 +65,7 @@ if (!/^\d+$/.test(String(opts.seed)) || Number(opts.seed) <= 0)
65
65
 
66
66
  // ── the inputs deriveBandShape uses, read the same way it reads them ─────────────────────────────────
67
67
  //
68
- // Deliberately mirrors driver/pipeline.mjs:441-490 mergeRegisterBands rather than inventing a gathering step. If the two
68
+ // Deliberately mirrors driver/pipeline.mjs:501-563 mergeRegisterBands rather than inventing a gathering step. If the two
69
69
  // diverge, this probe measures a shape the run never had.
70
70
 
71
71
  const runDir = opts.run;
@@ -76,7 +76,7 @@ export const COUNTED_EVENTS = {
76
76
  "post-repair-correction-unrecoverable": FAILED, "post-repair-state-not-kept": FAILED,
77
77
  "digest-flush-timeout": FAILED, "document-coverage-render-failed": FAILED, "doubts-failed": FAILED,
78
78
  "evidence-claim-invariant": { kind: FAILED, when: (e) => e.ok === false || Number(e.violations) > 0 },
79
- "floor-duty-failed": FAILED, "frame-reopen-redigest-failed": FAILED, "goods-evidence-failed": FAILED, "placement-decisions-failed": FAILED, "placement-band-ids-failed": FAILED, "late-evidence-failed": FAILED, "band-refused-records-failed": FAILED, "verdict-write-deferred": FAILED, "ranking-read-by-writer-failed": FAILED, "audit-rebuild-failed": FAILED, "audit-build-failed": FAILED, "write-up-goods-from-failed": FAILED, "house-element-ownership-failed": FAILED,
79
+ "floor-duty-failed": FAILED, "frame-reopen-redigest-failed": FAILED, "goods-evidence-failed": FAILED, "placement-decisions-failed": FAILED, "placement-band-ids-failed": FAILED, "late-evidence-failed": FAILED, "band-refused-records-failed": FAILED, "verdict-write-deferred": FAILED, "ranking-read-by-writer-failed": FAILED, "audit-rebuild-failed": FAILED, "audit-build-failed": FAILED, "write-up-goods-from-failed": FAILED, "declination-goods-states-failed": FAILED, "house-element-ownership-failed": FAILED,
80
80
  "intake-asks-missing": FAILED, "invalid-artifact-strikes": FAILED, "knockout-band-checks-inert": FAILED,
81
81
  "knockout-in-use-as-unanswered": FAILED, "knockout-ladder-unreadable": FAILED, "knockout-method-checks-inert": FAILED,
82
82
  "knockout-owner-checks-failed": FAILED, "knockout-register-records-failed": FAILED, "knockout-sweep-outage": FAILED,
@@ -231,6 +231,7 @@ export const NOT_COUNTED_EVENTS = {
231
231
  ],
232
232
  "the twin of an incident counted under another record — its dispatch, its repair, its clamp or its failure": [
233
233
  "assessment-not-completed-delivery", "part-completed", "overview-not-completed-delivery", "review-not-completed-delivery",
234
+ "core-artifact-restored", "searches-not-completed-delivery", "unit-not-completed-delivery",
234
235
  "action-reemit", "ask-answer-reemit", "authority-write-denied", "connotation-remedy", "corrections-applied",
235
236
  "corrective-cycle-receipt", "corrective-cycle-settled", "corrective-findings-stale", "coverage-absence-rendered",
236
237
  "coverage-closure", "coverage-ledger-recovered", "delivery-stale-repair", "delivery-stale-requeued", "envelope-close-rows", "escalation-noop", "floor-duty-after-repair", "writer-duty-after-repair", "writer-duty-after-correction", "guard-clause-replaced", "guard-gap-closed", "verdict-recomputed-gap-closed", "guard-clause-named-elsewhere", "audit-rebuilt", "audit-withdrawn", "record-carry-doubts-again",
@@ -71,6 +71,7 @@ export const NAMES_IN_FORCE = Object.freeze([
71
71
  "CLEAROTRON_FRAME_REOPEN_MAX",
72
72
  "CLEAROTRON_GATHER_AGENT",
73
73
  "CLEAROTRON_GATHER_CONCURRENCY",
74
+ "CLEAROTRON_GATHER_SEND_UNTIL_MS",
74
75
  "CLEAROTRON_GATHER_SESSION_ID",
75
76
  "CLEAROTRON_GATHER_SESSION_KEY",
76
77
  "CLEAROTRON_HARD_MS",
@@ -0,0 +1,65 @@
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
+ //
4
+ // slices-taken-out.mjs — a band read with the slices a check could not prove ran taken out, by every reader of it.
5
+ //
6
+ // The run's searches record (driver/parts-not-completed.mjs, its `searches` part) names each slice taken out of a band
7
+ // and delivered open: its axis, and its qid or, for a slice with none, its query. A unit re-run after the fan-in writes
8
+ // its axis's band whole, so such a slice can come back with the records nobody could prove were searched. Every reader
9
+ // that takes a band's blocks as answers reads the recorded slices out: the pipeline's merge and plan join, the answers
10
+ // the workers share, the propose tool's check on a re-proposed question, and the executor's checks on a follow-up asked
11
+ // before and on a parent before it releases a child. Each reads them out in memory and never by writing the file: a
12
+ // unit or the executor may be writing that band at the same moment.
13
+ //
14
+ // HERE AND NOT IN THE DRIVER, because the executor reads it too and nothing under providers/ imports the driver. Where
15
+ // the record sits is stated here for the same reason, once, for its writer and its readers alike.
16
+ //
17
+ // A pure leaf: node:fs and shared/driver-dir.mjs only.
18
+ import { readFileSync } from "node:fs";
19
+ import { driverDir } from "./driver-dir.mjs";
20
+
21
+ /** The folder under `_driver/` holding the record of each part a run could not complete. */
22
+ export const NOT_COMPLETED_DIR = "not-completed";
23
+
24
+ /** The record's path for `part`. */
25
+ export function notCompletedPath(runDir, part) {
26
+ return driverDir(runDir, NOT_COMPLETED_DIR, `${part}.json`);
27
+ }
28
+
29
+ /**
30
+ * The slices the run's searches record took out of the band, by axis, each named by its qid or, with none, its query.
31
+ * Empty when the run has no record, and when its record cannot be read: a damaged record names no slice. IMPURE (reads
32
+ * the record).
33
+ */
34
+ export function slicesTakenOutByAxis(runDir) {
35
+ const byAxis = new Map();
36
+ let slices = [];
37
+ try { slices = runDir ? JSON.parse(readFileSync(notCompletedPath(runDir, "searches"), "utf8"))?.slices : []; } catch { slices = []; }
38
+ for (const s of Array.isArray(slices) ? slices : []) {
39
+ if (!s?.axis || (!s?.qid && !s?.query)) continue;
40
+ if (!byAxis.has(s.axis)) byAxis.set(s.axis, []);
41
+ byAxis.get(s.axis).push(s);
42
+ }
43
+ return byAxis;
44
+ }
45
+
46
+ /**
47
+ * A band's blocks with `slices` taken out: each becomes the executor's deferred block, or is added as one. A slice with
48
+ * no qid is named by its query: the block with no qid on that query becomes a deferred block on it, and nothing is
49
+ * added where the band holds none. PURE.
50
+ */
51
+ export function bandWithSlicesOut(blocks, slices) {
52
+ const out = Array.isArray(blocks) ? [...blocks] : [];
53
+ const taken = [];
54
+ for (const s of Array.isArray(slices) ? slices : []) {
55
+ const i = s.qid ? out.findIndex((b) => b?.qid === s.qid) : out.findIndex((b) => b && !b.qid && b.query === s.query);
56
+ if (!s.qid && i < 0) continue;
57
+ const was = i >= 0 ? out[i] : null;
58
+ if (was?.error === true && was.deferred === true && was.reason === s.reason) continue;
59
+ const block = { state: "incomplete", ...(s.qid ? { qid: s.qid } : {}), query: String(was?.query ?? s.query ?? s.qid), total_hits: 0, fetched: 0,
60
+ sample: [], error: true, deferred: true, reason: String(s.reason) };
61
+ if (i >= 0) out[i] = block; else out.push(block);
62
+ taken.push(s);
63
+ }
64
+ return { blocks: out, taken };
65
+ }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: clearotron-account
3
- description: Work with a trademark clearance account: review the searches already run for it, and commission new ones. Use when the user asks about their clearance searches in general, or wants a new search set up.
3
+ description: "Work with a trademark clearance account: review the searches already run for it, and commission new ones. Use when the user asks about their clearance searches in general, or wants a new search set up."
4
4
  ---
5
5
 
6
6
  # Working with your trademark clearance searches (account connector)
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: clearotron-ops
3
- description: Drive the clearance engine as an integrator: take an intake brief, start a run, monitor it, and courier the delivery. Use when operating the engine rather than reading its output.
3
+ description: "Drive the clearance engine as an integrator: take an intake brief, start a run, monitor it, and courier the delivery. Use when operating the engine rather than reading its output."
4
4
  ---
5
5
 
6
6
  # Operating the clearance engine (ops connector) — intake + monitoring