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
@@ -224,7 +224,8 @@ export function toolWrittenArtifact(p) {
224
224
  ?? TOOL_WRITTEN_PATTERNS.find((r) => r.re.test(base))
225
225
  ?? null;
226
226
  }
227
- import { buildGatherMcpConfig, allowedToolsFor, toolGroupsForStage, recordAxisFor, seatWritesForGroups } from "./engine/mcp/gather-config.mjs";
227
+ import { buildGatherMcpConfig, allowedToolsFor, toolGroupsForStage, recordAxisFor, seatWritesForGroups, withSendUntil } from "./engine/mcp/gather-config.mjs";
228
+ import { sendReserveMs } from "../providers/_shared/send-until.mjs"; // each turn's plan calls stop sending ahead of its deadline
228
229
  import { everyToolCallRefused } from "./engine/tool-refusal.mjs"; // one definition, shared with the engine probe
229
230
  export { everyToolCallRefused };
230
231
  // The profiles STORE root, for the write boundary only — read from the module that owns it, never
@@ -297,11 +298,11 @@ export function isEmbeddedFallback(json, stderr = "") {
297
298
  // nonzero exit (the v14 delivery failure, previously mislabelled `nonzero_exit_1`); (3) any nonzero
298
299
  // exit whose wall already reached the configured budget. Distinguished from transient errors so the
299
300
  // retry policy can give a slow stage ONE longer shot instead of 3 identical doomed attempts.
300
- export function isTimeout({ killed, code, wall, stderr = "", timeoutSec }) {
301
- if (killed) return true;
302
- if (/EmbeddedAttemptSessionTakeoverError|request timed out|\bETIMEDOUT\b/i.test(stderr)) return true;
303
- if (code !== 0 && wall >= timeoutSec) return true;
304
- return false;
301
+ export const isTimeout = (facts) => timeoutCause(facts) !== null;
302
+ export function timeoutCause({ killed, code, wall, stderr = "", timeoutSec }) { // which signal fired, or null
303
+ if (killed) return "killed";
304
+ if (/EmbeddedAttemptSessionTakeoverError|request timed out|\bETIMEDOUT\b/i.test(stderr)) return "stderr";
305
+ return code !== 0 && wall >= timeoutSec ? "wall" : null;
305
306
  }
306
307
 
307
308
  // A LANE WEDGE: a timeout whose turn moved ZERO tokens — the gateway never admitted it to a command lane (the
@@ -942,6 +943,9 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
942
943
  }
943
944
  }
944
945
  }
946
+ // EACH TURN'S OWN COPY of the stage's tool config, carrying that turn's time to stop sending: ahead of the turn's
947
+ // deadline by the reserve, so a plan call returns what it holds while the turn can still write it (send-until.mjs).
948
+ const turnConfig = (sec) => withSendUntil(gatherMcpConfig, Date.now() + sec * 1000 - sendReserveMs(sec));
945
949
  let lastFail;
946
950
  // — WHAT ACTUALLY RAN, carried out to the caller. The attempt rows have held the truth all along
947
951
  // (`modelActual` from the wire, `modelUsed` from the engine's own resolver); the ladder simply never
@@ -1125,13 +1129,14 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1125
1129
  // the snapshot, which only widens the window by their own time: an earlier attempt's refusals all
1126
1130
  // precede its own settle, so they stay outside it.
1127
1131
  const dispatchedAt = Date.now();
1132
+ const mcpConfig = turnConfig(effTimeout); // this turn's copy, with its time to stop sending
1128
1133
  for (const line of describeMethodologyDrift(witnessStageMethodology(runDir, name, effMessage, engineResolveSkill)))
1129
1134
  note(`[${name}] ${line}`);
1130
1135
  // — the frozen judged-by set, hashed into THIS PROCESS'S MEMORY before the seat runs. Never
1131
1136
  // written to disk before the comparison, so the thing being watched cannot reach it. See
1132
1137
  // run-integrity.mjs for why it is not a manifest file and why the append-only journals are excluded.
1133
1138
  const integrityBefore = frozenSnapshot(runDir);
