@indigoai-us/hq-cli 5.312.1 → 5.313.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 +4 -0
- package/dist/command-catalog.generated.d.ts +15 -0
- package/dist/command-catalog.generated.js +19 -0
- package/dist/commands/core-knowledge-prefs.d.ts +0 -1
- package/dist/commands/core-knowledge-prefs.js +1 -1
- package/dist/commands/core-worker-share-native.d.ts +0 -1
- package/dist/commands/core-worker-share-native.js +1 -1
- package/dist/commands/lanes-message.js +3 -0
- package/dist/commands/lanes-questions.js +2 -1
- package/dist/commands/lanes-resume.js +3 -0
- package/dist/commands/lanes.js +151 -21
- package/dist/lib/lanes/config.d.ts +3 -0
- package/dist/lib/lanes/config.js +14 -0
- package/dist/lib/lanes/disk-guard.d.ts +66 -0
- package/dist/lib/lanes/disk-guard.js +172 -0
- package/dist/lib/lanes/flag-keys.d.ts +2 -0
- package/dist/lib/lanes/flag-keys.js +2 -0
- package/dist/lib/lanes/grants.d.ts +4 -3
- package/dist/lib/lanes/grants.js +12 -7
- package/dist/lib/lanes/hire-grant-dispatch.js +2 -3
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/reconcile.d.ts +7 -1
- package/dist/lib/lanes/reconcile.js +142 -11
- package/dist/lib/lanes/registry-flag.d.ts +6 -0
- package/dist/lib/lanes/registry-flag.js +25 -0
- package/dist/lib/lanes/spawn.d.ts +5 -0
- package/dist/lib/lanes/spawn.js +26 -1
- package/dist/lib/lanes/store.js +11 -0
- package/dist/lib/lanes/types.d.ts +11 -1
- package/dist/lib/lanes/types.js +1 -0
- package/dist/utils/pack-contributions.d.ts +0 -5
- package/dist/utils/pack-contributions.js +1 -1
- package/dist/utils/redact-error-text.d.ts +0 -2
- package/dist/utils/redact-error-text.js +1 -1
- package/dist/utils/sandbox-runner-client.d.ts +2 -1
- package/package.json +1 -1
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { createGzip } from "node:zlib";
|
|
4
|
+
import { createReadStream, createWriteStream } from "node:fs";
|
|
5
|
+
import { pipeline } from "node:stream/promises";
|
|
6
|
+
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
7
|
+
import { resolveLaneCompany } from "./lane-company.js";
|
|
8
|
+
import { effectiveAccountHome } from "./account-home.js";
|
|
9
|
+
import { lanesConfigPath } from "./config.js";
|
|
10
|
+
export const LOW_DISK_GUARD_FLAG = "lanes.low-disk-guard";
|
|
11
|
+
export const CODEX_ROLLOUT_RETENTION_FLAG = "lanes.codex-rollout-retention";
|
|
12
|
+
export const DEFAULT_LOW_DISK_FREE_BYTES = 5 * 1024 ** 3;
|
|
13
|
+
export const DEFAULT_LOW_DISK_FRACTION = 0.02;
|
|
14
|
+
const EXCLUDED_SESSION_FILES = new Set(["auth.json", ".credentials.json", "thread_history.sqlite"]);
|
|
15
|
+
export async function lowDiskGuardEnabledForLane(hqRoot, lane, readFlag = readLanesRegistryFlag) {
|
|
16
|
+
const company = resolveLaneCompany(hqRoot, lane).company;
|
|
17
|
+
return readFlag(LOW_DISK_GUARD_FLAG, { company, companyUidSource: "resolved_lane_company" });
|
|
18
|
+
}
|
|
19
|
+
export function measureDiskSpace(target, statfs = (p) => fs.statfsSync(p), minFreeBytes = DEFAULT_LOW_DISK_FREE_BYTES) {
|
|
20
|
+
const stats = statfs(target);
|
|
21
|
+
if (![stats.bsize, stats.blocks, stats.bavail].every(Number.isFinite) || stats.bsize <= 0 || stats.blocks < 0 || stats.bavail < 0) {
|
|
22
|
+
throw new Error(`invalid statfs result for ${target}`);
|
|
23
|
+
}
|
|
24
|
+
const totalBytes = stats.bsize * stats.blocks;
|
|
25
|
+
const freeBytes = stats.bsize * stats.bavail;
|
|
26
|
+
const thresholdBytes = Math.floor(Math.min(minFreeBytes, totalBytes * DEFAULT_LOW_DISK_FRACTION));
|
|
27
|
+
return {
|
|
28
|
+
path: target,
|
|
29
|
+
free_bytes: freeBytes,
|
|
30
|
+
total_bytes: totalBytes,
|
|
31
|
+
threshold_bytes: thresholdBytes,
|
|
32
|
+
low: freeBytes < thresholdBytes,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function checkLaneDiskSpace(hqRoot, providerHome, statfs = (p) => fs.statfsSync(p), minFreeBytes = DEFAULT_LOW_DISK_FREE_BYTES) {
|
|
36
|
+
const hq = measureDiskSpace(hqRoot, statfs, minFreeBytes);
|
|
37
|
+
const provider = measureDiskSpace(providerHome, statfs, minFreeBytes);
|
|
38
|
+
return { low: hq.low || provider.low, hq, provider };
|
|
39
|
+
}
|
|
40
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
41
|
+
function rolloutSessionId(name) {
|
|
42
|
+
const match = /^rollout-.+-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:\.jsonl(?:\.gz)?)$/.exec(name);
|
|
43
|
+
return match?.[1];
|
|
44
|
+
}
|
|
45
|
+
function listRollouts(root, io, depth = 0) {
|
|
46
|
+
if (depth > 4)
|
|
47
|
+
return [];
|
|
48
|
+
let entries;
|
|
49
|
+
try {
|
|
50
|
+
entries = (io.readdirSync ?? fs.readdirSync)(root, { withFileTypes: true });
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code === "ENOENT")
|
|
54
|
+
return [];
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
const found = [];
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (entry.isDirectory())
|
|
60
|
+
found.push(...listRollouts(path.join(root, entry.name), io, depth + 1));
|
|
61
|
+
else if (entry.isFile() &&
|
|
62
|
+
!EXCLUDED_SESSION_FILES.has(entry.name) &&
|
|
63
|
+
/^rollout-.+\.jsonl(?:\.gz)?$/.test(entry.name))
|
|
64
|
+
found.push(path.join(root, entry.name));
|
|
65
|
+
}
|
|
66
|
+
return found;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Codex homes that rollout retention may touch: enabled accounts in the
|
|
70
|
+
* operator's lanes config file. Without that file the documented defaults
|
|
71
|
+
* point at the real `~/.codex`, so retention gets no homes at all.
|
|
72
|
+
*/
|
|
73
|
+
export function configuredCodexRolloutHomes(hqRoot, config, hostname) {
|
|
74
|
+
if (!fs.existsSync(lanesConfigPath(hqRoot)))
|
|
75
|
+
return [];
|
|
76
|
+
return [...new Set(Object.values(config.providers.codex.accounts)
|
|
77
|
+
.filter((account) => account.enabled)
|
|
78
|
+
.map((account) => effectiveAccountHome(account, hostname).home))];
|
|
79
|
+
}
|
|
80
|
+
/** Operates only on rollout files under configured Codex `sessions/` trees. */
|
|
81
|
+
export async function retainCodexRollouts(options) {
|
|
82
|
+
const now = options.nowMs ?? Date.now();
|
|
83
|
+
const compressAfter = options.compressAfterMs ?? DAY_MS;
|
|
84
|
+
const deleteAfter = options.deleteAfterMs ?? 7 * DAY_MS;
|
|
85
|
+
if (!(compressAfter > 0 && deleteAfter >= compressAfter))
|
|
86
|
+
throw new Error("invalid rollout retention ages");
|
|
87
|
+
const io = options;
|
|
88
|
+
const candidates = [];
|
|
89
|
+
for (const home of new Set(options.homes)) {
|
|
90
|
+
const sessions = path.join(home, "sessions");
|
|
91
|
+
for (const file of listRollouts(sessions, io)) {
|
|
92
|
+
const session = rolloutSessionId(path.basename(file));
|
|
93
|
+
if (!session)
|
|
94
|
+
continue;
|
|
95
|
+
const stat = (io.statSync ?? fs.statSync)(file);
|
|
96
|
+
candidates.push({ file, session, mtimeMs: stat.mtimeMs, size: stat.size });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const protectedSessions = new Set();
|
|
100
|
+
const mappedSessions = new Set();
|
|
101
|
+
for (const lane of options.lanes) {
|
|
102
|
+
if (lane.provider !== "codex")
|
|
103
|
+
continue;
|
|
104
|
+
const sessions = new Set([lane.provider_session_id, lane.last_resume?.session_id].filter((session) => Boolean(session)));
|
|
105
|
+
for (const session of sessions)
|
|
106
|
+
mappedSessions.add(session);
|
|
107
|
+
if (["running", "queued"].includes(lane.state)) {
|
|
108
|
+
for (const session of sessions)
|
|
109
|
+
protectedSessions.add(session);
|
|
110
|
+
}
|
|
111
|
+
else if (!["killed", "cancelled"].includes(lane.state)) {
|
|
112
|
+
for (const session of sessions) {
|
|
113
|
+
const newest = candidates.filter((candidate) => candidate.session === session).sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
|
|
114
|
+
if (newest)
|
|
115
|
+
protectedSessions.add(`${session}:${newest.file}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const result = { scanned: candidates.length, compressed: [], deleted: [], bytes_freed: 0 };
|
|
120
|
+
for (const candidate of candidates) {
|
|
121
|
+
const isProtected = protectedSessions.has(candidate.session) || protectedSessions.has(`${candidate.session}:${candidate.file}`);
|
|
122
|
+
if (isProtected)
|
|
123
|
+
continue;
|
|
124
|
+
const age = now - candidate.mtimeMs;
|
|
125
|
+
if (age < compressAfter)
|
|
126
|
+
continue;
|
|
127
|
+
if (age >= deleteAfter && mappedSessions.has(candidate.session)) {
|
|
128
|
+
result.deleted.push(candidate.file);
|
|
129
|
+
if (!options.dryRun) {
|
|
130
|
+
result.bytes_freed += candidate.size;
|
|
131
|
+
(io.unlinkSync ?? fs.unlinkSync)(candidate.file);
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (candidate.file.endsWith(".gz"))
|
|
136
|
+
continue;
|
|
137
|
+
const compressedPath = `${candidate.file}.gz`;
|
|
138
|
+
result.compressed.push(compressedPath);
|
|
139
|
+
if (options.dryRun)
|
|
140
|
+
continue;
|
|
141
|
+
const temp = `${compressedPath}.${process.pid}.tmp`;
|
|
142
|
+
const compressedBytes = options.compressFile
|
|
143
|
+
? await options.compressFile(candidate.file, compressedPath)
|
|
144
|
+
: await gzipRolloutStream(candidate.file, temp, compressedPath, io);
|
|
145
|
+
// Keep the source age on the compressed file so the seven-day deletion
|
|
146
|
+
// policy measures from the original rollout, not from compression time.
|
|
147
|
+
(io.utimesSync ?? fs.utimesSync)(compressedPath, new Date(candidate.mtimeMs), new Date(candidate.mtimeMs));
|
|
148
|
+
(io.unlinkSync ?? fs.unlinkSync)(candidate.file);
|
|
149
|
+
result.bytes_freed += Math.max(0, candidate.size - compressedBytes);
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
async function gzipRolloutStream(source, temp, target, io) {
|
|
154
|
+
try {
|
|
155
|
+
await pipeline(createReadStream(source), createGzip(), createWriteStream(temp, { flags: "wx" }));
|
|
156
|
+
const compressedBytes = (io.statSync ?? fs.statSync)(temp).size;
|
|
157
|
+
(io.renameSync ?? fs.renameSync)(temp, target);
|
|
158
|
+
return compressedBytes;
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
try {
|
|
162
|
+
(io.unlinkSync ?? fs.unlinkSync)(temp);
|
|
163
|
+
}
|
|
164
|
+
catch (cleanupError) {
|
|
165
|
+
if (cleanupError.code !== "ENOENT") {
|
|
166
|
+
process.stderr.write(`hq lanes: rollout compression temp cleanup failed for ${temp}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}\n`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=disk-guard.js.map
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export declare const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
3
3
|
/** Enables one automatic continuation after a missing-envelope repair. */
|
|
4
4
|
export declare const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
|
|
5
|
+
/** Count only children with live work against their senior's hire grant. */
|
|
6
|
+
export declare const LANE_HIRE_GRANT_FINISHED_LANES_FLAG = "lanes.hire-grant-count-live-only";
|
|
5
7
|
/** Enables missed-deadline lead nudges and second-miss operator escalation. */
|
|
6
8
|
export declare const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
|
|
7
9
|
/** Enables company-scoped lookup of a hostless lane ULID across Outpost indexes. */
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
3
3
|
/** Enables one automatic continuation after a missing-envelope repair. */
|
|
4
4
|
export const LANE_ENVELOPE_REPAIR_CONTINUE_FLAG = "lanes.envelope-repair-continue";
|
|
5
|
+
/** Count only children with live work against their senior's hire grant. */
|
|
6
|
+
export const LANE_HIRE_GRANT_FINISHED_LANES_FLAG = "lanes.hire-grant-count-live-only";
|
|
5
7
|
/** Enables missed-deadline lead nudges and second-miss operator escalation. */
|
|
6
8
|
export const LANE_AUTOPILOT_MISSED_DEADLINE_ESCALATION_FLAG = "lanes.autopilot-missed-deadline-escalation";
|
|
7
9
|
/** Enables company-scoped lookup of a hostless lane ULID across Outpost indexes. */
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type LaneRecord } from "./types.js";
|
|
2
2
|
import { LanesError } from "./errors.js";
|
|
3
3
|
export type LaneHireGrantRow = Pick<LaneRecord, "lane_id" | "state" | "senior" | "hire_grant">;
|
|
4
|
-
export declare function
|
|
4
|
+
export declare function isFinishedResumableHireChild(state: LaneRecord["state"]): boolean;
|
|
5
|
+
export declare function aliveDirectChildren<T extends LaneHireGrantRow>(lanes: readonly T[], parentLaneId: string, finishedChildrenDoNotCount?: boolean): T[];
|
|
5
6
|
export declare function assignedChildHireGrants(children: readonly LaneHireGrantRow[], exceptLaneId?: string): number;
|
|
6
7
|
export interface HireGrantAdmissionSnapshot {
|
|
7
8
|
parent: LaneHireGrantRow;
|
|
@@ -10,10 +11,10 @@ export interface HireGrantAdmissionSnapshot {
|
|
|
10
11
|
availableChildGrant: number;
|
|
11
12
|
}
|
|
12
13
|
/** Shared refusal logic for resuming a terminal lane under an active senior. */
|
|
13
|
-
export declare function terminalChildResumeHireGrantError(current: LaneRecord, snapshot: HireGrantAdmissionSnapshot): LanesError | null;
|
|
14
|
+
export declare function terminalChildResumeHireGrantError(current: LaneRecord, snapshot: HireGrantAdmissionSnapshot, finishedChildrenDoNotCount?: boolean): LanesError | null;
|
|
14
15
|
export declare function hireGrantCapacityError(parent: LaneHireGrantRow, children: readonly LaneHireGrantRow[]): LanesError;
|
|
15
16
|
/** Keep the occupancy snapshot and the resulting lane write in one parent-lock scope. */
|
|
16
|
-
export declare function withHireGrantAdmission<T>(hqRoot: string, parentLaneId: string, fn: (snapshot: HireGrantAdmissionSnapshot) => T, suppliedHierarchy?: readonly LaneHireGrantRow[], preferSuppliedLaneIds?: ReadonlySet<string
|
|
17
|
+
export declare function withHireGrantAdmission<T>(hqRoot: string, parentLaneId: string, fn: (snapshot: HireGrantAdmissionSnapshot) => T, suppliedHierarchy?: readonly LaneHireGrantRow[], preferSuppliedLaneIds?: ReadonlySet<string>, finishedChildrenDoNotCount?: boolean): T;
|
|
17
18
|
export interface HireGrantBackfillRow {
|
|
18
19
|
lane_id: string;
|
|
19
20
|
existing_grant: number | null;
|
package/dist/lib/lanes/grants.js
CHANGED
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { isTerminalLaneState } from "./types.js";
|
|
2
2
|
import { listLaneIds, readLane, withLaneHireLocks } from "./store.js";
|
|
3
3
|
import { LanesError } from "./errors.js";
|
|
4
|
-
export function
|
|
4
|
+
export function isFinishedResumableHireChild(state) {
|
|
5
|
+
return state === "awaiting_input" || state === "blocked" || state === "done";
|
|
6
|
+
}
|
|
7
|
+
export function aliveDirectChildren(lanes, parentLaneId, finishedChildrenDoNotCount = false) {
|
|
5
8
|
return lanes.filter((lane) => lane.senior.kind === "lane" &&
|
|
6
9
|
lane.senior.id === parentLaneId &&
|
|
7
|
-
!isTerminalLaneState(lane.state)
|
|
10
|
+
!isTerminalLaneState(lane.state) &&
|
|
11
|
+
!(finishedChildrenDoNotCount && isFinishedResumableHireChild(lane.state)));
|
|
8
12
|
}
|
|
9
13
|
export function assignedChildHireGrants(children, exceptLaneId) {
|
|
10
14
|
return children.reduce((sum, child) => child.lane_id === exceptLaneId ? sum : sum + (child.hire_grant ?? 0), 0);
|
|
11
15
|
}
|
|
12
16
|
/** Shared refusal logic for resuming a terminal lane under an active senior. */
|
|
13
|
-
export function terminalChildResumeHireGrantError(current, snapshot) {
|
|
14
|
-
|
|
17
|
+
export function terminalChildResumeHireGrantError(current, snapshot, finishedChildrenDoNotCount = false) {
|
|
18
|
+
const terminal = isTerminalLaneState(current.state);
|
|
19
|
+
if (!terminal && !(finishedChildrenDoNotCount && isFinishedResumableHireChild(current.state)))
|
|
15
20
|
return null;
|
|
16
21
|
const { parent, children, parentGrant, availableChildGrant } = snapshot;
|
|
17
22
|
if (isTerminalLaneState(parent.state)) {
|
|
@@ -21,7 +26,7 @@ export function terminalChildResumeHireGrantError(current, snapshot) {
|
|
|
21
26
|
return hireGrantCapacityError(parent, children);
|
|
22
27
|
const requestedGrant = current.hire_grant ?? 0;
|
|
23
28
|
if (requestedGrant > availableChildGrant) {
|
|
24
|
-
return new LanesError("hire_grant_carve_exceeded", `Senior lane ${parent.lane_id} has ${availableChildGrant} unassigned hire grant remaining, but terminal child ${current.lane_id} needs ${requestedGrant} to resume. Ask its senior for more: write an ask envelope with the desired total grant and why; the senior runs hq lanes grant ${parent.lane_id} --hire <n>.`, {
|
|
29
|
+
return new LanesError("hire_grant_carve_exceeded", `Senior lane ${parent.lane_id} has ${availableChildGrant} unassigned hire grant remaining, but ${terminal ? "terminal" : "finished"} child ${current.lane_id} needs ${requestedGrant} to resume. Ask its senior for more: write an ask envelope with the desired total grant and why; the senior runs hq lanes grant ${parent.lane_id} --hire <n>.`, {
|
|
25
30
|
senior_lane_id: parent.lane_id,
|
|
26
31
|
parent_grant: parentGrant,
|
|
27
32
|
alive_children: children.map((child) => child.lane_id),
|
|
@@ -72,7 +77,7 @@ export function hireGrantCapacityError(parent, children) {
|
|
|
72
77
|
});
|
|
73
78
|
}
|
|
74
79
|
/** Keep the occupancy snapshot and the resulting lane write in one parent-lock scope. */
|
|
75
|
-
export function withHireGrantAdmission(hqRoot, parentLaneId, fn, suppliedHierarchy, preferSuppliedLaneIds = new Set()) {
|
|
80
|
+
export function withHireGrantAdmission(hqRoot, parentLaneId, fn, suppliedHierarchy, preferSuppliedLaneIds = new Set(), finishedChildrenDoNotCount = false) {
|
|
76
81
|
return withLaneHireLocks(hqRoot, [parentLaneId], () => {
|
|
77
82
|
const ownerLanes = listLaneIds(hqRoot).map((id) => readLane(hqRoot, id));
|
|
78
83
|
let allLanes;
|
|
@@ -96,7 +101,7 @@ export function withHireGrantAdmission(hqRoot, parentLaneId, fn, suppliedHierarc
|
|
|
96
101
|
}
|
|
97
102
|
parent = suppliedParent;
|
|
98
103
|
}
|
|
99
|
-
const children = aliveDirectChildren(allLanes, parentLaneId);
|
|
104
|
+
const children = aliveDirectChildren(allLanes, parentLaneId, finishedChildrenDoNotCount);
|
|
100
105
|
const parentGrant = parent.hire_grant ?? 0;
|
|
101
106
|
const availableChildGrant = Math.max(0, parentGrant - assignedChildHireGrants(children));
|
|
102
107
|
return fn({ parent, children, parentGrant, availableChildGrant });
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { aliveDirectChildren } from "./grants.js";
|
|
2
1
|
import { LanesError } from "./errors.js";
|
|
3
2
|
import { isLaneState, isSeniorKind, } from "./types.js";
|
|
4
3
|
const HIRE_GRANT_CONTEXT_SCHEMA = "hq-lane-hire-grant-context.v1";
|
|
@@ -92,11 +91,11 @@ export function createOutpostHireGrantContext(target, lanes, supersededMessageCa
|
|
|
92
91
|
throw new LanesError("hire_grant_senior_invalid", `Lane ${target.lane_id} has no senior lane in the primary hierarchy`, { lane_id: target.lane_id, senior_lane_id: parentId ?? null });
|
|
93
92
|
}
|
|
94
93
|
relevant.set(parent.lane_id, parent);
|
|
95
|
-
for (const sibling of
|
|
94
|
+
for (const sibling of directChildren(lanes, parent.lane_id)) {
|
|
96
95
|
relevant.set(sibling.lane_id, sibling);
|
|
97
96
|
}
|
|
98
97
|
}
|
|
99
|
-
for (const child of
|
|
98
|
+
for (const child of directChildren(lanes, current.lane_id)) {
|
|
100
99
|
relevant.set(child.lane_id, child);
|
|
101
100
|
}
|
|
102
101
|
if (supersededMessageCallerId !== undefined) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
|
|
5
5
|
export { MAX_LANE_DEPTH } from "./constants.js";
|
|
6
|
-
export { assignedChildHireGrants, aliveDirectChildren, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
|
|
6
|
+
export { assignedChildHireGrants, aliveDirectChildren, isFinishedResumableHireChild, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
|
|
7
7
|
export type { HireGrantBackfillRow } from "./grants.js";
|
|
8
8
|
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
9
9
|
export type { AdmissionCapReason, AdmissionHost } from "./admission.js";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { AccountResolutionError, LaneNotFoundError, LaneSchemaError, LaneVersionConflict, LanesConfigError, LanesError, WorkerResolutionError, isLaneVersionConflict, isLanesError, } from "./errors.js";
|
|
5
5
|
export { MAX_LANE_DEPTH } from "./constants.js";
|
|
6
|
-
export { assignedChildHireGrants, aliveDirectChildren, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
|
|
6
|
+
export { assignedChildHireGrants, aliveDirectChildren, isFinishedResumableHireChild, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
|
|
7
7
|
export { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, hasEarlierEligibleQueuedLane, laneOccupiesRunningSlot, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, withLaneAdmissionLock, } from "./admission.js";
|
|
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";
|
|
@@ -61,6 +61,12 @@ export interface ReconcileSummary {
|
|
|
61
61
|
code?: string;
|
|
62
62
|
}>;
|
|
63
63
|
heartbeat_at: string | null;
|
|
64
|
+
rollout_retention?: {
|
|
65
|
+
scanned: number;
|
|
66
|
+
compressed: string[];
|
|
67
|
+
deleted: string[];
|
|
68
|
+
bytes_freed: number;
|
|
69
|
+
};
|
|
64
70
|
}
|
|
65
71
|
export interface ReconcileHost {
|
|
66
72
|
hostname: string;
|
|
@@ -144,7 +150,7 @@ export declare function runPassBody(hqRoot: string, deps: ReconcileDeps): Promis
|
|
|
144
150
|
*/
|
|
145
151
|
export declare function runReconcilePass(hqRoot: string, deps: ReconcileDeps): Promise<ReconcileSummary>;
|
|
146
152
|
export interface AnsweredLaneWakeResult {
|
|
147
|
-
status: "resumed" | "queued" | "awaiting_answers" | "failed";
|
|
153
|
+
status: "resumed" | "queued" | "awaiting_answers" | "deferred" | "failed";
|
|
148
154
|
lane_state: LaneRecord["state"];
|
|
149
155
|
queued_reason?: QueuedReason;
|
|
150
156
|
remaining_question_ids?: string[];
|
|
@@ -21,8 +21,10 @@ import { classifySessionLiveness, defaultLivenessDeps, loadLivenessIndex, readDa
|
|
|
21
21
|
import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, readPidRecord, readProcStarttime, readSpawnManifest, markSpawnFailure, stopLaneProcesses, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
|
|
22
22
|
import { appendCappedHistory, lanesStoreRoot, parseLaneRecord, parseQuestionRecord, readLane, readQuestion, listQuestionIds, updateLane, updateQuestion, } from "./store.js";
|
|
23
23
|
import { applyThrottleDecision, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyObservation, recordCpuBusyObservation, throttleStatePath, writeThrottleState, } from "./throttle.js";
|
|
24
|
-
import { readLanesRegistryFlag } from "./registry-flag.js";
|
|
25
|
-
import {
|
|
24
|
+
import { readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "./registry-flag.js";
|
|
25
|
+
import { LANE_HIRE_GRANT_FINISHED_LANES_FLAG } from "./flag-keys.js";
|
|
26
|
+
import { checkLaneDiskSpace, configuredCodexRolloutHomes, lowDiskGuardEnabledForLane, retainCodexRollouts } from "./disk-guard.js";
|
|
27
|
+
import { laneCompanyUidSource, resolveLaneCompany } from "./lane-company.js";
|
|
26
28
|
import { appendDropboxMessage } from "./dropbox.js";
|
|
27
29
|
import { DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG, listHostProcesses, runRunawayToolGuard, RUNAWAY_TOOL_GUARD_FLAG, } from "./runaway-tool-guard.js";
|
|
28
30
|
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, hasPendingKeepAliveWork, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
|
|
@@ -916,6 +918,55 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
916
918
|
return summary;
|
|
917
919
|
}
|
|
918
920
|
const config = loadLanesConfig(hqRoot);
|
|
921
|
+
try {
|
|
922
|
+
const companyRefs = new Map();
|
|
923
|
+
for (const lane of lanes) {
|
|
924
|
+
const company = resolveLaneCompany(hqRoot, lane).company;
|
|
925
|
+
if (company)
|
|
926
|
+
companyRefs.set(company.uid ?? company.slug, company);
|
|
927
|
+
}
|
|
928
|
+
const readFlag = deps.readRegistryFlag ?? readLanesRegistryFlag;
|
|
929
|
+
const homes = configuredCodexRolloutHomes(hqRoot, config, deps.host.hostname);
|
|
930
|
+
let retentionEnabled = false;
|
|
931
|
+
for (const company of homes.length > 0 ? companyRefs.values() : []) {
|
|
932
|
+
retentionEnabled ||= await readFlag("lanes.codex-rollout-retention", { company });
|
|
933
|
+
if (retentionEnabled)
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
if (retentionEnabled) {
|
|
937
|
+
const statePath = path.join(hqRoot, "workspace", "lanes-runs", `.rollout-retention-${deps.host.hostname.replaceAll(/[^a-zA-Z0-9_-]/g, "_")}.json`);
|
|
938
|
+
let lastRunAt;
|
|
939
|
+
try {
|
|
940
|
+
const state = JSON.parse(fs.readFileSync(statePath, "utf8"));
|
|
941
|
+
if (typeof state.last_run_at !== "string" || !Number.isFinite(Date.parse(state.last_run_at))) {
|
|
942
|
+
throw new LanesError("rollout_retention_state_invalid", `Invalid rollout-retention state at ${statePath}`, { path: statePath });
|
|
943
|
+
}
|
|
944
|
+
lastRunAt = Date.parse(state.last_run_at);
|
|
945
|
+
}
|
|
946
|
+
catch (err) {
|
|
947
|
+
if (err.code !== "ENOENT")
|
|
948
|
+
throw err;
|
|
949
|
+
}
|
|
950
|
+
const lowDisk = homes.some((home) => checkLaneDiskSpace(hqRoot, home, deps.spawnLaneDeps?.statfs, config.low_disk_min_free_bytes).low);
|
|
951
|
+
if (lowDisk || lastRunAt === undefined || nowMs - lastRunAt >= 60 * 60 * 1000) {
|
|
952
|
+
const result = await retainCodexRollouts({
|
|
953
|
+
homes,
|
|
954
|
+
lanes,
|
|
955
|
+
nowMs,
|
|
956
|
+
compressAfterMs: config.codex_rollout_compress_after_hours * 60 * 60 * 1000,
|
|
957
|
+
deleteAfterMs: config.codex_rollout_delete_after_days * 24 * 60 * 60 * 1000,
|
|
958
|
+
});
|
|
959
|
+
summary.rollout_retention = result;
|
|
960
|
+
fs.mkdirSync(path.dirname(statePath), { recursive: true });
|
|
961
|
+
const tempPath = `${statePath}.${process.pid}.tmp`;
|
|
962
|
+
fs.writeFileSync(tempPath, `${JSON.stringify({ last_run_at: nowIso }, null, 2)}\n`, { flag: "wx" });
|
|
963
|
+
fs.renameSync(tempPath, statePath);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
catch (err) {
|
|
968
|
+
markDirty(summary, { kind: "store", why: `Codex rollout retention failed: ${err instanceof Error ? err.message : String(err)}` });
|
|
969
|
+
}
|
|
919
970
|
const laneById = new Map(lanes.map((lane) => [lane.lane_id, lane]));
|
|
920
971
|
const readLaneSenior = (laneId) => {
|
|
921
972
|
const lane = laneById.get(laneId);
|
|
@@ -1333,6 +1384,32 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
|
|
|
1333
1384
|
repairCandidate &&
|
|
1334
1385
|
repairCandidate.envelope === null &&
|
|
1335
1386
|
(!repairCandidate.missing || envelopeRepairMode(classified.provider) === "resume")) {
|
|
1387
|
+
if (await lowDiskGuardEnabledForLane(hqRoot, classified, deps.readRegistryFlag)) {
|
|
1388
|
+
const repairConfig = loadLanesConfig(hqRoot);
|
|
1389
|
+
const repairAccount = classified.account
|
|
1390
|
+
? repairConfig.providers[classified.provider].accounts[classified.account]
|
|
1391
|
+
: undefined;
|
|
1392
|
+
if (!repairAccount) {
|
|
1393
|
+
throw new LanesError("low_disk_account_unavailable", `Cannot determine provider home for envelope repair of lane ${classified.lane_id}`, { lane_id: classified.lane_id });
|
|
1394
|
+
}
|
|
1395
|
+
const providerHome = effectiveAccountHome(repairAccount, deps.host.hostname).home;
|
|
1396
|
+
const free = checkLaneDiskSpace(hqRoot, providerHome, deps.spawnLaneDeps?.statfs, repairConfig.low_disk_min_free_bytes);
|
|
1397
|
+
if (free.low) {
|
|
1398
|
+
const measuredAt = now.toISOString();
|
|
1399
|
+
updateLane(hqRoot, classified.lane_id, classified.version, "reconcile", (current) => ({
|
|
1400
|
+
...current,
|
|
1401
|
+
state: "queued",
|
|
1402
|
+
queued_reason: "low_disk",
|
|
1403
|
+
queued_details: { reason: "low_disk", hq_free_bytes: free.hq.free_bytes, provider_free_bytes: free.provider.free_bytes, measured_at: measuredAt },
|
|
1404
|
+
queued_at: current.queued_at ?? measuredAt,
|
|
1405
|
+
admission_reserved_at: undefined,
|
|
1406
|
+
terminal_reason: undefined,
|
|
1407
|
+
terminal_details: undefined,
|
|
1408
|
+
}));
|
|
1409
|
+
summary.skipped.push({ kind: "lane", id: classified.lane_id, why: `low_disk: hq=${free.hq.free_bytes} provider=${free.provider.free_bytes}` });
|
|
1410
|
+
continue;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1336
1413
|
const spawnRepair = deps.spawnRepair ??
|
|
1337
1414
|
((root, repairLane, input) => spawnEnvelopeRepair(root, repairLane, input, spawnDepsForHost(deps)));
|
|
1338
1415
|
const repair = decideEnvelopeRepair(hqRoot, lane.lane_id, repairCandidate, nowMs, spawnRepair, () => undefined);
|
|
@@ -1676,7 +1753,10 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds) {
|
|
|
1676
1753
|
nowMs,
|
|
1677
1754
|
});
|
|
1678
1755
|
const spawnFn = deps.spawn ??
|
|
1679
|
-
((root, lane) => spawnLaneWorker(root, lane,
|
|
1756
|
+
(async (root, lane) => spawnLaneWorker(root, lane, {
|
|
1757
|
+
...spawnDepsForHost(deps),
|
|
1758
|
+
lowDiskGuardEnabled: await lowDiskGuardEnabledForLane(root, lane, deps.readRegistryFlag),
|
|
1759
|
+
}));
|
|
1680
1760
|
const running = lanes.filter((lane) => (lane.state === "running" ||
|
|
1681
1761
|
hasFreshAdmissionReservation(lane, now.getTime()) ||
|
|
1682
1762
|
hasActivePendingResume(lane, now.getTime())) &&
|
|
@@ -1749,7 +1829,7 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds) {
|
|
|
1749
1829
|
spawned.push({ ...admittedLane, state: "running" });
|
|
1750
1830
|
continue;
|
|
1751
1831
|
}
|
|
1752
|
-
const started = spawnFn(hqRoot, admittedLane);
|
|
1832
|
+
const started = await spawnFn(hqRoot, admittedLane);
|
|
1753
1833
|
if (decision.reason === "starvation_bound") {
|
|
1754
1834
|
casLaneIf(hqRoot, lane.lane_id, (current) => current.admitted_reason !== "starvation_bound", (current) => ({ ...current, admitted_reason: "starvation_bound" }), false);
|
|
1755
1835
|
}
|
|
@@ -1757,7 +1837,7 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds) {
|
|
|
1757
1837
|
id: started.lane_id,
|
|
1758
1838
|
from: "queued",
|
|
1759
1839
|
to: started.state,
|
|
1760
|
-
reason: decision.reason,
|
|
1840
|
+
reason: started.queued_reason ?? decision.reason,
|
|
1761
1841
|
...(started.queued_reason === "provider_binary_missing" &&
|
|
1762
1842
|
typeof started.terminal_details?.searched_path === "string"
|
|
1763
1843
|
? {
|
|
@@ -2668,9 +2748,17 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2668
2748
|
continue;
|
|
2669
2749
|
}
|
|
2670
2750
|
const seniorId = current.senior.kind === "lane" ? current.senior.id : undefined;
|
|
2751
|
+
let finishedChildrenDoNotCount = false;
|
|
2671
2752
|
if (seniorId !== undefined) {
|
|
2672
2753
|
let grantPreflight;
|
|
2673
2754
|
try {
|
|
2755
|
+
const companyResolution = resolveLaneCompany(hqRoot, current);
|
|
2756
|
+
const company = companyResolution.company ?? undefined;
|
|
2757
|
+
finishedChildrenDoNotCount = await readLanesRegistryFlagFailOff(LANE_HIRE_GRANT_FINISHED_LANES_FLAG, {
|
|
2758
|
+
company,
|
|
2759
|
+
...(company ? { companyUidSource: laneCompanyUidSource(companyResolution.source) } : {}),
|
|
2760
|
+
env: deps.env,
|
|
2761
|
+
}, deps.readRegistryFlag ?? readLanesRegistryFlag);
|
|
2674
2762
|
grantPreflight = withHireGrantAdmission(hqRoot, seniorId, (snapshot) => {
|
|
2675
2763
|
const latestLane = readLane(hqRoot, current.lane_id);
|
|
2676
2764
|
if (latestLane.version !== current.version ||
|
|
@@ -2680,11 +2768,11 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2680
2768
|
latestLane.senior.kind !== "lane" ||
|
|
2681
2769
|
latestLane.senior.id !== seniorId)
|
|
2682
2770
|
return { kind: "stale" };
|
|
2683
|
-
const refusal = terminalChildResumeHireGrantError(latestLane, snapshot);
|
|
2771
|
+
const refusal = terminalChildResumeHireGrantError(latestLane, snapshot, finishedChildrenDoNotCount);
|
|
2684
2772
|
return refusal === null
|
|
2685
2773
|
? { kind: "eligible" }
|
|
2686
2774
|
: { kind: "refused", error: refusal.error, message: refusal.message };
|
|
2687
|
-
});
|
|
2775
|
+
}, undefined, new Set(), finishedChildrenDoNotCount);
|
|
2688
2776
|
}
|
|
2689
2777
|
catch (err) {
|
|
2690
2778
|
markDirty(summary, {
|
|
@@ -2880,7 +2968,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2880
2968
|
latestLane.senior.kind !== "lane" ||
|
|
2881
2969
|
latestLane.senior.id !== seniorId)
|
|
2882
2970
|
return { changed: false, record: latestLane };
|
|
2883
|
-
const refusal = terminalChildResumeHireGrantError(latestLane, snapshot);
|
|
2971
|
+
const refusal = terminalChildResumeHireGrantError(latestLane, snapshot, finishedChildrenDoNotCount);
|
|
2884
2972
|
if (refusal !== null) {
|
|
2885
2973
|
summary.skipped.push({
|
|
2886
2974
|
kind: "lane",
|
|
@@ -2890,7 +2978,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
|
|
|
2890
2978
|
return { changed: false, record: latestLane };
|
|
2891
2979
|
}
|
|
2892
2980
|
return queue(seniorId);
|
|
2893
|
-
});
|
|
2981
|
+
}, undefined, new Set(), finishedChildrenDoNotCount);
|
|
2894
2982
|
}
|
|
2895
2983
|
catch (err) {
|
|
2896
2984
|
markDirty(summary, {
|
|
@@ -2953,7 +3041,10 @@ async function resumeAwaiting(hqRoot, lanes, questions, summary, deps, onlyLaneI
|
|
|
2953
3041
|
const throttle = await decideAdmission(hqRoot, config.throttle, throttleDeps, deps.cpuBusyFraction, deps.cpuObservation);
|
|
2954
3042
|
throttleDecision = throttle.decision;
|
|
2955
3043
|
}
|
|
2956
|
-
const spawnFn = deps.spawn ?? ((root, lane) => spawnLaneWorker(root, lane,
|
|
3044
|
+
const spawnFn = deps.spawn ?? (async (root, lane) => spawnLaneWorker(root, lane, {
|
|
3045
|
+
...spawnDepsForHost(deps),
|
|
3046
|
+
lowDiskGuardEnabled: await lowDiskGuardEnabledForLane(root, lane, deps.readRegistryFlag),
|
|
3047
|
+
}));
|
|
2957
3048
|
const nowMs = deps.now().getTime();
|
|
2958
3049
|
const fallbackAnswerAt = new Date(nowMs).toISOString();
|
|
2959
3050
|
const answerAtByLane = new Map();
|
|
@@ -2992,6 +3083,39 @@ async function resumeAwaiting(hqRoot, lanes, questions, summary, deps, onlyLaneI
|
|
|
2992
3083
|
if (pending.length > 0 || answered.length === 0)
|
|
2993
3084
|
continue;
|
|
2994
3085
|
try {
|
|
3086
|
+
if (current.senior.kind === "lane" && current.senior.id !== undefined) {
|
|
3087
|
+
const companyResolution = resolveLaneCompany(hqRoot, current);
|
|
3088
|
+
const company = companyResolution.company ?? undefined;
|
|
3089
|
+
const finishedChildrenDoNotCount = await readLanesRegistryFlagFailOff(LANE_HIRE_GRANT_FINISHED_LANES_FLAG, {
|
|
3090
|
+
company,
|
|
3091
|
+
...(company ? { companyUidSource: laneCompanyUidSource(companyResolution.source) } : {}),
|
|
3092
|
+
env: deps.env,
|
|
3093
|
+
}, deps.readRegistryFlag ?? readLanesRegistryFlag);
|
|
3094
|
+
if (finishedChildrenDoNotCount) {
|
|
3095
|
+
const admission = withHireGrantAdmission(hqRoot, current.senior.id, (snapshot) => {
|
|
3096
|
+
const latest = readLane(hqRoot, current.lane_id);
|
|
3097
|
+
if (latest.version !== current.version || latest.state !== "awaiting_input") {
|
|
3098
|
+
return { kind: "stale" };
|
|
3099
|
+
}
|
|
3100
|
+
const refusal = terminalChildResumeHireGrantError(latest, snapshot, true);
|
|
3101
|
+
return refusal === null
|
|
3102
|
+
? { kind: "eligible" }
|
|
3103
|
+
: { kind: "refused", error: refusal.error, message: refusal.message };
|
|
3104
|
+
}, undefined, new Set(), true);
|
|
3105
|
+
if (admission.kind === "stale")
|
|
3106
|
+
continue;
|
|
3107
|
+
if (admission.kind === "refused") {
|
|
3108
|
+
if (!deps.dryRun)
|
|
3109
|
+
writeAnsweredQuestions(hqRoot, current, questions);
|
|
3110
|
+
summary.skipped.push({
|
|
3111
|
+
kind: "lane",
|
|
3112
|
+
id: current.lane_id,
|
|
3113
|
+
why: `${admission.error}: ${admission.message}`,
|
|
3114
|
+
});
|
|
3115
|
+
continue;
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
2995
3119
|
let profileMaxConcurrent;
|
|
2996
3120
|
if (current.worker_profile !== undefined) {
|
|
2997
3121
|
profileMaxConcurrent = loadWorkerProfile(hqRoot, current.worker_profile).meta.max_concurrent;
|
|
@@ -3062,7 +3186,7 @@ async function resumeAwaiting(hqRoot, lanes, questions, summary, deps, onlyLaneI
|
|
|
3062
3186
|
running.push({ ...current, state: "running" });
|
|
3063
3187
|
continue;
|
|
3064
3188
|
}
|
|
3065
|
-
const started = spawnFn(hqRoot, lane);
|
|
3189
|
+
const started = await spawnFn(hqRoot, lane);
|
|
3066
3190
|
if (started.state === "running")
|
|
3067
3191
|
running.push(started);
|
|
3068
3192
|
summary.resumed.push({
|
|
@@ -3184,6 +3308,13 @@ export async function wakeAnsweredLaneNow(hqRoot, laneId, deps) {
|
|
|
3184
3308
|
};
|
|
3185
3309
|
}
|
|
3186
3310
|
const failure = summary.skipped.find((skip) => skip.kind === "lane" && skip.id === laneId);
|
|
3311
|
+
if (latest.state === "awaiting_input" && failure?.why.startsWith("hire_grant_exhausted:")) {
|
|
3312
|
+
return {
|
|
3313
|
+
status: "deferred",
|
|
3314
|
+
lane_state: latest.state,
|
|
3315
|
+
error: failure.why,
|
|
3316
|
+
};
|
|
3317
|
+
}
|
|
3187
3318
|
return {
|
|
3188
3319
|
status: "failed",
|
|
3189
3320
|
lane_state: latest.state,
|
|
@@ -25,5 +25,11 @@ export declare function readLanesFlagLocalOverride(flagKey: string, env?: NodeJS
|
|
|
25
25
|
* written to disk. A local override is checked on every call and wins first.
|
|
26
26
|
*/
|
|
27
27
|
export declare function readLanesRegistryFlag(flagKey: string, options?: ReadLanesFlagOptions): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Flag readers normally report their own failures and return the safe default.
|
|
30
|
+
* Keep enforcement callers safe if an injected or unexpected reader rejects.
|
|
31
|
+
*/
|
|
32
|
+
export declare function readLanesRegistryFlagFailOff(flagKey: string, options?: ReadLanesFlagOptions, reader?: typeof readLanesRegistryFlag): Promise<boolean>;
|
|
33
|
+
export declare function _resetThrownFlagReadWarningsForTests(): void;
|
|
28
34
|
export declare function _resetLanesRegistryFlagCacheForTests(): void;
|
|
29
35
|
//# sourceMappingURL=registry-flag.d.ts.map
|
|
@@ -7,6 +7,7 @@ export function readLanesFlagLocalOverride(flagKey, env = process.env) {
|
|
|
7
7
|
return readLocalOverride(flagKey, env);
|
|
8
8
|
}
|
|
9
9
|
const registryReads = new Map();
|
|
10
|
+
const reportedThrownFlagReads = new Set();
|
|
10
11
|
class MissingFlagTokenError extends Error {
|
|
11
12
|
constructor() {
|
|
12
13
|
super("No cached HQ identity token is available");
|
|
@@ -67,6 +68,30 @@ export async function readLanesRegistryFlag(flagKey, options = {}) {
|
|
|
67
68
|
registryReads.set(key, pending);
|
|
68
69
|
return pending;
|
|
69
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Flag readers normally report their own failures and return the safe default.
|
|
73
|
+
* Keep enforcement callers safe if an injected or unexpected reader rejects.
|
|
74
|
+
*/
|
|
75
|
+
export async function readLanesRegistryFlagFailOff(flagKey, options = {}, reader = readLanesRegistryFlag) {
|
|
76
|
+
try {
|
|
77
|
+
return await reader(flagKey, options);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (!reportedThrownFlagReads.has(flagKey)) {
|
|
81
|
+
reportedThrownFlagReads.add(flagKey);
|
|
82
|
+
process.stderr.write(`${JSON.stringify({
|
|
83
|
+
event: "lanes.flag_read_threw",
|
|
84
|
+
flag: flagKey,
|
|
85
|
+
error: error instanceof Error ? "Error" : "NonError",
|
|
86
|
+
fallback: false,
|
|
87
|
+
})}\n`);
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function _resetThrownFlagReadWarningsForTests() {
|
|
93
|
+
reportedThrownFlagReads.clear();
|
|
94
|
+
}
|
|
70
95
|
async function readRegistryValue(flagKey, endpoint, options) {
|
|
71
96
|
const company = options.company;
|
|
72
97
|
const companyUid = company?.uid?.trim();
|