@indigoai-us/hq-cli 5.125.0 → 5.127.0
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 +329 -0
- package/assets/bot-workers/setup/skills/joining-a-team.md +125 -0
- package/assets/bot-workers/setup/skills/your-business.md +270 -0
- package/assets/bot-workers/setup/worker.yaml +189 -58
- package/assets/lanes-hooks/hq-lanes-drain.json +15 -0
- package/assets/lanes-hooks/hq-lanes-drain.mjs +254 -0
- package/assets/lanes-hooks/hq-lanes-drain.sh +21 -0
- package/assets/lanes-reconcile/hq-lanes-reconcile.service +11 -0
- package/assets/lanes-reconcile/hq-lanes-reconcile.timer +12 -0
- package/dist/command-catalog.generated.d.ts +643 -6
- package/dist/command-catalog.generated.js +836 -6
- package/dist/command-registration-plan.d.ts +7 -0
- package/dist/command-registration-plan.js +11 -1
- package/dist/commands/agents.d.ts +12 -0
- package/dist/commands/agents.js +46 -2
- package/dist/commands/bot.d.ts +10 -0
- package/dist/commands/bot.js +6 -0
- package/dist/commands/lanes.d.ts +114 -0
- package/dist/commands/lanes.js +2969 -0
- package/dist/commands/mesh.js +287 -24
- package/dist/commands/onboard-create-identity.d.ts +26 -0
- package/dist/commands/onboard-create-identity.js +23 -0
- package/dist/commands/onboard.js +13 -4
- package/dist/lib/bot/progress.d.ts +6 -0
- package/dist/lib/bot/progress.js +16 -0
- package/dist/lib/bot/runtime/grok.d.ts +30 -7
- package/dist/lib/bot/runtime/grok.js +86 -8
- package/dist/lib/bot/runtime/index.js +15 -8
- package/dist/lib/bot/runtime/messages-stream.d.ts +12 -3
- package/dist/lib/bot/runtime/messages-stream.js +49 -3
- package/dist/lib/lanes/accounts.d.ts +103 -0
- package/dist/lib/lanes/accounts.js +318 -0
- package/dist/lib/lanes/compaction.d.ts +96 -0
- package/dist/lib/lanes/compaction.js +197 -0
- package/dist/lib/lanes/config.d.ts +71 -0
- package/dist/lib/lanes/config.js +408 -0
- package/dist/lib/lanes/delivery.d.ts +151 -0
- package/dist/lib/lanes/delivery.js +354 -0
- package/dist/lib/lanes/dropbox.d.ts +96 -0
- package/dist/lib/lanes/dropbox.js +408 -0
- package/dist/lib/lanes/effort.d.ts +50 -0
- package/dist/lib/lanes/effort.js +109 -0
- package/dist/lib/lanes/errors.d.ts +39 -0
- package/dist/lib/lanes/errors.js +72 -0
- package/dist/lib/lanes/hooks-install.d.ts +81 -0
- package/dist/lib/lanes/hooks-install.js +326 -0
- package/dist/lib/lanes/hq-root.d.ts +19 -0
- package/dist/lib/lanes/hq-root.js +28 -0
- package/dist/lib/lanes/index.d.ts +49 -0
- package/dist/lib/lanes/index.js +27 -0
- package/dist/lib/lanes/liveness.d.ts +95 -0
- package/dist/lib/lanes/liveness.js +332 -0
- package/dist/lib/lanes/machine-identity.d.ts +34 -0
- package/dist/lib/lanes/machine-identity.js +92 -0
- package/dist/lib/lanes/mesh-events.d.ts +109 -0
- package/dist/lib/lanes/mesh-events.js +275 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +65 -0
- package/dist/lib/lanes/outpost-dispatch.js +90 -0
- package/dist/lib/lanes/questions.d.ts +75 -0
- package/dist/lib/lanes/questions.js +243 -0
- package/dist/lib/lanes/quota.d.ts +95 -0
- package/dist/lib/lanes/quota.js +320 -0
- package/dist/lib/lanes/reconcile-install.d.ts +69 -0
- package/dist/lib/lanes/reconcile-install.js +221 -0
- package/dist/lib/lanes/reconcile.d.ts +102 -0
- package/dist/lib/lanes/reconcile.js +985 -0
- package/dist/lib/lanes/registry.d.ts +116 -0
- package/dist/lib/lanes/registry.js +521 -0
- package/dist/lib/lanes/spawn.d.ts +274 -0
- package/dist/lib/lanes/spawn.js +1363 -0
- package/dist/lib/lanes/store.d.ts +68 -0
- package/dist/lib/lanes/store.js +745 -0
- package/dist/lib/lanes/throttle.d.ts +89 -0
- package/dist/lib/lanes/throttle.js +297 -0
- package/dist/lib/lanes/types.d.ts +535 -0
- package/dist/lib/lanes/types.js +350 -0
- package/dist/lib/lanes/watcher-main.d.ts +9 -0
- package/dist/lib/lanes/watcher-main.js +22 -0
- package/dist/lib/lanes/watcher.d.ts +216 -0
- package/dist/lib/lanes/watcher.js +859 -0
- package/dist/lib/mesh/api.d.ts +71 -7
- package/dist/lib/mesh/api.js +167 -28
- package/dist/lib/mesh/live/backfill-held.d.ts +18 -7
- package/dist/lib/mesh/live/backfill-held.js +66 -21
- package/dist/lib/mesh/live/daemon/config.d.ts +27 -0
- package/dist/lib/mesh/live/daemon/config.js +75 -0
- package/dist/lib/mesh/live/daemon/index.d.ts +4 -3
- package/dist/lib/mesh/live/daemon/index.js +3 -3
- package/dist/lib/mesh/live/daemon/install.d.ts +143 -1
- package/dist/lib/mesh/live/daemon/install.js +444 -2
- package/dist/lib/mesh/live/daemon/paths.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/paths.js +2 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/run.js +111 -54
- package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.js +31 -8
- package/dist/lib/scan-packages/index.d.ts +2 -0
- package/dist/lib/scan-packages/index.js +18 -13
- package/dist/lib/work-context/config.d.ts +20 -0
- package/dist/lib/work-context/config.js +26 -5
- package/dist/lib/work-context/reconcile.js +28 -7
- package/dist/lib/work-context/state.d.ts +12 -0
- package/dist/lib/work-context/state.js +131 -3
- package/dist/lib/work-context/windows-acl.d.ts +123 -0
- package/dist/lib/work-context/windows-acl.js +425 -0
- package/dist/utils/pack-contributions.js +5 -4
- package/package.json +3 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CPU busy-fraction and memory admission gate (spec §5).
|
|
3
|
+
*
|
|
4
|
+
* Algorithm is the workflow-runner sampler (two os.cpus() readings
|
|
5
|
+
* CPU_SAMPLE_MS apart, 1 - idleDelta/totalDelta, clamped 0..1). The sampler
|
|
6
|
+
* is reimplemented here because hq-cli cannot import core/scripts and the
|
|
7
|
+
* shared @indigoai-us/hq-cloud export listed in spec §13 does not exist yet.
|
|
8
|
+
*
|
|
9
|
+
* Overrides HQ_LANES_CPU_BUSY_OVERRIDE and HQ_LANES_CPU_CHECK are deliberately
|
|
10
|
+
* distinct from the runner's HQ_WORKFLOW_* pair — this module never reads those.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as os from "node:os";
|
|
14
|
+
import type { ThrottleConfig } from "./types.js";
|
|
15
|
+
export declare const CPU_SAMPLE_MS = 200;
|
|
16
|
+
export declare const STARVATION_MAX_DEFERRALS = 10;
|
|
17
|
+
export declare const STARVATION_MAX_MS: number;
|
|
18
|
+
export declare const PROC_MEMINFO = "/proc/meminfo";
|
|
19
|
+
export declare const LANES_CPU_BUSY_OVERRIDE = "HQ_LANES_CPU_BUSY_OVERRIDE";
|
|
20
|
+
export declare const LANES_CPU_CHECK = "HQ_LANES_CPU_CHECK";
|
|
21
|
+
export interface CpuTimes {
|
|
22
|
+
idle: number;
|
|
23
|
+
total: number;
|
|
24
|
+
}
|
|
25
|
+
export interface ThrottleDeps {
|
|
26
|
+
env: NodeJS.ProcessEnv;
|
|
27
|
+
now: () => number;
|
|
28
|
+
cpus: () => os.CpuInfo[];
|
|
29
|
+
sleep: (ms: number) => Promise<void>;
|
|
30
|
+
readFileSync: (file: string, encoding: "utf8") => string;
|
|
31
|
+
/**
|
|
32
|
+
* Fallback only where MemAvailable is absent from /proc/meminfo (or the
|
|
33
|
+
* file itself is missing). Do not use this as the Linux primary reading.
|
|
34
|
+
*/
|
|
35
|
+
freemem: () => number;
|
|
36
|
+
existsSync: (file: string) => boolean;
|
|
37
|
+
mkdirSync: typeof fs.mkdirSync;
|
|
38
|
+
writeFileSync: typeof fs.writeFileSync;
|
|
39
|
+
renameSync: typeof fs.renameSync;
|
|
40
|
+
unlinkSync: typeof fs.unlinkSync;
|
|
41
|
+
}
|
|
42
|
+
export declare function defaultThrottleDeps(env?: NodeJS.ProcessEnv): ThrottleDeps;
|
|
43
|
+
export declare function cpuTimesSnapshot(cpus: os.CpuInfo[]): CpuTimes;
|
|
44
|
+
export declare function busyFractionFromDeltas(before: CpuTimes, after: CpuTimes): number | null;
|
|
45
|
+
export declare function sampleCpuBusyFraction(deps?: ThrottleDeps, sampleMs?: number): Promise<number | null>;
|
|
46
|
+
export declare function isCpuCheckEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
47
|
+
export declare function resolveCpuBusyFraction(deps?: ThrottleDeps): Promise<number | null>;
|
|
48
|
+
/** Parse MemAvailable kB from /proc/meminfo. */
|
|
49
|
+
export declare function parseMemAvailableKb(contents: string): number | null;
|
|
50
|
+
/**
|
|
51
|
+
* Available memory in MiB. Prefers kernel MemAvailable; falls back to
|
|
52
|
+
* os.freemem() only where MemAvailable is absent.
|
|
53
|
+
*/
|
|
54
|
+
export declare function readAvailableMemMb(deps?: ThrottleDeps): number | null;
|
|
55
|
+
export interface ThrottleState {
|
|
56
|
+
consecutiveDeferrals: number;
|
|
57
|
+
lastAdmissionAt: string | null;
|
|
58
|
+
cpuPaused: boolean;
|
|
59
|
+
}
|
|
60
|
+
export type ThrottleAdmitReason = "ok" | "starvation_bound" | "unreadable" | "cpu_check_disabled";
|
|
61
|
+
export type ThrottleDeferReason = "cpu" | "memory";
|
|
62
|
+
export type ThrottleDecision = {
|
|
63
|
+
admit: true;
|
|
64
|
+
reason: ThrottleAdmitReason;
|
|
65
|
+
} | {
|
|
66
|
+
admit: false;
|
|
67
|
+
reason: ThrottleDeferReason;
|
|
68
|
+
busy?: number;
|
|
69
|
+
availableMemMb?: number;
|
|
70
|
+
};
|
|
71
|
+
export declare function emptyThrottleState(): ThrottleState;
|
|
72
|
+
export declare function starvationBoundReached(state: ThrottleState, nowMs: number): boolean;
|
|
73
|
+
export declare function evaluateThrottle(input: {
|
|
74
|
+
busy: number | null;
|
|
75
|
+
availableMemMb: number | null;
|
|
76
|
+
cpuCheckEnabled: boolean;
|
|
77
|
+
state: ThrottleState | "unreadable";
|
|
78
|
+
throttle: ThrottleConfig;
|
|
79
|
+
nowMs: number;
|
|
80
|
+
}): ThrottleDecision;
|
|
81
|
+
export declare function applyThrottleDecision(state: ThrottleState, decision: ThrottleDecision, nowMs: number): ThrottleState;
|
|
82
|
+
export declare function throttleStatePath(hqRoot: string): string;
|
|
83
|
+
export declare function readThrottleState(filePath: string, deps?: ThrottleDeps): ThrottleState | "unreadable" | "missing";
|
|
84
|
+
export declare function writeThrottleState(filePath: string, state: ThrottleState, deps?: ThrottleDeps): void;
|
|
85
|
+
export declare function decideAdmission(hqRoot: string, throttle?: ThrottleConfig, deps?: ThrottleDeps): Promise<{
|
|
86
|
+
decision: ThrottleDecision;
|
|
87
|
+
state: ThrottleState;
|
|
88
|
+
}>;
|
|
89
|
+
//# sourceMappingURL=throttle.d.ts.map
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CPU busy-fraction and memory admission gate (spec §5).
|
|
3
|
+
*
|
|
4
|
+
* Algorithm is the workflow-runner sampler (two os.cpus() readings
|
|
5
|
+
* CPU_SAMPLE_MS apart, 1 - idleDelta/totalDelta, clamped 0..1). The sampler
|
|
6
|
+
* is reimplemented here because hq-cli cannot import core/scripts and the
|
|
7
|
+
* shared @indigoai-us/hq-cloud export listed in spec §13 does not exist yet.
|
|
8
|
+
*
|
|
9
|
+
* Overrides HQ_LANES_CPU_BUSY_OVERRIDE and HQ_LANES_CPU_CHECK are deliberately
|
|
10
|
+
* distinct from the runner's HQ_WORKFLOW_* pair — this module never reads those.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as os from "node:os";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { DEFAULT_CPU_BUSY_PAUSE, DEFAULT_CPU_BUSY_RESUME, DEFAULT_MIN_AVAILABLE_MEM_MB, } from "./config.js";
|
|
16
|
+
export const CPU_SAMPLE_MS = 200;
|
|
17
|
+
export const STARVATION_MAX_DEFERRALS = 10;
|
|
18
|
+
export const STARVATION_MAX_MS = 30 * 60 * 1000;
|
|
19
|
+
export const PROC_MEMINFO = "/proc/meminfo";
|
|
20
|
+
export const LANES_CPU_BUSY_OVERRIDE = "HQ_LANES_CPU_BUSY_OVERRIDE";
|
|
21
|
+
export const LANES_CPU_CHECK = "HQ_LANES_CPU_CHECK";
|
|
22
|
+
function defaultSleep(ms) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
setTimeout(resolve, ms);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function defaultThrottleDeps(env = process.env) {
|
|
28
|
+
return {
|
|
29
|
+
env,
|
|
30
|
+
now: () => Date.now(),
|
|
31
|
+
cpus: () => os.cpus() ?? [],
|
|
32
|
+
sleep: defaultSleep,
|
|
33
|
+
readFileSync: (file, encoding) => fs.readFileSync(file, encoding),
|
|
34
|
+
// os.freemem() reports MemFree on Linux (excludes page cache). Used only
|
|
35
|
+
// where MemAvailable is absent — see readAvailableMemMb.
|
|
36
|
+
freemem: () => os.freemem(),
|
|
37
|
+
existsSync: (file) => fs.existsSync(file),
|
|
38
|
+
mkdirSync: fs.mkdirSync.bind(fs),
|
|
39
|
+
writeFileSync: fs.writeFileSync.bind(fs),
|
|
40
|
+
renameSync: fs.renameSync.bind(fs),
|
|
41
|
+
unlinkSync: fs.unlinkSync.bind(fs),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function cpuTimesSnapshot(cpus) {
|
|
45
|
+
let idle = 0;
|
|
46
|
+
let total = 0;
|
|
47
|
+
for (const cpu of cpus) {
|
|
48
|
+
const t = cpu.times;
|
|
49
|
+
idle += t.idle;
|
|
50
|
+
total += t.user + t.nice + t.sys + t.idle + t.irq;
|
|
51
|
+
}
|
|
52
|
+
return { idle, total };
|
|
53
|
+
}
|
|
54
|
+
export function busyFractionFromDeltas(before, after) {
|
|
55
|
+
const idleDelta = after.idle - before.idle;
|
|
56
|
+
const totalDelta = after.total - before.total;
|
|
57
|
+
if (!(totalDelta > 0))
|
|
58
|
+
return null;
|
|
59
|
+
return Math.max(0, Math.min(1, 1 - idleDelta / totalDelta));
|
|
60
|
+
}
|
|
61
|
+
export async function sampleCpuBusyFraction(deps = defaultThrottleDeps(), sampleMs = CPU_SAMPLE_MS) {
|
|
62
|
+
try {
|
|
63
|
+
const a = cpuTimesSnapshot(deps.cpus());
|
|
64
|
+
await deps.sleep(sampleMs);
|
|
65
|
+
const b = cpuTimesSnapshot(deps.cpus());
|
|
66
|
+
return busyFractionFromDeltas(a, b);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
process.stderr.write(`lanes throttle: CPU sample failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function isCpuCheckEnabled(env = process.env) {
|
|
74
|
+
return !/^(0|false|off|no)$/i.test(env[LANES_CPU_CHECK] || "");
|
|
75
|
+
}
|
|
76
|
+
export async function resolveCpuBusyFraction(deps = defaultThrottleDeps()) {
|
|
77
|
+
const raw = deps.env[LANES_CPU_BUSY_OVERRIDE];
|
|
78
|
+
if (raw !== undefined && raw !== "") {
|
|
79
|
+
const v = Number(raw);
|
|
80
|
+
if (Number.isFinite(v))
|
|
81
|
+
return Math.max(0, Math.min(1, v));
|
|
82
|
+
process.stderr.write(`lanes throttle: ${LANES_CPU_BUSY_OVERRIDE} is not a finite number (${JSON.stringify(raw)}); sampling instead\n`);
|
|
83
|
+
}
|
|
84
|
+
return sampleCpuBusyFraction(deps);
|
|
85
|
+
}
|
|
86
|
+
/** Parse MemAvailable kB from /proc/meminfo. */
|
|
87
|
+
export function parseMemAvailableKb(contents) {
|
|
88
|
+
for (const line of contents.split("\n")) {
|
|
89
|
+
const match = /^MemAvailable:\s+(\d+)\s+kB\s*$/.exec(line.trimEnd());
|
|
90
|
+
if (match) {
|
|
91
|
+
const kb = Number(match[1]);
|
|
92
|
+
return Number.isFinite(kb) && kb >= 0 ? kb : null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Available memory in MiB. Prefers kernel MemAvailable; falls back to
|
|
99
|
+
* os.freemem() only where MemAvailable is absent.
|
|
100
|
+
*/
|
|
101
|
+
export function readAvailableMemMb(deps = defaultThrottleDeps()) {
|
|
102
|
+
try {
|
|
103
|
+
const contents = deps.readFileSync(PROC_MEMINFO, "utf8");
|
|
104
|
+
const kb = parseMemAvailableKb(contents);
|
|
105
|
+
if (kb !== null)
|
|
106
|
+
return kb / 1024;
|
|
107
|
+
// /proc/meminfo exists but has no MemAvailable (unusual). Fall back.
|
|
108
|
+
process.stderr.write("lanes throttle: MemAvailable missing from /proc/meminfo; falling back to os.freemem()\n");
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const code = err.code;
|
|
112
|
+
if (code !== "ENOENT") {
|
|
113
|
+
process.stderr.write(`lanes throttle: failed to read ${PROC_MEMINFO}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
// Platform without /proc/meminfo — fall back to free memory.
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const bytes = deps.freemem();
|
|
120
|
+
if (!Number.isFinite(bytes) || bytes < 0)
|
|
121
|
+
return null;
|
|
122
|
+
return bytes / (1024 * 1024);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
process.stderr.write(`lanes throttle: freemem fallback failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export function emptyThrottleState() {
|
|
130
|
+
return {
|
|
131
|
+
consecutiveDeferrals: 0,
|
|
132
|
+
lastAdmissionAt: null,
|
|
133
|
+
cpuPaused: false,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function starvationBoundReached(state, nowMs) {
|
|
137
|
+
if (state.consecutiveDeferrals >= STARVATION_MAX_DEFERRALS)
|
|
138
|
+
return true;
|
|
139
|
+
if (state.lastAdmissionAt === null)
|
|
140
|
+
return false;
|
|
141
|
+
const last = Date.parse(state.lastAdmissionAt);
|
|
142
|
+
if (!Number.isFinite(last))
|
|
143
|
+
return false;
|
|
144
|
+
return nowMs - last >= STARVATION_MAX_MS;
|
|
145
|
+
}
|
|
146
|
+
export function evaluateThrottle(input) {
|
|
147
|
+
if (input.state === "unreadable") {
|
|
148
|
+
return { admit: true, reason: "unreadable" };
|
|
149
|
+
}
|
|
150
|
+
const pause = input.throttle.cpu_busy_pause;
|
|
151
|
+
const resume = input.throttle.cpu_busy_resume;
|
|
152
|
+
const minMem = input.throttle.min_available_mem_mb;
|
|
153
|
+
let cpuBlocks = false;
|
|
154
|
+
if (input.cpuCheckEnabled && input.busy !== null) {
|
|
155
|
+
if (input.state.cpuPaused) {
|
|
156
|
+
cpuBlocks = input.busy > resume;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
cpuBlocks = input.busy >= pause;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
let memBlocks = false;
|
|
163
|
+
if (input.availableMemMb !== null) {
|
|
164
|
+
memBlocks = input.availableMemMb < minMem;
|
|
165
|
+
}
|
|
166
|
+
if (!cpuBlocks && !memBlocks) {
|
|
167
|
+
return {
|
|
168
|
+
admit: true,
|
|
169
|
+
reason: input.cpuCheckEnabled ? "ok" : "cpu_check_disabled",
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (starvationBoundReached(input.state, input.nowMs)) {
|
|
173
|
+
return { admit: true, reason: "starvation_bound" };
|
|
174
|
+
}
|
|
175
|
+
if (cpuBlocks) {
|
|
176
|
+
return { admit: false, reason: "cpu", busy: input.busy ?? undefined };
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
admit: false,
|
|
180
|
+
reason: "memory",
|
|
181
|
+
availableMemMb: input.availableMemMb ?? undefined,
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export function applyThrottleDecision(state, decision, nowMs) {
|
|
185
|
+
if (decision.admit) {
|
|
186
|
+
return {
|
|
187
|
+
consecutiveDeferrals: 0,
|
|
188
|
+
lastAdmissionAt: new Date(nowMs).toISOString(),
|
|
189
|
+
cpuPaused: false,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
consecutiveDeferrals: state.consecutiveDeferrals + 1,
|
|
194
|
+
lastAdmissionAt: state.lastAdmissionAt,
|
|
195
|
+
cpuPaused: decision.reason === "cpu" ? true : state.cpuPaused,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
export function throttleStatePath(hqRoot) {
|
|
199
|
+
return path.join(hqRoot, "workspace", "lanes-runs", "throttle-state.json");
|
|
200
|
+
}
|
|
201
|
+
export function readThrottleState(filePath, deps = defaultThrottleDeps()) {
|
|
202
|
+
if (!deps.existsSync(filePath))
|
|
203
|
+
return "missing";
|
|
204
|
+
let raw;
|
|
205
|
+
try {
|
|
206
|
+
raw = deps.readFileSync(filePath, "utf8");
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
process.stderr.write(`lanes throttle: failed to read state ${filePath}: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
210
|
+
return "unreadable";
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const parsed = JSON.parse(raw);
|
|
214
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
215
|
+
process.stderr.write(`lanes throttle: state ${filePath} is not an object\n`);
|
|
216
|
+
return "unreadable";
|
|
217
|
+
}
|
|
218
|
+
const obj = parsed;
|
|
219
|
+
if (typeof obj.consecutiveDeferrals !== "number" ||
|
|
220
|
+
!Number.isInteger(obj.consecutiveDeferrals) ||
|
|
221
|
+
obj.consecutiveDeferrals < 0) {
|
|
222
|
+
process.stderr.write(`lanes throttle: state ${filePath} has invalid consecutiveDeferrals\n`);
|
|
223
|
+
return "unreadable";
|
|
224
|
+
}
|
|
225
|
+
if (obj.lastAdmissionAt !== null &&
|
|
226
|
+
typeof obj.lastAdmissionAt !== "string") {
|
|
227
|
+
process.stderr.write(`lanes throttle: state ${filePath} has invalid lastAdmissionAt\n`);
|
|
228
|
+
return "unreadable";
|
|
229
|
+
}
|
|
230
|
+
if (typeof obj.cpuPaused !== "boolean") {
|
|
231
|
+
process.stderr.write(`lanes throttle: state ${filePath} has invalid cpuPaused\n`);
|
|
232
|
+
return "unreadable";
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
235
|
+
consecutiveDeferrals: obj.consecutiveDeferrals,
|
|
236
|
+
lastAdmissionAt: obj.lastAdmissionAt,
|
|
237
|
+
cpuPaused: obj.cpuPaused,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
process.stderr.write(`lanes throttle: state ${filePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
242
|
+
return "unreadable";
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
export function writeThrottleState(filePath, state, deps = defaultThrottleDeps()) {
|
|
246
|
+
const dir = path.dirname(filePath);
|
|
247
|
+
deps.mkdirSync(dir, { recursive: true });
|
|
248
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
249
|
+
try {
|
|
250
|
+
deps.writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`, {
|
|
251
|
+
encoding: "utf8",
|
|
252
|
+
});
|
|
253
|
+
deps.renameSync(tmp, filePath);
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
try {
|
|
257
|
+
deps.unlinkSync(tmp);
|
|
258
|
+
}
|
|
259
|
+
catch (unlinkErr) {
|
|
260
|
+
const code = unlinkErr.code;
|
|
261
|
+
if (code !== "ENOENT") {
|
|
262
|
+
process.stderr.write(`lanes throttle: unlink tmp failed for ${tmp}: ${unlinkErr instanceof Error ? unlinkErr.message : String(unlinkErr)}\n`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
throw err;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
export async function decideAdmission(hqRoot, throttle = {
|
|
269
|
+
cpu_busy_pause: DEFAULT_CPU_BUSY_PAUSE,
|
|
270
|
+
cpu_busy_resume: DEFAULT_CPU_BUSY_RESUME,
|
|
271
|
+
min_available_mem_mb: DEFAULT_MIN_AVAILABLE_MEM_MB,
|
|
272
|
+
}, deps = defaultThrottleDeps()) {
|
|
273
|
+
const filePath = throttleStatePath(hqRoot);
|
|
274
|
+
const loaded = readThrottleState(filePath, deps);
|
|
275
|
+
if (loaded === "unreadable") {
|
|
276
|
+
return {
|
|
277
|
+
decision: { admit: true, reason: "unreadable" },
|
|
278
|
+
state: emptyThrottleState(),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const state = loaded === "missing" ? emptyThrottleState() : loaded;
|
|
282
|
+
const busy = await resolveCpuBusyFraction(deps);
|
|
283
|
+
const availableMemMb = readAvailableMemMb(deps);
|
|
284
|
+
const nowMs = deps.now();
|
|
285
|
+
const decision = evaluateThrottle({
|
|
286
|
+
busy,
|
|
287
|
+
availableMemMb,
|
|
288
|
+
cpuCheckEnabled: isCpuCheckEnabled(deps.env),
|
|
289
|
+
state,
|
|
290
|
+
throttle,
|
|
291
|
+
nowMs,
|
|
292
|
+
});
|
|
293
|
+
const next = applyThrottleDecision(state, decision, nowMs);
|
|
294
|
+
writeThrottleState(filePath, next, deps);
|
|
295
|
+
return { decision, state: next };
|
|
296
|
+
}
|
|
297
|
+
//# sourceMappingURL=throttle.js.map
|