clearotron 0.5.0-beta.2 → 0.5.0-beta.4
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/03-run-lifecycle.md +6 -0
- package/docs/architecture/04-configuration-reference.md +1 -0
- package/docs/architecture/05-config-governance.md +1 -1
- package/driver/CHANGELOG.md +21 -0
- package/driver/band-shape.mjs +19 -11
- 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/band-server.mjs +12 -12
- package/driver/engine/mcp/clarivate-server.mjs +2 -0
- package/driver/engine/mcp/codex-config.mjs +12 -7
- package/driver/engine/mcp/corsearch-server.mjs +2 -0
- package/driver/engine/mcp/declination-server.mjs +4 -5
- package/driver/engine/mcp/dispositions-server.mjs +7 -0
- 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/perplexity-server.mjs +29 -1
- package/driver/engine/mcp/recording-server.mjs +16 -1
- package/driver/engine/mcp/signa-server.mjs +2 -0
- package/driver/engine/mcp/supplemental.mjs +11 -3
- package/driver/engine/mcp/uspto-local-server.mjs +2 -0
- package/driver/engine/openai-agent.mjs +48 -9
- package/driver/envelope-settle.mjs +13 -3
- package/driver/findings-model.mjs +17 -0
- 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 +474 -81
- package/driver/placement-decisions.mjs +24 -9
- package/driver/placement-form-io.mjs +21 -1
- package/driver/placement-form.mjs +25 -15
- package/driver/placement-union.mjs +53 -8
- package/driver/predelivery-lint.mjs +8 -1
- package/driver/publish/index.mjs +7 -3
- package/driver/register-digest-record.mjs +6 -3
- package/driver/research-answers.mjs +54 -7
- package/driver/runner.mjs +4 -4
- package/driver/saved-findings-block.mjs +36 -0
- package/driver/skills/clearance-register/digest.md +1 -1
- package/driver/skills/clearance-search/phase2-execution.md +7 -5
- package/driver/skills/clearance-search/synthesis-rules.md +3 -3
- package/driver/skills/placement-inquiry/SKILL.md +3 -3
- package/driver/stage-context.mjs +18 -0
- package/driver/stages.mjs +41 -13
- package/driver/suite-census.json +140 -14
- package/driver/synthesis-record.mjs +167 -1
- package/driver/verify.mjs +6 -6
- package/mcp-server/CHANGELOG.md +8 -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 +33 -7
- package/providers/_shared/send-until.mjs +25 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -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/scripts/release-notes-lint.mjs +1 -1
- package/shared/inside-run.mjs +54 -0
- 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
|
@@ -60,6 +60,7 @@ import { MAX_ROWS_PER_CALL } from "../../disposition-call.mjs";
|
|
|
60
60
|
// The lane's second statement, and a different one: which coverage units were searched to what end. Its
|
|
61
61
|
// own module and its own file, never a row in the disposition form — see record_coverage_status below.
|
|
62
62
|
import { recordCoverageStatus, COMMON_LAW_COVERAGE_STATUSES } from "../../common-law-coverage-status.mjs";
|
|
63
|
+
import { insideRun } from "../../../shared/inside-run.mjs"; // a path a tool is handed is served only inside this run
|
|
63
64
|
|
|
64
65
|
// ── B — THE TYPED DISPOSITION TRANSPORT ─────────────────────────────────────────────────────────────
|
|
65
66
|
//
|
|
@@ -76,11 +77,17 @@ function specFrom(params) {
|
|
|
76
77
|
const { grid_spec_path } = params ?? {};
|
|
77
78
|
if (!grid_spec_path)
|
|
78
79
|
return { error: "ERROR: grid_spec_path is required — it is the same driver-written spec path the grid tool was given. Do not compose a path." };
|
|
80
|
+
// THIS RUN'S SPEC AND LEDGER ONLY (shared/inside-run.mjs): the run folder is the one the driver wired, never the call's.
|
|
81
|
+
const runDir = process.env.CLEAROTRON_BAND_RUN_DIR || "";
|
|
82
|
+
if (runDir && !insideRun(runDir, grid_spec_path))
|
|
83
|
+
return { error: "ERROR: grid_spec_path is outside this run's folder. The driver writes the spec inside the run; use the path it gave you." };
|
|
79
84
|
let spec;
|
|
80
85
|
try { spec = validateGridSpec(JSON.parse(readFileSync(grid_spec_path, "utf8"))); }
|
|
81
86
|
catch (err) { return { error: `ERROR: grid_spec_path unreadable/invalid (${err.message}). The driver writes this file; do not hand-author it.` }; }
|
|
82
87
|
if (!underStudioSegment(spec.output_path)) // either spelling: an install keeps the studio segment it has
|
|
83
88
|
return { error: `ERROR: grid spec.output_path must be within a studio/clearance-search run dir; got ${spec.output_path}` };
|
|
89
|
+
if (runDir && !insideRun(runDir, spec.output_path))
|
|
90
|
+
return { error: "ERROR: the grid spec's output_path is outside this run's folder. The driver writes the spec inside the run; use the path it gave you." };
|
|
84
91
|
return { spec };
|
|
85
92
|
}
|
|
86
93
|
|
|
@@ -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
|
}
|
|
@@ -29,7 +29,8 @@ import { parsePrRiskResults, connotationObligations, renderConnotationObligation
|
|
|
29
29
|
// What stays is the GRID path's own need — the obligations sidecar written beside the grid's ledger.
|
|
30
30
|
import { obligationsSidecarPath } from "../../disposition-tool.mjs";
|
|
31
31
|
import { writeGridProvenance } from "./grid-provenance.mjs"; // — what served the grid, beside the ledger
|
|
32
|
-
import {
|
|
32
|
+
import { insideRun } from "../../../shared/inside-run.mjs"; // a path a tool is handed is served only inside this run
|
|
33
|
+
import { keepResearchAnswer, researchReplayOn, savedResearchAnswer } from "../../research-answers.mjs"; // each answer's text, kept for a later attempt of its stage; a replay answers from them
|
|
33
34
|
|
|
34
35
|
const API_KEY = process.env.PERPLEXITY_API_KEY || "";
|
|
35
36
|
|
|
@@ -219,14 +220,21 @@ function judgeBlock(cands, part, gridSpecPath, reserve = 0) {
|
|
|
219
220
|
}
|
|
220
221
|
|
|
221
222
|
async function research(params) {
|
|
223
|
+
// A REPLAY SENDS NOTHING OUT, so it is answered before the key is asked for: an offline replay needs none.
|
|
224
|
+
if (researchReplayOn(RUN_DIR)) return replayResearch(params);
|
|
222
225
|
if (!API_KEY) return "ERROR: PERPLEXITY_API_KEY not set — marketplace research unavailable.";
|
|
223
226
|
const { task, depth, model: modelOverride, allow_fetch = false, domain_filter, enable_sandbox = false, response_schema, schema_name, grid_spec_path, part } = params;
|
|
224
227
|
|
|
225
228
|
// ── Deterministic grid mode: the caller dictates the cells; the tool runs + captures them. ──
|
|
226
229
|
if (grid_spec_path) {
|
|
230
|
+
// THIS RUN'S SPEC, WRITING THIS RUN'S LEDGER: a spec, or a ledger it names, in another run's folder is never served.
|
|
231
|
+
if (RUN_DIR && !insideRun(RUN_DIR, grid_spec_path))
|
|
232
|
+
return "ERROR: grid_spec_path is outside this run's folder. The driver writes the spec inside the run; use the path it gave you.";
|
|
227
233
|
let spec;
|
|
228
234
|
try { spec = validateGridSpec(JSON.parse(readFileSync(grid_spec_path, "utf8"))); }
|
|
229
235
|
catch (err) { return `ERROR: grid_spec_path unreadable/invalid (${err.message}). The driver writes this file; do not hand-author it.`; }
|
|
236
|
+
if (RUN_DIR && !insideRun(RUN_DIR, spec.output_path))
|
|
237
|
+
return requiredLedgerRefusal("ERROR: the grid spec's output_path is outside this run's folder. The driver writes the spec inside the run; use the path it gave you.", { spec, gridSpecPath: grid_spec_path });
|
|
230
238
|
if (!underStudioSegment(spec.output_path)) // either spelling: an install keeps the studio segment it has
|
|
231
239
|
return requiredLedgerRefusal(`ERROR: grid spec.output_path must be within a studio/clearance-search run dir; got ${spec.output_path}`, { spec, gridSpecPath: grid_spec_path });
|
|
232
240
|
// — already recorded and complete? Answer from the ledger; do not buy the grid twice. This is also
|
|
@@ -309,6 +317,26 @@ async function research(params) {
|
|
|
309
317
|
}
|
|
310
318
|
}
|
|
311
319
|
|
|
320
|
+
// ── A REPLAY: THE RUN'S OWN ANSWER, OR NOTHING ─────────────────────────────────────────────────────────
|
|
321
|
+
//
|
|
322
|
+
// In the experiment sandbox under `--research replay` (research-answers.mjs), a question the run already asked
|
|
323
|
+
// gets the answer the run got, and any other question, a grid among them, is refused: nothing goes out to the
|
|
324
|
+
// research service. Both are logged as a lookup is, marked as a replay, so the arm's reading log says which.
|
|
325
|
+
function replayResearch(params) {
|
|
326
|
+
const { task, grid_spec_path } = params ?? {};
|
|
327
|
+
if (grid_spec_path) {
|
|
328
|
+
logCall("perplexity_research", { task: task ?? "", grid: true, replay: true }, { ok: false, error: "replay: grid not replayed" });
|
|
329
|
+
return "ERROR: replay — this replays the run's saved research answers and runs no grid; nothing was sent to the research service.";
|
|
330
|
+
}
|
|
331
|
+
const saved = savedResearchAnswer(RUN_DIR, task);
|
|
332
|
+
if (!saved) {
|
|
333
|
+
logCall("perplexity_research", { task: task ?? "", replay: true }, { ok: false, error: "replay: no saved answer" });
|
|
334
|
+
return "ERROR: replay — the run asked no question worded exactly like this one, so there is no saved answer to give; nothing was sent to the research service.";
|
|
335
|
+
}
|
|
336
|
+
logCall("perplexity_research", { task, replay: true }, { ok: true, bytes: saved.text.length, replayed: true });
|
|
337
|
+
return saved.text;
|
|
338
|
+
}
|
|
339
|
+
|
|
312
340
|
serve({
|
|
313
341
|
name: "perplexity", version: "0.1.0",
|
|
314
342
|
tools: [{
|
|
@@ -1046,7 +1046,10 @@ serve({
|
|
|
1046
1046
|
"are. The driver renders narrative.md and findings.json. Your coverage rows are joined to this " +
|
|
1047
1047
|
"run's plan-execution receipt as you send them, so a clean claim over a slice that did not run is " +
|
|
1048
1048
|
"refused here — in this turn, where restating it is free — rather than at a gate that can only " +
|
|
1049
|
-
"re-ask the whole stage. The answer tells you what was stored, or names the exact defect token."
|
|
1049
|
+
"re-ask the whole stage. The answer tells you what was stored, or names the exact defect token. " +
|
|
1050
|
+
"Save each finding as you complete it, alone or a few at a time, with `save_findings`: each is checked as " +
|
|
1051
|
+
"the record checks a finding and kept the moment it passes, so a stopped attempt loses none of them. The " +
|
|
1052
|
+
"call that sends the whole record takes every saved finding for a position it does not carry itself.",
|
|
1050
1053
|
inputSchema: {
|
|
1051
1054
|
type: "object",
|
|
1052
1055
|
// ── NO BLANKET `required`, AND THAT IS THE FIX FOR ─────────────────────────
|
|
@@ -1079,6 +1082,18 @@ serve({
|
|
|
1079
1082
|
// reads the run's own accepted-call record rather than trusting the shape. That check is older
|
|
1080
1083
|
// than this change and is the reason relaxing `required` costs nothing.
|
|
1081
1084
|
properties: {
|
|
1085
|
+
// A COMPLETED FINDING, KEPT BEFORE THE WHOLE RECORD EXISTS (synthesis-record.mjs, `saveFindings`). Its items
|
|
1086
|
+
// are findings as the record carries them, declared from the same key ladder as a patch row.
|
|
1087
|
+
save_findings: {
|
|
1088
|
+
type: "array",
|
|
1089
|
+
description:
|
|
1090
|
+
"COMPLETED FINDINGS, saved before the whole record exists. Send one or a few complete finding objects as the findings record carries them, and nothing else on the call. Each is checked as the record checks a finding and kept as soon as it passes; one that is refused leaves the others saved. A finding saved again for the same position replaces the earlier one. Saving writes no report: the record is complete only when you send `narrative` with `findings`, and that call takes every saved finding for a position it does not carry.",
|
|
1091
|
+
items: {
|
|
1092
|
+
type: "object",
|
|
1093
|
+
description: "One complete finding object, as the findings record carries it.",
|
|
1094
|
+
properties: Object.fromEntries(FINDING_KEYS_CURRENT.map((k) => [k, { description: `Part of the finding record; see findings-model.mjs for what \`${k}\` carries.` }])),
|
|
1095
|
+
},
|
|
1096
|
+
},
|
|
1082
1097
|
findings_patch: {
|
|
1083
1098
|
type: "array",
|
|
1084
1099
|
description:
|
|
@@ -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) => {
|
|
@@ -34,8 +34,10 @@ import { dirname, join } from "node:path";
|
|
|
34
34
|
import { driverDir } from "../../../shared/driver-dir.mjs"; //
|
|
35
35
|
import { PLAN_PREDICATES, PLAN_MAX_OR_WIDTH, PLAN_MAX_NAME_LENGTH, planExactOrWidth, planPhoneticOrWidth, fingerprint, ownerIntersectionGap, goodsTextGap, resolveRegions, houseElementOf, withoutHouseElementTerms, containsFormSubstitution } from "../../register-plan.mjs";
|
|
36
36
|
import { entryTermIssues } from "../../../providers/_shared/term-shape.mjs";
|
|
37
|
-
import { OUTSTANDING_REASON } from "../../../providers/_shared/follow-up-searches.mjs";
|
|
37
|
+
import { OUTSTANDING_REASON, runDriverDirOf } from "../../../providers/_shared/follow-up-searches.mjs";
|
|
38
|
+
import { pathInRun } from "../../../shared/inside-run.mjs"; // the band path is served only inside this run
|
|
38
39
|
import { heldAnswers, questionKey } from "./held-answers.mjs";
|
|
40
|
+
import { bandWithSlicesOut, slicesTakenOutByAxis } from "../../../shared/slices-taken-out.mjs";
|
|
39
41
|
import { readWithheldFamilies } from "../../withheld-families.mjs";
|
|
40
42
|
import { addedClassBound, boundAddedClasses } from "../../frame-diff-model.mjs"; // an added class takes one question, the identical mark
|
|
41
43
|
import { scriptQuestionsByMarket } from "../../registration-scripts.mjs"; // one script question per market on the plan's list
|
|
@@ -390,6 +392,10 @@ export async function proposeSupplemental(params, tctx, deps) {
|
|
|
390
392
|
const proposals = Array.isArray(params?.proposals) ? params.proposals : null;
|
|
391
393
|
if (!axis || !outPath || !proposals || !proposals.length)
|
|
392
394
|
return { type: "text", text: "ERROR: axis, output_path and a non-empty proposals[] are all required." };
|
|
395
|
+
// The supplemental plan is written beside the band, so the band path must be this run's before anything is written.
|
|
396
|
+
const runOf = runDriverDirOf(tctx?.recordLog);
|
|
397
|
+
if (runOf && pathInRun(dirname(runOf), outPath) === null)
|
|
398
|
+
return { type: "text", text: "ERROR: output_path is outside this run's folder. Use the band path the driver gave you." };
|
|
393
399
|
|
|
394
400
|
const suppPath = join(dirname(outPath), `${axis}-supplemental-plan.json`);
|
|
395
401
|
// ── the supplemental plan INHERITS the frozen plan's regions (review findings 4/12) ───────────────
|
|
@@ -573,11 +579,13 @@ export async function proposeSupplemental(params, tctx, deps) {
|
|
|
573
579
|
}
|
|
574
580
|
|
|
575
581
|
// 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,
|
|
582
|
+
// answer, a failed one, one whose slice the run took out of the band because a check could not prove it ran
|
|
583
|
+
// (shared/slices-taken-out.mjs), or one this call enriched or gave its markets runs as before.
|
|
577
584
|
let reusedHeld = [];
|
|
578
585
|
try {
|
|
579
586
|
const own = new Map();
|
|
580
|
-
|
|
587
|
+
const { blocks } = bandWithSlicesOut(JSON.parse(readFileSync(outPath, "utf8")), slicesTakenOutByAxis(dirname(dirname(outPath))).get(axis));
|
|
588
|
+
for (const b of blocks) if (b?.qid) own.set(String(b.qid), b);
|
|
581
589
|
reusedHeld = reused.filter((q) => { const b = own.get(String(q)); return b && b.error !== true && !enrichedQids.includes(q) && !staleOnDisk; });
|
|
582
590
|
} catch { reusedHeld = []; }
|
|
583
591
|
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) => {
|
|
@@ -317,8 +317,10 @@ export function sweepStaleCodexHomes(root, { now = Date.now(), alive = processAl
|
|
|
317
317
|
for (const e of entries) {
|
|
318
318
|
if (!e.isDirectory() || !/^(?:stage|turn)-/.test(e.name)) continue;
|
|
319
319
|
const dir = join(root, e.name);
|
|
320
|
-
|
|
321
|
-
const
|
|
320
|
+
// A home's working folder is judged by its home's name, and goes when its home's maker has ended.
|
|
321
|
+
const home = e.name.endsWith(WORK_SUFFIX) ? e.name.slice(0, -WORK_SUFFIX.length) : e.name;
|
|
322
|
+
if (OWNED.has(join(root, home))) continue;
|
|
323
|
+
const maker = home.match(MAKER_RE);
|
|
322
324
|
let stale;
|
|
323
325
|
if (maker) stale = Number(maker[1]) !== process.pid && !alive(Number(maker[1]));
|
|
324
326
|
else { try { stale = now - statSync(dir).mtimeMs > STALE_UNMARKED_MS; } catch { stale = false; } }
|
|
@@ -329,7 +331,7 @@ export function sweepStaleCodexHomes(root, { now = Date.now(), alive = processAl
|
|
|
329
331
|
}
|
|
330
332
|
|
|
331
333
|
const removeOwned = () => {
|
|
332
|
-
for (const dir of OWNED) { try { rmSync(
|
|
334
|
+
for (const dir of OWNED) for (const d of [dir, workFolderOf(dir)]) { try { rmSync(d, { recursive: true, force: true }); } catch { /* best-effort */ } }
|
|
333
335
|
OWNED.clear();
|
|
334
336
|
};
|
|
335
337
|
|
|
@@ -363,13 +365,46 @@ export function makeCodexHome(prefix) {
|
|
|
363
365
|
return dir;
|
|
364
366
|
}
|
|
365
367
|
|
|
366
|
-
/** Remove a home this process made. Best-effort: never throws. */
|
|
368
|
+
/** Remove a home this process made, and its working folder. Best-effort: never throws. */
|
|
367
369
|
export function releaseCodexHome(dir) {
|
|
368
370
|
if (!dir) return;
|
|
369
371
|
OWNED.delete(dir);
|
|
370
|
-
try { rmSync(
|
|
372
|
+
for (const d of [dir, workFolderOf(dir)]) { try { rmSync(d, { recursive: true, force: true }); } catch { /* best-effort */ } }
|
|
371
373
|
}
|
|
372
374
|
|
|
375
|
+
// ── A STAGE'S TEMPORARY FILES ARE ITS OWN ─────────────────────────────────────────────────────────────
|
|
376
|
+
//
|
|
377
|
+
// The stage's profile granted the account's temp folder and `/tmp`, both shared by every stage of every run on
|
|
378
|
+
// the machine, and nothing removed what a stage left there. In a test run of a crowded matter, 2026-10-27, the
|
|
379
|
+
// writer's commands found and read the working notes, a state file with ratings, and a families file that an
|
|
380
|
+
// earlier run's writer had left in `/tmp` the day before. A run must read nothing from another run.
|
|
381
|
+
//
|
|
382
|
+
// So each home has one working folder of its own, beside it and never inside it (the home holds the sign-in,
|
|
383
|
+
// which the profile never names). The turn's TMPDIR, TMP and TEMP name it, and the profile grants it in place
|
|
384
|
+
// of the shared folders. It lives as long as its home: a stage's ladder shares one home across its attempts,
|
|
385
|
+
// its warm resumes and its repair turns, so they keep the notes they made, and the folder goes when the ladder
|
|
386
|
+
// settles. A home whose maker has ended takes its working folder with it in the sweep.
|
|
387
|
+
const WORK_SUFFIX = ".work";
|
|
388
|
+
/** The working folder of a Codex home. PURE. */
|
|
389
|
+
export const workFolderOf = (home) => `${home}${WORK_SUFFIX}`;
|
|
390
|
+
/** Make a home's working folder if it is not there yet, private to the account. Returns its path. */
|
|
391
|
+
export function ensureWorkFolder(home) {
|
|
392
|
+
const dir = workFolderOf(home);
|
|
393
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
394
|
+
return dir;
|
|
395
|
+
}
|
|
396
|
+
/** The turn's environment with every temp-folder name pointing at the home's working folder. PURE. */
|
|
397
|
+
export function withWorkFolder(env, dir) {
|
|
398
|
+
return { ...env, TMPDIR: dir, TMP: dir, TEMP: dir };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// WHAT THE MODEL IS TOLD, as the design ruling of 2026-10-27 approved it (its section on isolation). It describes the
|
|
402
|
+
// mechanism and replaces nothing: the profile is what keeps another run's files out of reach. Codex alone carries it,
|
|
403
|
+
// since only a Codex stage runs commands and only a Codex turn has a working folder of its own.
|
|
404
|
+
export const TEMP_FOLDER_RULE = "Keep temporary working files in the directory given by TMPDIR. Use only this run's supplied evidence and accepted work.";
|
|
405
|
+
/** The instructions every Codex turn carries: the output rule, then the temp-folder rule. */
|
|
406
|
+
export const CODEX_DEVELOPER_INSTRUCTIONS = `${WRITE_DISCIPLINE} ${TEMP_FOLDER_RULE}`;
|
|
407
|
+
|
|
373
408
|
/**
|
|
374
409
|
* The folder holding codex's own programs, which the stage's profile must let it read. codex runs every
|
|
375
410
|
* shell command through a helper of its own inside the sandbox, so a profile that cannot read the helper
|
|
@@ -884,7 +919,10 @@ export const openaiAgentEngine = {
|
|
|
884
919
|
let authMaster = null, seeded = null;
|
|
885
920
|
try {
|
|
886
921
|
// Auth toggle (may throw loud on api-key-without-key — a config error, not a retryable failure).
|
|
887
|
-
const { env, mode } = spawnEnv(process.env, codexHome);
|
|
922
|
+
const { env: baseEnv, mode } = spawnEnv(process.env, codexHome);
|
|
923
|
+
// The home's own working folder, as the turn's temp folder, with the sandbox on or off.
|
|
924
|
+
const workDir = ensureWorkFolder(codexHome);
|
|
925
|
+
const env = withWorkFolder(baseEnv, workDir);
|
|
888
926
|
if (mode === "subscription") {
|
|
889
927
|
const af = authFilePath(process.env);
|
|
890
928
|
if (!existsSync(af))
|
|
@@ -906,16 +944,17 @@ export const openaiAgentEngine = {
|
|
|
906
944
|
// backstop if a tool consumes the lot.
|
|
907
945
|
//
|
|
908
946
|
// — the fence: with the sandbox on, the stage's commands read only the instruction trees, the run
|
|
909
|
-
// folder and the
|
|
947
|
+
// folder and the home's working folder (codex-config.mjs, `fenceToml`). The same roots claude's file
|
|
948
|
+
// tools get, and a working folder claude needs none of, since no claude stage runs a command.
|
|
910
949
|
//
|
|
911
950
|
// — what those commands inherit: none of the register and research keys the program holds for its
|
|
912
951
|
// tool servers, and not the Codex key (engine-env.mjs, `codexCommandWithheld`), with codex's shell
|
|
913
952
|
// snapshot off because it replays them (codex-config.mjs, `commandEnvToml`). With the sandbox on or
|
|
914
953
|
// off, since the bypass builds no fence and a command could otherwise print them.
|
|
915
954
|
const fence = codexSandboxBypassed() ? null
|
|
916
|
-
: { runDir, readRoots: [...(skillsGrantRoots?.length ? skillsGrantRoots : [skillsDir]), codexProgramRoot(codexBin())].filter(Boolean) };
|
|
955
|
+
: { runDir, workDir, readRoots: [...(skillsGrantRoots?.length ? skillsGrantRoots : [skillsDir]), codexProgramRoot(codexBin())].filter(Boolean) };
|
|
917
956
|
writeFileSync(join(codexHome, "config.toml"),
|
|
918
|
-
renderCodexConfigToml({ mcpConfig, allowedTools, developerInstructions:
|
|
957
|
+
renderCodexConfigToml({ mcpConfig, allowedTools, developerInstructions: CODEX_DEVELOPER_INSTRUCTIONS, toolTimeoutSec: timeoutSec, fence,
|
|
919
958
|
withheldFromCommands: codexCommandWithheld() }));
|
|
920
959
|
|
|
921
960
|
const input = absolutizeSkillRefs(message, skillsDir, resolveSkill);
|
|
@@ -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]));
|
|
@@ -2670,3 +2670,20 @@ export function topKeysForVersion(schemaVersion) {
|
|
|
2670
2670
|
const v = typeof schemaVersion === "number" ? schemaVersion : 1;
|
|
2671
2671
|
return [...topKeysFor({ v4: v >= 4, v5: v >= 5, v6: v >= 6, v7: v >= 7 })];
|
|
2672
2672
|
}
|
|
2673
|
+
|
|
2674
|
+
/**
|
|
2675
|
+
* ONE FINDING, CHECKED AS THE WHOLE RECORD CHECKS IT, before the record exists. The writer saves a completed finding as
|
|
2676
|
+
* it completes it, and a saved finding must pass exactly what it would pass inside the record, under the schema the
|
|
2677
|
+
* record is written at and the run's frozen framework and method. The record-wide rules (an empty record, ordinals
|
|
2678
|
+
* unique across the record, the actions and the coverage) belong to the record and are checked when it is complete.
|
|
2679
|
+
* Returns the finding as validated; throws the record's own token-first refusal. PURE.
|
|
2680
|
+
*/
|
|
2681
|
+
export function validateFindingForSave(f, opts = {}) {
|
|
2682
|
+
const schemaVersion = FINDINGS_SCHEMA_VERSION;
|
|
2683
|
+
const mode = { v4: schemaVersion >= 4, v5: schemaVersion >= 5, v6: schemaVersion >= 6, v7: schemaVersion >= 7, manifest: opts.manifest ?? null,
|
|
2684
|
+
method: "method" in opts ? (opts.method ?? null) : undefined,
|
|
2685
|
+
recordOrigins: Array.isArray(opts.recordOrigins) ? opts.recordOrigins : null, foreignLinks: Array.isArray(opts.recordOrigins) ? [] : null };
|
|
2686
|
+
const out = validateFinding(f, 0, new Set(), mode);
|
|
2687
|
+
throwForeignLinks(mode);
|
|
2688
|
+
return out;
|
|
2689
|
+
}
|
package/driver/gateway.mjs
CHANGED
|
@@ -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
|
|
301
|
-
|
|
302
|
-
if (
|
|
303
|
-
if (
|
|
304
|
-
return
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
+
}
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.5.0-beta.
|
|
5
|
+
"version": "0.5.0-beta.4",
|
|
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": {
|