1134
- const turn = await engine.runTurn({ agent, sessionKey: key, message: effMessage, grantDispatch: base, model, thinking, timeoutSec: effTimeout, resumeRef: warm ? lastSessionRef : undefined, codexHome: stageCodexHome, mcpConfig: gatherMcpConfig, allowedTools: gatherAllowedTools, seatWrites: gatherSeatWrites, skillsDir: engineSkillsDir, skillsGrantRoots: engineSkillsGrantRoots, profilesDir: profilesStoreDir, resolveSkill: engineResolveSkill, runDir, stallSec,
1139
+ const turn = await engine.runTurn({ agent, sessionKey: key, message: effMessage, grantDispatch: base, model, thinking, timeoutSec: effTimeout, resumeRef: warm ? lastSessionRef : undefined, codexHome: stageCodexHome, mcpConfig, allowedTools: gatherAllowedTools, seatWrites: gatherSeatWrites, skillsDir: engineSkillsDir, skillsGrantRoots: engineSkillsGrantRoots, profilesDir: profilesStoreDir, resolveSkill: engineResolveSkill, runDir, stallSec,
1135
1140
  progressFiles: files, // the no-progress watchdog's artifact-advance signal (anthropic-agent; other adapters ignore it)
1136
1141
  // The raw stream of this session, beside its dispatch record (engine/session-record.mjs). The engines
1137
1142
  // keep it while the disk has room and say on the tuple's `stream` what landed.
@@ -1282,7 +1287,10 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1282
1287
  let fail = null;
1283
1288
  let stampedQuantity = null; //: a validator's own exact count, when it stamped one (see below)
1284
1289
  let failingFile = null; //: the artifact the validator rejected — the repair's only target
1285
- if (isTimeout({ killed, code, wall, stderr, timeoutSec: effTimeout })) fail = "timeout";
1290
+ // A TURN THAT COMPLETED IS JUDGED ON WHAT IT WROTE, whatever its stderr says about a request inside it
1291
+ // (attemptTimedOut). Which signal fired is kept on the row either way.
1292
+ const timedOutBy = timeoutCause({ killed, code, wall, stderr, timeoutSec: effTimeout });
1293
+ if (attemptTimedOut({ cause: timedOutBy, code, json })) fail = "timeout";
1286
1294
  else if (isEmbeddedFallback(json, stderr)) fail = "embedded_fallback";
1287
1295
  else if (code !== 0) fail = `nonzero_exit_${code}`;
1288
1296
  else if (!json) fail = "unparseable_json";
@@ -1354,7 +1362,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1354
1362
  // under the run dir to win the grant back, and warmPatchMessage renders the target through rel();
1355
1363
  // a relative path there would silently strip the write root off the one turn that needs it most.
1356
1364
  const rt = await engine.runTurn({ agent, sessionKey: key, message: repairMessage, model, thinking,
1357
- timeoutSec: effTimeout, resumeRef: repairRef, codexHome: stageCodexHome, mcpConfig: gatherMcpConfig, allowedTools: gatherAllowedTools,
1365
+ timeoutSec: effTimeout, resumeRef: repairRef, codexHome: stageCodexHome, mcpConfig: turnConfig(effTimeout), allowedTools: gatherAllowedTools,
1358
1366
  skillsDir: engineSkillsDir, skillsGrantRoots: engineSkillsGrantRoots, profilesDir: profilesStoreDir, resolveSkill: engineResolveSkill, runDir, stallSec,
1359
1367
  progressFiles: files, streamFile: streamFilePath(runDir, name, attempt, formRepairsUsed + 1) });
1360
1368
  repairRef = rt.sessionRef ?? repairRef;
@@ -1819,6 +1827,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1819
1827
  // `followup` matters too: a followup success PATCHES the prior session's output, it never
1820
1828
  // supersedes a taint the way a fresh full pass does.
1821
1829
  killed: killed || undefined,
1830
+ timeoutCause: timedOutBy ?? undefined, // — which of the three timeout signals fired: killed, stderr or wall
1822
1831
  signals: turn.signals ?? undefined,
1823
1832
  //: how long every declared artifact had been untouched when the turn settled. Present on any
1824
1833
  // hard-wall kill the wall rescue looked at — INCLUDING the ones it refused, so "considered and
@@ -1899,7 +1908,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
1899
1908
  cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}), cliSource: cli.source,
1900
1909
  cliFloor: cli.floor ?? null, cliBelowFloor: cli.belowFloor ?? null,
1901
1910
  wrote, warm: warm || undefined, warmEscalated: attempt === warmEscalatedAt || undefined,
1902
- rescued: rescued ?? undefined, killed: killed || undefined,
1911
+ rescued: rescued ?? undefined, killed: killed || undefined, timeoutCause: timedOutBy ?? undefined,
1903
1912
  quiescentMs: Number.isFinite(quiescentMs) ? Math.round(quiescentMs) : undefined, // — see the per-stage row
1904
1913
  rescueRefused: rescueRefused ?? undefined, // — the cause, when the rescue looked and refused
1905
1914
  rescueRefusedFile: rescueRefusedFile ?? undefined, rescueRefusedReason: rescueRefusedReason ?? undefined,
@@ -3735,3 +3744,15 @@ function toolCallCounts(turn) {
3735
3744
  return { toolCallsRefused: n(turn?.toolCallsRefused), commandToolCalls: n(turn?.commandToolCalls), mcpToolCalls: n(turn?.mcpToolCalls),
3736
3745
  toolCallsErroredByName: errored && typeof errored === "object" && !Array.isArray(errored) ? { ...errored } : null };
3737
3746
  }
3747
+
3748
+ /**
3749
+ * IS THIS ATTEMPT A TIMEOUT? A kill and a run past the limit with a non-zero exit always are. A timed-out request named
3750
+ * in stderr is one only when the turn did not complete: a turn that exited 0 with its engine's status ok outlived the
3751
+ * request, which was a call inside it (a web lookup, a tool), and is judged on what it wrote like any finished turn.
3752
+ * Recorded as a timeout, it was run again whole: on one test run a writer finished in 14.7 minutes of its 43, exited 0
3753
+ * with its output written, and paid a second attempt of 20.3 minutes. `cause` is `timeoutCause`'s answer. PURE.
3754
+ */
3755
+ export function attemptTimedOut({ cause, code, json }) {
3756
+ if (!cause) return false;
3757
+ return !(cause === "stderr" && code === 0 && json?.status === "ok");
3758
+ }
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
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": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
8
8
  "engines": {
@@ -17,35 +17,40 @@
17
17
  //
18
18
  // ONCE PER PART. The catch never writes a part whose record already exists: a failure on the pass that delivers
19
19
  // without it ends the run as it always did, with the stop notice, and the record is removed then, so a later resume
20
- // tries the part again.
20
+ // tries the part again. The searches are the one part whose record grows: each failure names the slices it could not
21
+ // prove ran, and a failure that names a slice the record does not hold yet adds it; one that names none new ends the
22
+ // run, so the run still ends.
21
23
  //
22
- // A pure leaf: node:fs, node:path and shared/driver-dir.mjs only.
24
+ // A pure leaf: node:fs, node:path, shared/driver-dir.mjs and shared/slices-taken-out.mjs (node:fs and the same) only.
23
25
  import { readFileSync, writeFileSync, renameSync, rmSync, readdirSync } from "node:fs";
24
26
  import { join } from "node:path";
25
27
  import { driverDir, ensureDriverDir } from "../shared/driver-dir.mjs";
28
+ // Where the record sits is stated with the band readers outside the driver, which read the searches' record too.
29
+ import { NOT_COMPLETED_DIR, notCompletedPath } from "../shared/slices-taken-out.mjs";
26
30
 
27
- export const NOT_COMPLETED_DIR = "not-completed";
31
+ export { NOT_COMPLETED_DIR, notCompletedPath };
28
32
 
29
33
  // The covered parts. `assessment` is the run's overall assessment: placement, the register write-up and synthesis, and
30
34
  // everything built from them. `review` is the first review of the assessment, whose verdict the sign-off is. `overview`
31
35
  // is the report overview, the conclusion under the rating. `delivery` is the
32
36
  // changed-material check before publishing: it never delivers past staleness, and its record buys the report one more
33
- // assembly from the current material instead of the stop notice.
34
- export const NOT_COMPLETED_PARTS = Object.freeze(["assessment", "review", "overview", "delivery"]);
35
-
36
- /** The record's path for `part`. */
37
- export function notCompletedPath(runDir, part) {
38
- return driverDir(runDir, NOT_COMPLETED_DIR, `${part}.json`);
39
- }
37
+ // assembly from the current material instead of the stop notice. `searches` is the register's searches: the slices a
38
+ // check could not prove ran, each with its axis, its qid and the reason it is open, taken out of the band and
39
+ // delivered as open rows; the axes whose pass was killed at its time limit, read as the last pass left them and
40
+ // disclosed, their clean claims open (`disclosed_axes`); and the units that wrote no result once their attempts were
41
+ // spent, delivered as their bands stand and not sent again (`unfinished_units`).
42
+ export const NOT_COMPLETED_PARTS = Object.freeze(["assessment", "review", "overview", "delivery", "searches"]);
40
43
 
41
44
  /**
42
45
  * Record that `part` could not be completed. `stage` is the stage that failed, `reason` its cause for the run's record
43
46
  * (never a reader's cell), `sig` and `terminalKind` what the recovery decision said. Written through a temporary file
44
47
  * and a rename, so a reader never sees half a record. Returns the record.
45
48
  */
46
- export function writePartNotCompleted(runDir, part, { stage = null, reason = null, sig = null, terminalKind = null } = {}) {
49
+ export function writePartNotCompleted(runDir, part, { stage = null, reason = null, sig = null, terminalKind = null, slices = null, disclosedAxes = null, unfinishedUnits = null } = {}) {
47
50
  if (!NOT_COMPLETED_PARTS.includes(part)) throw new Error(`parts-not-completed: "${part}" is not a covered part`);
48
51
  const record = { part, stage, reason: reason == null ? null : String(reason).slice(0, 600), sig, terminalKind,
52
+ ...(Array.isArray(slices) ? { slices } : {}), ...(Array.isArray(disclosedAxes) && disclosedAxes.length ? { disclosed_axes: disclosedAxes } : {}),
53
+ ...(Array.isArray(unfinishedUnits) && unfinishedUnits.length ? { unfinished_units: unfinishedUnits } : {}),
49
54
  ts: new Date().toISOString() };
50
55
  const path = notCompletedPath(runDir, part);
51
56
  ensureDriverDir(runDir, NOT_COMPLETED_DIR);
@@ -76,6 +81,42 @@ export function partsNotCompleted(runDir) {
76
81
  return NOT_COMPLETED_PARTS.filter((p) => names.includes(`${p}.json`)).map((p) => readPartNotCompleted(runDir, p));
77
82
  }
78
83
 
84
+ /**
85
+ * The slices of `failed` that `record` does not hold yet, each `{axis, qid, reason}`, or `{axis, query, reason}` for a
86
+ * slice with no qid, which its query names: what a failure of the searches adds to their record. [] when it adds
87
+ * none. PURE.
88
+ */
89
+ export function slicesNotYetRecorded(record, failed) {
90
+ const keyOf = (s) => `${s?.axis}\u0000${s?.qid ? `qid:${s.qid}` : `query:${s?.query}`}`;
91
+ const held = new Set((Array.isArray(record?.slices) ? record.slices : []).map(keyOf));
92
+ const out = [];
93
+ for (const s of Array.isArray(failed) ? failed : []) {
94
+ if (!s?.axis || (!s?.qid && !s?.query) || held.has(keyOf(s))) continue;
95
+ held.add(keyOf(s));
96
+ out.push(s.qid ? { axis: String(s.axis), qid: String(s.qid), reason: String(s.reason ?? "") }
97
+ : { axis: String(s.axis), query: String(s.query), reason: String(s.reason ?? "") });
98
+ }
99
+ return out;
100
+ }
101
+
102
+ /**
103
+ * The axes of `failed` that `record` does not list as disclosed yet: a pass on each was killed at its time limit and its
104
+ * recovery is spent, so the delivering pass reads its band as it stands and discloses it. [] when it adds none. PURE.
105
+ */
106
+ export function axesNotYetDisclosed(record, failed) {
107
+ const held = new Set(Array.isArray(record?.disclosed_axes) ? record.disclosed_axes : []);
108
+ return [...new Set((Array.isArray(failed) ? failed : []).map((a) => String(a ?? "").trim()).filter((a) => a && !held.has(a)))];
109
+ }
110
+
111
+ /**
112
+ * The units of `failed` that `record` does not list as unfinished yet: each wrote no result once its attempts were spent,
113
+ * so the delivering pass takes it as its band stands instead of sending it again. [] when it adds none. PURE.
114
+ */
115
+ export function unitsNotYetRecorded(record, failed) {
116
+ const held = new Set(Array.isArray(record?.unfinished_units) ? record.unfinished_units : []);
117
+ return [...new Set((Array.isArray(failed) ? failed : []).map((a) => String(a ?? "").trim()).filter((a) => a && !held.has(a)))];
118
+ }
119
+
79
120
  /** Remove one part's record, once that part has completed after all. */
80
121
  export function clearPartNotCompleted(runDir, part) {
81
122
  try { rmSync(notCompletedPath(runDir, part), { force: true }); } catch { /* none */ }
@@ -64,7 +64,7 @@ import { writeRunStatus, readRunStatus, rollupStatus, atomicWrite, identitySeed
64
64
  import { batchMarkName } from "./mark-name.mjs";
65
65
  import { runLog, note, outputMeta } from "./log.mjs";
66
66
  import { defaultTerritoryState } from "./effective-scope.mjs"; // the stored-defaults reading — one producer, shared with the clearance lane
67
- import { AGENT_WHATSAPP, whatsappRouting } from "./stages.mjs";
67
+ import { whatsappRouting } from "./stages.mjs";
68
68
  import { writeOutboxPacket } from "./outbox.mjs";
69
69
  import { rollupTokens, stampTokenRollup } from "./tokens.mjs";
70
70
  import { recordRunConsumption } from "./consumption-ledger.mjs";
@@ -1205,8 +1205,9 @@ export async function knockoutInner(ctx, job, opts = {}) {
1205
1205
  // two existing. Where no number is held the packet now SAYS so in `whatsappToReason` rather than
1206
1206
  // substituting the operator silently.
1207
1207
  //
1208
- // The failure notice below deliberately keeps `AGENT_WHATSAPP[agent]`: 289 ruled on the COMPLETION
1209
- // notice, and a stage failure is an operational page rather than an answer the requester ordered.
1208
+ // The failure notice below routes the same way (buildFailurePacket). It used to keep `AGENT_WHATSAPP[agent]` as an
1209
+ // operational page, but the courier relays it to the requester, who is owed it as they are owed a report, and the
1210
+ // operator keeps the page as the copy beside it.
1210
1211
  ...whatsappRouting(job, agent),
1211
1212
  // ONE LINE, N LINKS. This said "Report: <one url>" and on a batch that url is now null, which would
1212
1213
  // have read "Report: null" — the fail-visible shape doing its job, and still not a line to send. A
@@ -1350,7 +1351,7 @@ export async function knockoutInner(ctx, job, opts = {}) {
1350
1351
  try {
1351
1352
  const rich = buildFailurePacket({
1352
1353
  runId, agent, job, failedStage, shortReason, terminalKind,
1353
- reasonVerbatim: String(reason).slice(0, 1000), whatsappTo: AGENT_WHATSAPP[agent] ?? null,
1354
+ reasonVerbatim: String(reason).slice(0, 1000),
1354
1355
  });
1355
1356
  const packet = {
1356
1357
  kind: "run-failed", ...rich,