@indigoai-us/hq-cli 5.345.129 → 5.345.130
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/CHANGELOG.md +10 -0
- package/assets/scaffold/core/scripts/lib/codex-output-schema.mjs +304 -0
- package/assets/scaffold/core/scripts/workflow-runner.mjs +2890 -0
- package/dist/command-catalog.generated.d.ts +29 -0
- package/dist/command-catalog.generated.js +34 -0
- package/dist/commands/core-pipeline-workflow-runner-native.d.ts +175 -0
- package/dist/commands/core-pipeline-workflow-runner-native.js +3237 -0
- package/dist/commands/core-workflow-run-command.d.ts +4 -0
- package/dist/commands/core-workflow-run-command.js +41 -0
- package/dist/commands/core.js +4 -0
- package/dist/commands/lanes-accounts.js +0 -1
- package/dist/lib/core-utils/workflow-runner-schema.d.ts +7 -0
- package/dist/lib/core-utils/workflow-runner-schema.js +259 -0
- package/dist/lib/lanes/config.d.ts +8 -4
- package/dist/lib/lanes/config.js +30 -36
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/reconcile.js +3 -26
- package/dist/lib/lanes/throttle.js +5 -9
- package/dist/lib/lanes/types.d.ts +4 -5
- package/dist/lib/lanes/types.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
2
|
+
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
3
|
+
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
4
|
+
import { coreNativePipelineEnabled } from "./core-native-pipeline-flag.js";
|
|
5
|
+
export const WORKFLOW_RUNNER_ASSET = "core/scripts/workflow-runner.mjs";
|
|
6
|
+
export function registerCoreWorkflowRunCommand(core) {
|
|
7
|
+
const workflow = core.command("workflow").description("Workflow orchestration");
|
|
8
|
+
const command = workflow.command("run")
|
|
9
|
+
.description("Run a workflow script or pipeline lane")
|
|
10
|
+
.helpOption(false)
|
|
11
|
+
.allowUnknownOption()
|
|
12
|
+
.allowExcessArguments(true)
|
|
13
|
+
.argument("[args...]")
|
|
14
|
+
.action(async (args, _options, current) => {
|
|
15
|
+
const argv = current.args.length > 0 ? current.args : args;
|
|
16
|
+
const root = resolveLiveRoot({ hqRoot: core.opts().hqRoot });
|
|
17
|
+
if (!await coreNativePipelineEnabled()) {
|
|
18
|
+
const result = runBundledScript({ asset: WORKFLOW_RUNNER_ASSET, args: argv, cwd: process.cwd(), hqRoot: root });
|
|
19
|
+
if (result.code !== 0)
|
|
20
|
+
process.exit(result.code);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const previousRoot = process.env.HQ_ROOT;
|
|
24
|
+
process.env.HQ_ROOT = root;
|
|
25
|
+
try {
|
|
26
|
+
const native = await import("./core-pipeline-workflow-runner-native.js");
|
|
27
|
+
const runner = native;
|
|
28
|
+
const code = await runner.runWorkflowRunnerNative(argv, root);
|
|
29
|
+
if (code !== 0)
|
|
30
|
+
process.exit(code);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
if (previousRoot === undefined)
|
|
34
|
+
delete process.env.HQ_ROOT;
|
|
35
|
+
else
|
|
36
|
+
process.env.HQ_ROOT = previousRoot;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
describeCoreCommand(command, { execution: "native", asset: WORKFLOW_RUNNER_ASSET, root: "live" });
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=core-workflow-run-command.js.map
|
package/dist/commands/core.js
CHANGED
|
@@ -52,6 +52,7 @@ import { registerCoreHookStateHelpers } from "./core-hook-state-helpers-native.j
|
|
|
52
52
|
import { registerCoreHookBatchCommand } from "./core-hook-batch-command.js";
|
|
53
53
|
import { registerCorePipelineWorkerTableCommand } from "./core-pipeline-workers-command.js";
|
|
54
54
|
import { registerCorePipelineRuntimeCommands } from "./core-pipeline-conductor-command.js";
|
|
55
|
+
import { registerCoreWorkflowRunCommand } from "./core-workflow-run-command.js";
|
|
55
56
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
56
57
|
import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
|
|
57
58
|
import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
|
|
@@ -304,6 +305,8 @@ export const SCAFFOLD_ONLY_ASSETS = [
|
|
|
304
305
|
{ asset: "core/scripts/session-title.sh", root: "live", summary: "Rollback companion for native session title" },
|
|
305
306
|
{ asset: "core/scripts/session-title-config.sh", root: "live", summary: "Rollback companion for native session-title settings" },
|
|
306
307
|
{ asset: "core/scripts/pipeline-worker-table.sh", root: "live", summary: "Rollback companion for native pipeline worker-table" },
|
|
308
|
+
{ asset: "core/scripts/workflow-runner.mjs", root: "live", summary: "Rollback companion for native workflow runner" },
|
|
309
|
+
{ asset: "core/scripts/lib/codex-output-schema.mjs", root: "live", summary: "Schema helper required by the bundled workflow-runner rollback companion" },
|
|
307
310
|
{ asset: "core/scripts/share-suggestion-state.sh", root: "live", summary: "Rollback shell for native share-suggestion-state" },
|
|
308
311
|
{ asset: "core/scripts/repo-run-registry.sh", root: "live", summary: "Rollback shell for native repo-run-registry" },
|
|
309
312
|
{ asset: "core/scripts/detect-stale-review-base.sh", root: "live", summary: "Rollback shell for native detect-stale-review-base" },
|
|
@@ -456,6 +459,7 @@ export function registerCoreCommands(program) {
|
|
|
456
459
|
registerCoreHookStateHelpers(core);
|
|
457
460
|
registerCoreHookBatchCommand(core);
|
|
458
461
|
registerCorePipelineRuntimeCommands(core);
|
|
462
|
+
registerCoreWorkflowRunCommand(core);
|
|
459
463
|
const sessionHelperCommands = [
|
|
460
464
|
{
|
|
461
465
|
name: "session-journal", asset: "core/scripts/session-journal.sh", root: "live",
|
|
@@ -290,7 +290,6 @@ export function createLaneAccountsHandlers(deps) {
|
|
|
290
290
|
balance_gate_enabled: balanceGateEnabled,
|
|
291
291
|
accounts,
|
|
292
292
|
selections,
|
|
293
|
-
lead_reserve_per_host: config.lead_reserve_per_host,
|
|
294
293
|
queued_leads: loaded.snapshots.flatMap((host) => {
|
|
295
294
|
const leads = leadLaneIds(host.lanes);
|
|
296
295
|
return host.lanes
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Port of pipeline-envelope.sh normalize, including its candidate selection rules. */
|
|
2
|
+
export declare function normalizeWorkflowRunnerEnvelope(raw: string): string;
|
|
3
|
+
/** Makes a copy in Codex's strict structured-output dialect. */
|
|
4
|
+
export declare function strictifySchemaForCodex(schema: unknown): unknown;
|
|
5
|
+
/** Removes nulls that Codex had to add for originally optional properties. */
|
|
6
|
+
export declare function stripStrictNulls(value: unknown, schema: unknown): unknown;
|
|
7
|
+
//# sourceMappingURL=workflow-runner-schema.d.ts.map
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
function isJsonObject(value) {
|
|
2
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
}
|
|
4
|
+
function jsonDumps(value) {
|
|
5
|
+
if (value === null || typeof value !== "object")
|
|
6
|
+
return JSON.stringify(value) ?? "null";
|
|
7
|
+
if (Array.isArray(value))
|
|
8
|
+
return `[${value.map(jsonDumps).join(", ")}]`;
|
|
9
|
+
const entries = Object.entries(value);
|
|
10
|
+
return `{${entries.map(([key, child]) => `${JSON.stringify(key)}: ${jsonDumps(child)}`).join(", ")}}`;
|
|
11
|
+
}
|
|
12
|
+
function balancedJsonEnd(text, start) {
|
|
13
|
+
let depth = 0;
|
|
14
|
+
let inString = false;
|
|
15
|
+
for (let i = start; i < text.length; i += 1) {
|
|
16
|
+
const char = text[i];
|
|
17
|
+
if (inString) {
|
|
18
|
+
if (char === "\\")
|
|
19
|
+
i += 1;
|
|
20
|
+
else if (char === '"')
|
|
21
|
+
inString = false;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (char === '"')
|
|
25
|
+
inString = true;
|
|
26
|
+
else if (char === "{" || char === "[")
|
|
27
|
+
depth += 1;
|
|
28
|
+
else if (char === "}" || char === "]") {
|
|
29
|
+
depth -= 1;
|
|
30
|
+
if (depth === 0)
|
|
31
|
+
return i + 1;
|
|
32
|
+
if (depth < 0)
|
|
33
|
+
return -1;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return -1;
|
|
37
|
+
}
|
|
38
|
+
/** Port of pipeline-envelope.sh normalize, including its candidate selection rules. */
|
|
39
|
+
export function normalizeWorkflowRunnerEnvelope(raw) {
|
|
40
|
+
const text = raw.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n");
|
|
41
|
+
const candidates = [];
|
|
42
|
+
const fence = /```[A-Za-z0-9_-]*\s*\n([\s\S]*?)```/g;
|
|
43
|
+
let match;
|
|
44
|
+
while ((match = fence.exec(text)) !== null) {
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(match[1]?.trim() ?? "");
|
|
47
|
+
if (isJsonObject(parsed))
|
|
48
|
+
candidates.push(parsed);
|
|
49
|
+
}
|
|
50
|
+
catch { /* malformed fences are skipped by the shell normalizer */ }
|
|
51
|
+
}
|
|
52
|
+
if (candidates.length === 0) {
|
|
53
|
+
let offset = 0;
|
|
54
|
+
while (offset < text.length) {
|
|
55
|
+
const start = text.indexOf("{", offset);
|
|
56
|
+
if (start < 0)
|
|
57
|
+
break;
|
|
58
|
+
const end = balancedJsonEnd(text, start);
|
|
59
|
+
if (end > 0) {
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(text.slice(start, end));
|
|
62
|
+
if (isJsonObject(parsed))
|
|
63
|
+
candidates.push(parsed);
|
|
64
|
+
offset = end;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
catch { /* continue scanning after this opening brace */ }
|
|
68
|
+
}
|
|
69
|
+
offset = start + 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (candidates.length === 0)
|
|
73
|
+
throw new Error("normalize: no JSON object found");
|
|
74
|
+
if (candidates.length > 1) {
|
|
75
|
+
const tagged = candidates.filter((candidate) => candidate.schema === "hq-phase-handoff/v1" || candidate.schema === "hq-phase-envelope/v1");
|
|
76
|
+
if (tagged.length !== 1)
|
|
77
|
+
throw new Error(`normalize: ${candidates.length} JSON objects found, expected exactly one`);
|
|
78
|
+
return `${jsonDumps(tagged[0])}\n`;
|
|
79
|
+
}
|
|
80
|
+
return `${jsonDumps(candidates[0])}\n`;
|
|
81
|
+
}
|
|
82
|
+
function isRecord(value) {
|
|
83
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
84
|
+
}
|
|
85
|
+
function typesOf(node) {
|
|
86
|
+
if (node.type === undefined)
|
|
87
|
+
return [];
|
|
88
|
+
return Array.isArray(node.type) ? [...node.type] : [node.type];
|
|
89
|
+
}
|
|
90
|
+
function isObjectNode(node) {
|
|
91
|
+
const types = typesOf(node);
|
|
92
|
+
return types.length ? types.includes("object") : isRecord(node.properties);
|
|
93
|
+
}
|
|
94
|
+
const MAP_KEYS = ["properties", "$defs", "definitions"];
|
|
95
|
+
const LIST_KEYS = ["anyOf", "oneOf", "allOf", "prefixItems"];
|
|
96
|
+
const SINGLE_KEYS = ["items", "not", "if", "then", "else", "contains", "propertyNames"];
|
|
97
|
+
function schemaError(at, message) {
|
|
98
|
+
return new Error(`codex output schema at ${at}: ${message}`);
|
|
99
|
+
}
|
|
100
|
+
function widenToNullable(node, at) {
|
|
101
|
+
if (!isRecord(node))
|
|
102
|
+
return;
|
|
103
|
+
const types = typesOf(node);
|
|
104
|
+
if (types.length) {
|
|
105
|
+
if (!types.includes("null"))
|
|
106
|
+
node.type = [...types, "null"];
|
|
107
|
+
if (Array.isArray(node.enum) && !node.enum.includes(null))
|
|
108
|
+
node.enum = [...node.enum, null];
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
112
|
+
const branches = node[key];
|
|
113
|
+
if (!Array.isArray(branches) || branches.length === 0)
|
|
114
|
+
continue;
|
|
115
|
+
if (!branches.some((branch) => isRecord(branch) && typesOf(branch).includes("null")))
|
|
116
|
+
branches.push({ type: "null" });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (typeof node.$ref === "string" || Array.isArray(node.allOf)) {
|
|
120
|
+
const inner = { ...node };
|
|
121
|
+
for (const key of Object.keys(node))
|
|
122
|
+
delete node[key];
|
|
123
|
+
node.anyOf = [inner, { type: "null" }];
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (isRecord(node.properties)) {
|
|
127
|
+
node.type = ["object", "null"];
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
throw schemaError(at, "this property is optional (absent from `required`) but its schema declares neither a type nor a composition, so there is no way to express \"no answer\" in the strict dialect. Give it a type, or list it in `required`.");
|
|
131
|
+
}
|
|
132
|
+
function strictifyInPlace(node, at) {
|
|
133
|
+
if (Array.isArray(node)) {
|
|
134
|
+
node.forEach((child, index) => strictifyInPlace(child, `${at}[${index}]`));
|
|
135
|
+
return node;
|
|
136
|
+
}
|
|
137
|
+
if (!isRecord(node))
|
|
138
|
+
return node;
|
|
139
|
+
for (const key of MAP_KEYS) {
|
|
140
|
+
if (!isRecord(node[key]))
|
|
141
|
+
continue;
|
|
142
|
+
for (const [name, child] of Object.entries(node[key]))
|
|
143
|
+
strictifyInPlace(child, `${at}.${key}.${name}`);
|
|
144
|
+
}
|
|
145
|
+
for (const key of LIST_KEYS) {
|
|
146
|
+
if (Array.isArray(node[key]))
|
|
147
|
+
node[key].forEach((child, index) => strictifyInPlace(child, `${at}.${key}[${index}]`));
|
|
148
|
+
}
|
|
149
|
+
for (const key of SINGLE_KEYS)
|
|
150
|
+
if (node[key] !== undefined)
|
|
151
|
+
strictifyInPlace(node[key], `${at}.${key}`);
|
|
152
|
+
if (!isObjectNode(node))
|
|
153
|
+
return node;
|
|
154
|
+
if (node.additionalProperties !== undefined && node.additionalProperties !== false) {
|
|
155
|
+
throw schemaError(at, "strict structured output permits no dynamic keys, so `additionalProperties` must be false (got "
|
|
156
|
+
+ `${JSON.stringify(node.additionalProperties)}). Model a map as an array of {key, value} entries, or run this agent on the grok or claude engine, which have no strict-schema flag.`);
|
|
157
|
+
}
|
|
158
|
+
node.additionalProperties = false;
|
|
159
|
+
if (typesOf(node).length === 0)
|
|
160
|
+
node.type = "object";
|
|
161
|
+
const props = isRecord(node.properties) ? node.properties : null;
|
|
162
|
+
if (!props)
|
|
163
|
+
return node;
|
|
164
|
+
const keys = Object.keys(props);
|
|
165
|
+
const declared = Array.isArray(node.required) ? node.required.filter((key) => typeof key === "string") : [];
|
|
166
|
+
const orphans = declared.filter((key) => !keys.includes(key));
|
|
167
|
+
if (orphans.length)
|
|
168
|
+
throw schemaError(at, `\`required\` lists ${orphans.map((key) => JSON.stringify(key)).join(", ")} with no matching property. Strict rejects a required name that is not in \`properties\`, and dropping it would leave the result validator demanding a key the model can no longer emit. Add the property, or remove the name from \`required\`.`);
|
|
169
|
+
const required = [];
|
|
170
|
+
for (const key of declared)
|
|
171
|
+
if (!required.includes(key))
|
|
172
|
+
required.push(key);
|
|
173
|
+
for (const key of keys) {
|
|
174
|
+
if (required.includes(key))
|
|
175
|
+
continue;
|
|
176
|
+
widenToNullable(props[key], `${at}.properties.${key}`);
|
|
177
|
+
required.push(key);
|
|
178
|
+
}
|
|
179
|
+
node.required = required;
|
|
180
|
+
return node;
|
|
181
|
+
}
|
|
182
|
+
/** Makes a copy in Codex's strict structured-output dialect. */
|
|
183
|
+
export function strictifySchemaForCodex(schema) {
|
|
184
|
+
if (!isRecord(schema) && !Array.isArray(schema))
|
|
185
|
+
return schema;
|
|
186
|
+
return strictifyInPlace(JSON.parse(JSON.stringify(schema)), "<root>");
|
|
187
|
+
}
|
|
188
|
+
const MAX_REF_DEPTH = 16;
|
|
189
|
+
function resolveRef(node, root, depth = 0) {
|
|
190
|
+
if (!isRecord(node) || typeof node.$ref !== "string" || depth >= MAX_REF_DEPTH || !isRecord(root))
|
|
191
|
+
return node;
|
|
192
|
+
const match = /^#\/(\$defs|definitions)\/([^/]+)$/.exec(node.$ref);
|
|
193
|
+
if (!match)
|
|
194
|
+
return node;
|
|
195
|
+
const bucket = root[match[1] ?? ""];
|
|
196
|
+
const target = isRecord(bucket) ? bucket[match[2] ?? ""] : undefined;
|
|
197
|
+
return isRecord(target) ? resolveRef(target, root, depth + 1) : node;
|
|
198
|
+
}
|
|
199
|
+
function allowsNull(node, root, depth = 0) {
|
|
200
|
+
const resolved = resolveRef(node, root, depth);
|
|
201
|
+
if (!isRecord(resolved))
|
|
202
|
+
return true;
|
|
203
|
+
const types = typesOf(resolved);
|
|
204
|
+
if (types.length)
|
|
205
|
+
return types.includes("null");
|
|
206
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
207
|
+
if (Array.isArray(resolved[key]))
|
|
208
|
+
return resolved[key].some((branch) => depth < MAX_REF_DEPTH && allowsNull(branch, root, depth + 1));
|
|
209
|
+
}
|
|
210
|
+
return isRecord(resolved.properties) ? false : true;
|
|
211
|
+
}
|
|
212
|
+
function effectiveSchema(node, root, depth = 0) {
|
|
213
|
+
const resolved = resolveRef(node, root, depth);
|
|
214
|
+
if (!isRecord(resolved))
|
|
215
|
+
return null;
|
|
216
|
+
for (const key of ["anyOf", "oneOf"]) {
|
|
217
|
+
if (!Array.isArray(resolved[key]))
|
|
218
|
+
continue;
|
|
219
|
+
const branches = resolved[key].map((branch) => resolveRef(branch, root, depth + 1))
|
|
220
|
+
.filter(isRecord).filter((branch) => !(typesOf(branch).length === 1 && typesOf(branch)[0] === "null"));
|
|
221
|
+
if (branches.length !== 1 || depth >= MAX_REF_DEPTH)
|
|
222
|
+
return null;
|
|
223
|
+
return effectiveSchema(branches[0], root, depth + 1);
|
|
224
|
+
}
|
|
225
|
+
return resolved;
|
|
226
|
+
}
|
|
227
|
+
function stripInPlace(value, schema, root, depth = 0) {
|
|
228
|
+
const effective = effectiveSchema(schema, root, depth);
|
|
229
|
+
if (!effective)
|
|
230
|
+
return value;
|
|
231
|
+
if (Array.isArray(value)) {
|
|
232
|
+
if (effective.items !== undefined)
|
|
233
|
+
value.forEach((item) => stripInPlace(item, effective.items, root, depth + 1));
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
if (!isRecord(value))
|
|
237
|
+
return value;
|
|
238
|
+
const props = isRecord(effective.properties) ? effective.properties : null;
|
|
239
|
+
if (!props)
|
|
240
|
+
return value;
|
|
241
|
+
const required = Array.isArray(effective.required) ? effective.required : [];
|
|
242
|
+
for (const [key, sub] of Object.entries(props)) {
|
|
243
|
+
if (!(key in value))
|
|
244
|
+
continue;
|
|
245
|
+
if (value[key] === null && !required.includes(key) && !allowsNull(sub, root)) {
|
|
246
|
+
delete value[key];
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
stripInPlace(value[key], sub, root, depth + 1);
|
|
250
|
+
}
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
/** Removes nulls that Codex had to add for originally optional properties. */
|
|
254
|
+
export function stripStrictNulls(value, schema) {
|
|
255
|
+
if (!isRecord(schema) || value === null || typeof value !== "object")
|
|
256
|
+
return value;
|
|
257
|
+
return stripInPlace(JSON.parse(JSON.stringify(value)), schema, schema);
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=workflow-runner-schema.js.map
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import { type AccountConfig, type LaneProvider, type LanesConfig, type RunawaySearchGuardConfig, type LivenessWindows, type ProviderConfig } from "./types.js";
|
|
11
11
|
export declare const LANES_CONFIG_REL: string;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Owner directive 2026-10-14: no default lane caps or CPU/memory admission
|
|
14
|
+
* gates. A provider has no running or queued limit unless the operator sets
|
|
15
|
+
* `max_running` / `max_queued` explicitly; this value stands for "no limit"
|
|
16
|
+
* and stays a finite integer so it survives JSON output.
|
|
17
|
+
*/
|
|
18
|
+
export declare const UNLIMITED_LANES: number;
|
|
19
|
+
export declare const DEFAULT_MAX_QUEUED: number;
|
|
16
20
|
/**
|
|
17
21
|
* Owner directive 2026-09-22: "if it's been an hour since last msg, always
|
|
18
22
|
* run compact". Override per provider with `compact_after_ms`.
|
package/dist/lib/lanes/config.js
CHANGED
|
@@ -13,10 +13,14 @@ import { parseConfigEffort } from "./effort.js";
|
|
|
13
13
|
import { LanesConfigError } from "./errors.js";
|
|
14
14
|
import { CONFIG_SCHEMA_VERSION, LANE_PROVIDERS, isLaneProvider, parseMajorSchema, } from "./types.js";
|
|
15
15
|
export const LANES_CONFIG_REL = path.join("personal", "lanes", "config.json");
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Owner directive 2026-10-14: no default lane caps or CPU/memory admission
|
|
18
|
+
* gates. A provider has no running or queued limit unless the operator sets
|
|
19
|
+
* `max_running` / `max_queued` explicitly; this value stands for "no limit"
|
|
20
|
+
* and stays a finite integer so it survives JSON output.
|
|
21
|
+
*/
|
|
22
|
+
export const UNLIMITED_LANES = Number.MAX_SAFE_INTEGER;
|
|
23
|
+
export const DEFAULT_MAX_QUEUED = UNLIMITED_LANES;
|
|
20
24
|
/**
|
|
21
25
|
* Owner directive 2026-09-22: "if it's been an hour since last msg, always
|
|
22
26
|
* run compact". Override per provider with `compact_after_ms`.
|
|
@@ -69,9 +73,9 @@ export const DEFAULT_LIVENESS_WINDOWS = {
|
|
|
69
73
|
silent_lost_ms: DEFAULT_SILENT_LOST_MS,
|
|
70
74
|
};
|
|
71
75
|
export const DEFAULT_PROVIDER_MAX_RUNNING = {
|
|
72
|
-
claude:
|
|
73
|
-
codex:
|
|
74
|
-
grok:
|
|
76
|
+
claude: UNLIMITED_LANES,
|
|
77
|
+
codex: UNLIMITED_LANES,
|
|
78
|
+
grok: UNLIMITED_LANES,
|
|
75
79
|
};
|
|
76
80
|
export const DEFAULT_ACCOUNT_HOMES = {
|
|
77
81
|
claude: "~/.claude",
|
|
@@ -152,14 +156,6 @@ function positiveInt(value, field, fallback) {
|
|
|
152
156
|
}
|
|
153
157
|
return value;
|
|
154
158
|
}
|
|
155
|
-
function nonNegativeInt(value, field, fallback) {
|
|
156
|
-
if (value === undefined)
|
|
157
|
-
return fallback;
|
|
158
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
159
|
-
throw new LanesConfigError("invalid_config", `${field} must be a non-negative integer`, { field, value });
|
|
160
|
-
}
|
|
161
|
-
return value;
|
|
162
|
-
}
|
|
163
159
|
function positiveNumber(value, field, fallback) {
|
|
164
160
|
if (value === undefined)
|
|
165
161
|
return fallback;
|
|
@@ -168,9 +164,9 @@ function positiveNumber(value, field, fallback) {
|
|
|
168
164
|
}
|
|
169
165
|
return value;
|
|
170
166
|
}
|
|
171
|
-
function
|
|
167
|
+
function optionalUnitInterval(value, field) {
|
|
172
168
|
if (value === undefined)
|
|
173
|
-
return
|
|
169
|
+
return undefined;
|
|
174
170
|
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
175
171
|
throw new LanesConfigError("invalid_config", `${field} must be a number in [0, 1]`, { field, value });
|
|
176
172
|
}
|
|
@@ -377,30 +373,35 @@ function parseProvider(provider, raw, deps) {
|
|
|
377
373
|
}
|
|
378
374
|
return parsed;
|
|
379
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* CPU and memory admission gates are opt-in. With no `throttle` keys set,
|
|
378
|
+
* admission never waits on CPU load or free memory.
|
|
379
|
+
*/
|
|
380
380
|
function parseThrottle(raw) {
|
|
381
381
|
const obj = raw === undefined ? {} : raw;
|
|
382
382
|
if (!isPlainObject(obj)) {
|
|
383
383
|
throw new LanesConfigError("invalid_config", "throttle must be an object");
|
|
384
384
|
}
|
|
385
|
-
const
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
385
|
+
const result = {};
|
|
386
|
+
const pause = optionalUnitInterval(obj.cpu_busy_pause, "throttle.cpu_busy_pause");
|
|
387
|
+
const resumeRaw = optionalUnitInterval(obj.cpu_busy_resume, "throttle.cpu_busy_resume");
|
|
388
|
+
if (pause !== undefined) {
|
|
389
|
+
const resume = resumeRaw ?? pause;
|
|
390
|
+
if (resume > pause) {
|
|
391
|
+
throw new LanesConfigError("invalid_config", "throttle.cpu_busy_resume must be <= throttle.cpu_busy_pause", { cpu_busy_pause: pause, cpu_busy_resume: resume });
|
|
392
|
+
}
|
|
393
|
+
result.cpu_busy_pause = pause;
|
|
394
|
+
result.cpu_busy_resume = resume;
|
|
389
395
|
}
|
|
390
|
-
let minMem = DEFAULT_MIN_AVAILABLE_MEM_MB;
|
|
391
396
|
if (obj.min_available_mem_mb !== undefined) {
|
|
392
397
|
if (typeof obj.min_available_mem_mb !== "number" ||
|
|
393
398
|
!Number.isFinite(obj.min_available_mem_mb) ||
|
|
394
399
|
obj.min_available_mem_mb < 0) {
|
|
395
400
|
throw new LanesConfigError("invalid_config", "throttle.min_available_mem_mb must be a non-negative number", { value: obj.min_available_mem_mb });
|
|
396
401
|
}
|
|
397
|
-
|
|
402
|
+
result.min_available_mem_mb = obj.min_available_mem_mb;
|
|
398
403
|
}
|
|
399
|
-
return
|
|
400
|
-
cpu_busy_pause: pause,
|
|
401
|
-
cpu_busy_resume: resume,
|
|
402
|
-
min_available_mem_mb: minMem,
|
|
403
|
-
};
|
|
404
|
+
return result;
|
|
404
405
|
}
|
|
405
406
|
function parseLiveness(raw) {
|
|
406
407
|
const obj = raw === undefined ? {} : raw;
|
|
@@ -458,16 +459,11 @@ export function documentedDefaultConfig(deps = defaultDeps()) {
|
|
|
458
459
|
return {
|
|
459
460
|
schema_version: CONFIG_SCHEMA_VERSION,
|
|
460
461
|
providers,
|
|
461
|
-
throttle: {
|
|
462
|
-
cpu_busy_pause: DEFAULT_CPU_BUSY_PAUSE,
|
|
463
|
-
cpu_busy_resume: DEFAULT_CPU_BUSY_RESUME,
|
|
464
|
-
min_available_mem_mb: DEFAULT_MIN_AVAILABLE_MEM_MB,
|
|
465
|
-
},
|
|
462
|
+
throttle: {},
|
|
466
463
|
liveness: { ...DEFAULT_LIVENESS_WINDOWS },
|
|
467
464
|
tracker: { checks: { ...DEFAULT_TRACKER_CHECKS } },
|
|
468
465
|
runaway_search_guard: { ...DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG },
|
|
469
466
|
low_disk_min_free_bytes: DEFAULT_LOW_DISK_MIN_FREE_BYTES,
|
|
470
|
-
lead_reserve_per_host: 1,
|
|
471
467
|
codex_rollout_compress_after_hours: DEFAULT_CODEX_ROLLOUT_COMPRESS_AFTER_HOURS,
|
|
472
468
|
codex_rollout_delete_after_days: DEFAULT_CODEX_ROLLOUT_DELETE_AFTER_DAYS,
|
|
473
469
|
session_question_timeout_secs: DEFAULT_SESSION_QUESTION_TIMEOUT_SECS,
|
|
@@ -508,7 +504,6 @@ export function parseLanesConfig(raw, deps = defaultDeps()) {
|
|
|
508
504
|
}
|
|
509
505
|
const compressAfterHours = positiveInt(raw.codex_rollout_compress_after_hours, "codex_rollout_compress_after_hours", DEFAULT_CODEX_ROLLOUT_COMPRESS_AFTER_HOURS);
|
|
510
506
|
const deleteAfterDays = positiveInt(raw.codex_rollout_delete_after_days, "codex_rollout_delete_after_days", DEFAULT_CODEX_ROLLOUT_DELETE_AFTER_DAYS);
|
|
511
|
-
const leadReservePerHost = nonNegativeInt(raw.lead_reserve_per_host, "lead_reserve_per_host", 1);
|
|
512
507
|
if (deleteAfterDays * 24 < compressAfterHours) {
|
|
513
508
|
throw new LanesConfigError("invalid_config", "codex_rollout_delete_after_days must be at least codex_rollout_compress_after_hours / 24", { codex_rollout_compress_after_hours: compressAfterHours, codex_rollout_delete_after_days: deleteAfterDays });
|
|
514
509
|
}
|
|
@@ -522,7 +517,6 @@ export function parseLanesConfig(raw, deps = defaultDeps()) {
|
|
|
522
517
|
tracker: { checks: parseTrackerChecks(raw.tracker) },
|
|
523
518
|
runaway_search_guard: parseRunawaySearchGuard(raw.runaway_search_guard),
|
|
524
519
|
low_disk_min_free_bytes: positiveInt(raw.low_disk_min_free_bytes, "low_disk_min_free_bytes", DEFAULT_LOW_DISK_MIN_FREE_BYTES),
|
|
525
|
-
lead_reserve_per_host: leadReservePerHost,
|
|
526
520
|
codex_rollout_compress_after_hours: compressAfterHours,
|
|
527
521
|
codex_rollout_delete_after_days: deleteAfterDays,
|
|
528
522
|
session_question_timeout_secs: positiveInt(raw.session_question_timeout_secs, "session_question_timeout_secs", DEFAULT_SESSION_QUESTION_TIMEOUT_SECS),
|
|
@@ -12,7 +12,7 @@ export type { HostCapacityAccount, HostCapacityProvider, HostCapacitySnapshot, H
|
|
|
12
12
|
export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STORY_STATUSES, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
|
|
13
13
|
export type { AccountConfig, AttributionHistoryEntry, EnvelopeDecision, EnvelopeQuestion, HostKind, LaneCompanyRef, LaneAccountSelection, LaneAccountSelectionConsidered, LaneAccountSelectionHostCount, LaneEnvelope, LaneHost, LaneProvider, LanePlacementPolicy, LaneRecord, LaneStory, LaneStoryStatus, PendingResume, AccountHistoryEntry, LaneState, LanesConfig, LastEnvelopeRef, LivenessState, LivenessWindows, ProviderConfig, QuestionAnswer, QuestionBodyRef, QuestionPriority, QuestionRecord, QuestionStatus, QueuedReason, RaisedVia, RoutingHistoryEntry, RoutingReason, SeniorHistoryEntry, SeniorKind, SeniorRef, WorkerProfileRepinHistoryEntry, LaneSettingsHistoryEntry, StoryStatus, TerminalLaneState, ThrottleConfig, } from "./types.js";
|
|
14
14
|
export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
|
|
15
|
-
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS,
|
|
15
|
+
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
|
|
16
16
|
export type { ConfigLoadDeps, ConfigWriteDeps } from "./config.js";
|
|
17
17
|
export { resolveLanesHqRoot } from "./hq-root.js";
|
|
18
18
|
export type { LanesHqRootOpts } from "./hq-root.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -8,7 +8,7 @@ export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingR
|
|
|
8
8
|
export { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "./host-capacity.js";
|
|
9
9
|
export { CONFIG_SCHEMA_VERSION, ENVELOPE_DECISIONS, ENVELOPE_SCHEMA_VERSION, HISTORY_CAP, HOST_KINDS, LANE_PROVIDERS, LANE_SCHEMA_VERSION, LANE_STORY_STATUSES, LANE_STATES, LIVENESS_STATES, QUESTION_BODY_STORES, QUESTION_SCHEMA_VERSION, QUESTION_STATUSES, QUEUED_REASONS, RAISED_VIA, ROUTING_REASONS, SENIOR_KINDS, STORY_STATUSES, TERMINAL_LANE_STATES, isEnvelopeDecision, isHostKind, isLaneProvider, isLaneState, isLivenessState, isQuestionStatus, isSeniorKind, isTerminalLaneState, parseLaneEnvelope, parseLaneProvider, parseLaneState, parseLivenessState, parseMajorSchema, parseQuestionStatus, parseSeniorKind, } from "./types.js";
|
|
10
10
|
export { ACTIVE_LANE_STORY_STATUSES, activeLaneStories, laneStories, laneStory, storyKey, storyStatusForEnvelope, storyWarning, withCurrentStory, withLaneStories, } from "./stories.js";
|
|
11
|
-
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS,
|
|
11
|
+
export { DEFAULT_ACCOUNT_HOMES, DEFAULT_AUTO_COMPACT_TOKENS, DEFAULT_COMPACT_AFTER_MS, DEFAULT_LIVE_WITHIN_MS, DEFAULT_LIVENESS_WINDOWS, DEFAULT_MAX_QUEUED, DEFAULT_PROVIDER_MAX_RUNNING, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, LANES_CONFIG_REL, accountMaxRunning, canonicalizeHome, documentedDefaultConfig, expandHome, lanesConfigPath, loadLanesConfig, parseLanesConfig, providerCompactAfterMs, providerAutoCompactTokens, providerQuotaDefaultHoldMs, DEFAULT_QUOTA_HOLD_MS, writeLanesConfig, } from "./config.js";
|
|
12
12
|
export { resolveLanesHqRoot } from "./hq-root.js";
|
|
13
13
|
export { LANE_STORY_LEDGER_FLAG, buildLaneStatusSnapshot, formatLaneStatusHuman, laneStoryLedgerEnabled, resolveProjectStoryLedger, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, } from "./story-ledger.js";
|
|
14
14
|
export { buildOutpostPickupCommand, parseOutpostPickupPayload, } from "./outpost-dispatch.js";
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import { parseUserYaml } from "../../utils/user-yaml-error.js";
|
|
11
|
-
import { compareLaneQueueOrder, compareLaneQueueOrderWithLeads,
|
|
11
|
+
import { compareLaneQueueOrder, compareLaneQueueOrderWithLeads, leadLaneIds, hasActiveAdmissionReservation, hasActivePendingResume, laneAdmissionCapReason, laneOccupiesRunningSlot, laneRunsOnHost, withAllLaneAdmissionLocks, } from "./admission.js";
|
|
12
12
|
import { accountStatus, isCredentialProven, resolveAccount } from "./accounts.js";
|
|
13
13
|
import { effectiveAccountHome } from "./account-home.js";
|
|
14
14
|
import { enforceRegistryApprovalGate } from "./approval-gate.js";
|
|
@@ -33,7 +33,7 @@ import { DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG, listHostProcesses, runRunawayToolG
|
|
|
33
33
|
import { LEAD_LANE_KEEP_ALIVE_FLAG, LEAD_LANE_KEEP_ALIVE_RESUME_BLOCKED_FLAG, LEAD_LANE_KEEP_ALIVE_IMMEDIATE_FIRST_RESUME_FLAG, LEAD_LANE_KEEP_ALIVE_FIRST_RESUME_SETTLE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_GRACE_MS, LEAD_LANE_KEEP_ALIVE_BLOCKED_RESUME_TEXT, queueLaneTerminalResume, LEAD_LANE_KEEP_ALIVE_INTERRUPTED_RESUME_TEXT, LEAD_LANE_KEEP_ALIVE_MAX_RESUMES, LEAD_LANE_KEEP_ALIVE_RESUME_TEXT, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, doneKeepAliveVerdict, dueKeepAliveNextCheckMs, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
|
|
34
34
|
import { isFailedQuotaKeepAliveLane, providerSupportsSessionResume } from "./delivery.js";
|
|
35
35
|
import { LANE_PROVIDERS, isIsoDateTime, isTerminalLaneState } from "./types.js";
|
|
36
|
-
import { applyDefaultQuotaHold, clearAccountQuotaAfterSuccessfulTurnBestEffort,
|
|
36
|
+
import { applyDefaultQuotaHold, clearAccountQuotaAfterSuccessfulTurnBestEffort, readQuotaSignal, recordAccountQuota, } from "./quota.js";
|
|
37
37
|
import { deliverQuotaNotice, quotaLaneNotice } from "./quota-notice.js";
|
|
38
38
|
import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
|
|
39
39
|
import { SILENCE_MS, appendWatcherEvent, classifyLaneExit, isPidAlive, missingExpectedEnvelopeFile, readLatestEnvelopeFile, readLatestEnvelopeRecord, watcherEventsPath, watcherOutLogPath, writeWatcherDecision, } from "./watcher.js";
|
|
@@ -2045,7 +2045,6 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds, exclu
|
|
|
2045
2045
|
if (localQueued.length === 0 || deps.skipAdmissionWithoutCpuSample === true)
|
|
2046
2046
|
return;
|
|
2047
2047
|
const config = loadLanesConfig(hqRoot);
|
|
2048
|
-
const admissionQuotaState = loadQuotaState(hqRoot);
|
|
2049
2048
|
const candidateLeadIds = leadLaneIds(lanes);
|
|
2050
2049
|
const flagReader = deps.readRegistryFlag ?? readLanesRegistryFlag;
|
|
2051
2050
|
const leadAdmissionEnabledById = new Map();
|
|
@@ -2140,25 +2139,6 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds, exclu
|
|
|
2140
2139
|
}
|
|
2141
2140
|
}
|
|
2142
2141
|
const projected = [...running, ...spawned];
|
|
2143
|
-
const laneResolution = lane.provider === "claude" ? resolveLaneCompany(hqRoot, lane) : undefined;
|
|
2144
|
-
const laneCompany = laneResolution?.company ?? undefined;
|
|
2145
|
-
const laneCompanyKey = laneCompany?.uid ?? laneCompany?.slug ?? lane.company?.uid ?? lane.company?.slug;
|
|
2146
|
-
let reserveEnabled = false;
|
|
2147
|
-
if (lane.provider === "claude" && laneCompanyKey) {
|
|
2148
|
-
let enabled = leadFlagByCompany.get(laneCompanyKey);
|
|
2149
|
-
if (!enabled) {
|
|
2150
|
-
enabled = readLanesRegistryFlagFailOff(LANE_LEAD_ADMISSION_PRIORITY_FLAG, {
|
|
2151
|
-
company: laneCompany,
|
|
2152
|
-
...(laneCompany && laneResolution ? { companyUidSource: laneCompanyUidSource(laneResolution.source) } : {}),
|
|
2153
|
-
env: deps.env,
|
|
2154
|
-
}, flagReader);
|
|
2155
|
-
leadFlagByCompany.set(laneCompanyKey, enabled);
|
|
2156
|
-
}
|
|
2157
|
-
reserveEnabled = await enabled;
|
|
2158
|
-
}
|
|
2159
|
-
const reserveBlocked = reserveEnabled && lane.provider === "claude" &&
|
|
2160
|
-
!priorityLeads.has(lane.lane_id) &&
|
|
2161
|
-
freeClaudeAccountSlots(projected, config, deps.host, now.getTime(), admissionQuotaState) <= config.lead_reserve_per_host;
|
|
2162
2142
|
const alternateAccount = availableBalancedAccount(hqRoot, lane, projected, config, deps.host, profileMaxConcurrent, now.getTime());
|
|
2163
2143
|
const admittedLane = alternateAccount !== undefined
|
|
2164
2144
|
? writeAdmissionAccount(hqRoot, lane, alternateAccount, deps.dryRun)
|
|
@@ -2166,10 +2146,7 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds, exclu
|
|
|
2166
2146
|
const baseCap = laneAdmissionCapReason(running, admittedLane, config, deps.host, profileMaxConcurrent, now.getTime());
|
|
2167
2147
|
const currentCap = laneAdmissionCapReason([...running, ...spawned], admittedLane, config, deps.host, profileMaxConcurrent, now.getTime());
|
|
2168
2148
|
let blockedBy;
|
|
2169
|
-
if (
|
|
2170
|
-
blockedBy = "lead_reserve";
|
|
2171
|
-
}
|
|
2172
|
-
else if (currentCap !== undefined) {
|
|
2149
|
+
if (currentCap !== undefined) {
|
|
2173
2150
|
blockedBy = baseCap ?? "queue_position";
|
|
2174
2151
|
}
|
|
2175
2152
|
if (!blockedBy && !decision.admit)
|
|
@@ -13,7 +13,6 @@ import { execFileSync as nodeExecFileSync } from "node:child_process";
|
|
|
13
13
|
import * as fs from "node:fs";
|
|
14
14
|
import * as os from "node:os";
|
|
15
15
|
import * as path from "node:path";
|
|
16
|
-
import { DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_MIN_AVAILABLE_MEM_MB, } from "./config.js";
|
|
17
16
|
// Reconcile normally measures the interval between cumulative CPU snapshots.
|
|
18
17
|
// A five-second window establishes a baseline or recovers from stale counters.
|
|
19
18
|
export const CPU_SAMPLE_MS = 5_000;
|
|
@@ -294,17 +293,18 @@ export function evaluateThrottle(input) {
|
|
|
294
293
|
const pause = input.throttle.cpu_busy_pause;
|
|
295
294
|
const resume = input.throttle.cpu_busy_resume;
|
|
296
295
|
const minMem = input.throttle.min_available_mem_mb;
|
|
296
|
+
// Each gate is opt-in: an unset threshold never blocks admission.
|
|
297
297
|
let cpuBlocks = false;
|
|
298
|
-
if (input.cpuCheckEnabled && input.busy !== null) {
|
|
298
|
+
if (pause !== undefined && input.cpuCheckEnabled && input.busy !== null) {
|
|
299
299
|
if (input.state.cpuPaused) {
|
|
300
|
-
cpuBlocks = input.busy > resume;
|
|
300
|
+
cpuBlocks = input.busy > (resume ?? pause);
|
|
301
301
|
}
|
|
302
302
|
else {
|
|
303
303
|
cpuBlocks = input.busy >= pause;
|
|
304
304
|
}
|
|
305
305
|
}
|
|
306
306
|
let memBlocks = false;
|
|
307
|
-
if (input.availableMemMb !== null) {
|
|
307
|
+
if (minMem !== undefined && input.availableMemMb !== null) {
|
|
308
308
|
memBlocks = input.availableMemMb < minMem;
|
|
309
309
|
}
|
|
310
310
|
if (!cpuBlocks && !memBlocks) {
|
|
@@ -448,11 +448,7 @@ export function writeThrottleState(filePath, state, deps = defaultThrottleDeps()
|
|
|
448
448
|
throw err;
|
|
449
449
|
}
|
|
450
450
|
}
|
|
451
|
-
export async function decideAdmission(hqRoot, throttle = {
|
|
452
|
-
cpu_busy_pause: DEFAULT_CPU_BUSY_PAUSE,
|
|
453
|
-
cpu_busy_resume: DEFAULT_CPU_BUSY_RESUME,
|
|
454
|
-
min_available_mem_mb: DEFAULT_MIN_AVAILABLE_MEM_MB,
|
|
455
|
-
}, deps = defaultThrottleDeps(), cpuBusyFraction, cpuObservation) {
|
|
451
|
+
export async function decideAdmission(hqRoot, throttle = {}, deps = defaultThrottleDeps(), cpuBusyFraction, cpuObservation) {
|
|
456
452
|
const filePath = throttleStatePath(hqRoot);
|
|
457
453
|
const loaded = readThrottleState(filePath, deps);
|
|
458
454
|
if (loaded === "unreadable") {
|