@indigoai-us/hq-cli 5.124.0 → 5.126.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 +149 -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/dist/command-catalog.generated.d.ts +414 -5
- package/dist/command-catalog.generated.js +531 -5
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/bot.d.ts +10 -0
- package/dist/commands/bot.js +6 -0
- package/dist/commands/lanes.d.ts +75 -0
- package/dist/commands/lanes.js +2032 -0
- package/dist/commands/login.js +49 -6
- package/dist/commands/mesh.js +133 -28
- 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/doctor/checks/work-context.js +4 -2
- package/dist/lib/lanes/accounts.d.ts +43 -0
- package/dist/lib/lanes/accounts.js +128 -0
- package/dist/lib/lanes/config.d.ts +56 -0
- package/dist/lib/lanes/config.js +351 -0
- package/dist/lib/lanes/errors.d.ts +39 -0
- package/dist/lib/lanes/errors.js +72 -0
- package/dist/lib/lanes/index.d.ts +19 -0
- package/dist/lib/lanes/index.js +12 -0
- package/dist/lib/lanes/liveness.d.ts +89 -0
- package/dist/lib/lanes/liveness.js +273 -0
- package/dist/lib/lanes/registry.d.ts +103 -0
- package/dist/lib/lanes/registry.js +247 -0
- package/dist/lib/lanes/store.d.ts +64 -0
- package/dist/lib/lanes/store.js +627 -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 +193 -0
- package/dist/lib/lanes/types.js +217 -0
- package/dist/lib/mesh/live/backfill-held.d.ts +2 -0
- package/dist/lib/mesh/live/backfill-held.js +8 -4
- package/dist/lib/mesh/live/daemon/board-refresh.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/board-refresh.js +6 -0
- 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/doctor.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/doctor.js +21 -1
- 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 +11 -0
- package/dist/lib/mesh/live/daemon/run.js +129 -10
- package/dist/lib/mesh/live/daemon/state.d.ts +2 -0
- package/dist/lib/mesh/live/flush.d.ts +3 -0
- package/dist/lib/mesh/live/flush.js +11 -0
- package/dist/lib/scan-packages/index.js +17 -12
- package/dist/lib/work-context/company.d.ts +8 -0
- package/dist/lib/work-context/company.js +62 -18
- package/dist/lib/work-context/config.d.ts +55 -0
- package/dist/lib/work-context/config.js +333 -67
- package/dist/lib/work-context/contract.d.ts +2 -0
- package/dist/lib/work-context/default-company.d.ts +43 -0
- package/dist/lib/work-context/default-company.js +83 -0
- package/dist/lib/work-context/outbox.d.ts +5 -1
- package/dist/lib/work-context/outbox.js +5 -0
- package/dist/lib/work-context/reconcile.d.ts +4 -0
- package/dist/lib/work-context/reconcile.js +86 -9
- package/dist/lib/work-context/state.d.ts +2 -0
- package/dist/lib/work-context/state.js +14 -0
- 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 +1 -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
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane record, question routing record, envelope, and config types (spec §3,
|
|
3
|
+
* §4, §5.5, §6.2). Finite sets are string-literal unions plus `const` arrays
|
|
4
|
+
* so the command layer can validate flags explicitly.
|
|
5
|
+
*/
|
|
6
|
+
export declare const LANE_SCHEMA_VERSION: "hq-lane.v1";
|
|
7
|
+
export declare const QUESTION_SCHEMA_VERSION: "hq-lane-question.v1";
|
|
8
|
+
export declare const ENVELOPE_SCHEMA_VERSION: "hq-lane-envelope.v1";
|
|
9
|
+
export declare const CONFIG_SCHEMA_VERSION: "hq-lanes-config.v1";
|
|
10
|
+
export declare const LANE_STATES: readonly ["queued", "running", "awaiting_input", "blocked", "done", "failed", "interrupted", "cancelled", "killed"];
|
|
11
|
+
export type LaneState = (typeof LANE_STATES)[number];
|
|
12
|
+
/** Exact terminal set from spec §6.7. `awaiting_input` and `blocked` are not terminal. */
|
|
13
|
+
export declare const TERMINAL_LANE_STATES: readonly ["done", "failed", "interrupted", "cancelled", "killed"];
|
|
14
|
+
export type TerminalLaneState = (typeof TERMINAL_LANE_STATES)[number];
|
|
15
|
+
export declare const LANE_PROVIDERS: readonly ["claude", "codex", "grok"];
|
|
16
|
+
export type LaneProvider = (typeof LANE_PROVIDERS)[number];
|
|
17
|
+
export declare const QUEUED_REASONS: readonly ["cap", "queue_position", "throttle"];
|
|
18
|
+
export type QueuedReason = (typeof QUEUED_REASONS)[number];
|
|
19
|
+
export declare const HOST_KINDS: readonly ["local", "outpost"];
|
|
20
|
+
export type HostKind = (typeof HOST_KINDS)[number];
|
|
21
|
+
export declare const SENIOR_KINDS: readonly ["session", "lane", "user"];
|
|
22
|
+
export type SeniorKind = (typeof SENIOR_KINDS)[number];
|
|
23
|
+
export declare const ENVELOPE_DECISIONS: readonly ["done", "ask", "blocked"];
|
|
24
|
+
export type EnvelopeDecision = (typeof ENVELOPE_DECISIONS)[number];
|
|
25
|
+
export declare const STORY_STATUSES: readonly ["queued", "in_progress", "review", "done"];
|
|
26
|
+
export type StoryStatus = (typeof STORY_STATUSES)[number];
|
|
27
|
+
export declare const QUESTION_STATUSES: readonly ["pending", "answered", "cancelled", "expired"];
|
|
28
|
+
export type QuestionStatus = (typeof QUESTION_STATUSES)[number];
|
|
29
|
+
export declare const RAISED_VIA: readonly ["cli", "envelope"];
|
|
30
|
+
export type RaisedVia = (typeof RAISED_VIA)[number];
|
|
31
|
+
export declare const ROUTING_REASONS: readonly ["asked", "escalated", "audience_ended", "reparented", "unanswered_timeout", "cancelled"];
|
|
32
|
+
export type RoutingReason = (typeof ROUTING_REASONS)[number];
|
|
33
|
+
export declare const QUESTION_BODY_STORES: readonly ["company", "personal"];
|
|
34
|
+
export type QuestionBodyStore = (typeof QUESTION_BODY_STORES)[number];
|
|
35
|
+
export declare const LIVENESS_STATES: readonly ["live", "idle", "ended", "lost"];
|
|
36
|
+
export type LivenessState = (typeof LIVENESS_STATES)[number];
|
|
37
|
+
export declare const HISTORY_CAP = 50;
|
|
38
|
+
export declare function isLaneState(value: unknown): value is LaneState;
|
|
39
|
+
export declare function isTerminalLaneState(value: unknown): value is TerminalLaneState;
|
|
40
|
+
export declare function isLaneProvider(value: unknown): value is LaneProvider;
|
|
41
|
+
export declare function isSeniorKind(value: unknown): value is SeniorKind;
|
|
42
|
+
export declare function isHostKind(value: unknown): value is HostKind;
|
|
43
|
+
export declare function isEnvelopeDecision(value: unknown): value is EnvelopeDecision;
|
|
44
|
+
export declare function isQuestionStatus(value: unknown): value is QuestionStatus;
|
|
45
|
+
export declare function isLivenessState(value: unknown): value is LivenessState;
|
|
46
|
+
export declare function parseLaneProvider(value: unknown): LaneProvider;
|
|
47
|
+
export declare function parseLaneState(value: unknown): LaneState;
|
|
48
|
+
export declare function parseQuestionStatus(value: unknown): QuestionStatus;
|
|
49
|
+
export declare function parseLivenessState(value: unknown): LivenessState;
|
|
50
|
+
export declare function parseSeniorKind(value: unknown): SeniorKind;
|
|
51
|
+
export interface LaneCompanyRef {
|
|
52
|
+
slug: string;
|
|
53
|
+
uid: string;
|
|
54
|
+
}
|
|
55
|
+
export interface SeniorRef {
|
|
56
|
+
kind: SeniorKind;
|
|
57
|
+
/** Required for session and lane; omitted for the user terminal. */
|
|
58
|
+
id?: string;
|
|
59
|
+
}
|
|
60
|
+
export interface LaneHost {
|
|
61
|
+
kind: HostKind;
|
|
62
|
+
id: string;
|
|
63
|
+
}
|
|
64
|
+
export interface SeniorHistoryEntry {
|
|
65
|
+
from: SeniorRef;
|
|
66
|
+
to: SeniorRef;
|
|
67
|
+
at: string;
|
|
68
|
+
by: string;
|
|
69
|
+
reason: string;
|
|
70
|
+
}
|
|
71
|
+
export interface AttributionHistoryEntry {
|
|
72
|
+
project_id: string;
|
|
73
|
+
story_id: string;
|
|
74
|
+
message_id: string;
|
|
75
|
+
at: string;
|
|
76
|
+
}
|
|
77
|
+
export interface LastEnvelopeRef {
|
|
78
|
+
ref: string;
|
|
79
|
+
decision: EnvelopeDecision;
|
|
80
|
+
at: string;
|
|
81
|
+
}
|
|
82
|
+
export interface LaneRecord {
|
|
83
|
+
schema_version: typeof LANE_SCHEMA_VERSION | string;
|
|
84
|
+
lane_id: string;
|
|
85
|
+
company: LaneCompanyRef;
|
|
86
|
+
project_id: string;
|
|
87
|
+
story_id: string;
|
|
88
|
+
thread_id?: string;
|
|
89
|
+
depth: number;
|
|
90
|
+
senior: SeniorRef;
|
|
91
|
+
senior_history: SeniorHistoryEntry[];
|
|
92
|
+
state: LaneState;
|
|
93
|
+
queued_reason?: QueuedReason;
|
|
94
|
+
priority: number;
|
|
95
|
+
worker?: string;
|
|
96
|
+
provider: LaneProvider;
|
|
97
|
+
account?: string;
|
|
98
|
+
host: LaneHost;
|
|
99
|
+
version: number;
|
|
100
|
+
created_at: string;
|
|
101
|
+
updated_at: string;
|
|
102
|
+
attribution_history: AttributionHistoryEntry[];
|
|
103
|
+
last_envelope?: LastEnvelopeRef;
|
|
104
|
+
}
|
|
105
|
+
export interface QuestionBodyRef {
|
|
106
|
+
store: QuestionBodyStore;
|
|
107
|
+
ref: string;
|
|
108
|
+
digest: string;
|
|
109
|
+
}
|
|
110
|
+
export interface RoutingHistoryEntry {
|
|
111
|
+
hop: number;
|
|
112
|
+
audience: SeniorRef;
|
|
113
|
+
at: string;
|
|
114
|
+
reason: RoutingReason;
|
|
115
|
+
}
|
|
116
|
+
export interface QuestionAnswer {
|
|
117
|
+
choice: string;
|
|
118
|
+
notes_ref?: string;
|
|
119
|
+
answered_by: string;
|
|
120
|
+
answered_at: string;
|
|
121
|
+
answered_level: number;
|
|
122
|
+
}
|
|
123
|
+
export interface QuestionRecord {
|
|
124
|
+
schema_version: typeof QUESTION_SCHEMA_VERSION | string;
|
|
125
|
+
question_id: string;
|
|
126
|
+
lane_id: string;
|
|
127
|
+
company: LaneCompanyRef;
|
|
128
|
+
project_id: string;
|
|
129
|
+
story_id: string;
|
|
130
|
+
thread_id?: string;
|
|
131
|
+
raised_via: RaisedVia;
|
|
132
|
+
asked_at: string;
|
|
133
|
+
origin: {
|
|
134
|
+
kind: "lane";
|
|
135
|
+
id: string;
|
|
136
|
+
};
|
|
137
|
+
hop: number;
|
|
138
|
+
status: QuestionStatus;
|
|
139
|
+
version: number;
|
|
140
|
+
body: QuestionBodyRef;
|
|
141
|
+
routing_history: RoutingHistoryEntry[];
|
|
142
|
+
answer?: QuestionAnswer;
|
|
143
|
+
}
|
|
144
|
+
export interface EnvelopeQuestion {
|
|
145
|
+
question: string;
|
|
146
|
+
context: string;
|
|
147
|
+
options: string[];
|
|
148
|
+
recommended?: string;
|
|
149
|
+
}
|
|
150
|
+
export interface LaneEnvelope {
|
|
151
|
+
schema_version: typeof ENVELOPE_SCHEMA_VERSION;
|
|
152
|
+
decision: EnvelopeDecision;
|
|
153
|
+
summary: string;
|
|
154
|
+
questions?: EnvelopeQuestion[];
|
|
155
|
+
story_status: StoryStatus;
|
|
156
|
+
artifacts: unknown[];
|
|
157
|
+
blocked_reason?: string;
|
|
158
|
+
}
|
|
159
|
+
export interface AccountConfig {
|
|
160
|
+
home: string;
|
|
161
|
+
enabled: boolean;
|
|
162
|
+
disabled_reason?: string;
|
|
163
|
+
max_running?: number;
|
|
164
|
+
}
|
|
165
|
+
export interface ProviderConfig {
|
|
166
|
+
max_running: number;
|
|
167
|
+
max_queued: number;
|
|
168
|
+
default_account: string;
|
|
169
|
+
accounts: Record<string, AccountConfig>;
|
|
170
|
+
}
|
|
171
|
+
export interface ThrottleConfig {
|
|
172
|
+
cpu_busy_pause: number;
|
|
173
|
+
cpu_busy_resume: number;
|
|
174
|
+
min_available_mem_mb: number;
|
|
175
|
+
}
|
|
176
|
+
/** Session liveness windows from spec §9. Missing keys keep these documented defaults. */
|
|
177
|
+
export interface LivenessWindows {
|
|
178
|
+
/** Last event within this window (or connected presence) → live. Default 10 minutes. */
|
|
179
|
+
live_within_ms: number;
|
|
180
|
+
/** `turn_start` with no later `turn_end` after this window → lost. Default 2 hours. */
|
|
181
|
+
turn_start_lost_ms: number;
|
|
182
|
+
/** No event at all for this window (and no `session_end`) → lost. Default 12 hours. */
|
|
183
|
+
silent_lost_ms: number;
|
|
184
|
+
}
|
|
185
|
+
export interface LanesConfig {
|
|
186
|
+
schema_version: typeof CONFIG_SCHEMA_VERSION | string;
|
|
187
|
+
providers: Record<LaneProvider, ProviderConfig>;
|
|
188
|
+
throttle: ThrottleConfig;
|
|
189
|
+
liveness: LivenessWindows;
|
|
190
|
+
}
|
|
191
|
+
export declare function parseMajorSchema(schemaVersion: string, expectedPrefix: string, expectedMajor: number): void;
|
|
192
|
+
export declare function parseLaneEnvelope(raw: unknown): LaneEnvelope;
|
|
193
|
+
//# sourceMappingURL=types.d.ts.map
|