@indigoai-us/hq-cli 5.312.2-beta.0 → 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.
Files changed (81) hide show
  1. package/CHANGELOG.md +9 -2
  2. package/assets/scaffold/core/scripts/lib/work-mesh-live-hook.sh +378 -0
  3. package/assets/scaffold/core/scripts/lib/work-mesh-live-rebind.sh +141 -0
  4. package/assets/scaffold/core/scripts/migrate-policy-triggers.sh +373 -0
  5. package/assets/scaffold/core/scripts/register-project.sh +262 -0
  6. package/assets/scaffold/core/scripts/work-mesh-live-rebind.sh +59 -0
  7. package/dist/command-catalog.generated.d.ts +80 -0
  8. package/dist/command-catalog.generated.js +99 -0
  9. package/dist/commands/cloud.js +0 -1
  10. package/dist/commands/company-rename-flag.d.ts +4 -0
  11. package/dist/commands/company-rename-flag.js +7 -0
  12. package/dist/commands/company-rename.d.ts +4 -0
  13. package/dist/commands/company-rename.js +90 -0
  14. package/dist/commands/company.js +2 -0
  15. package/dist/commands/core-eval-trigger-native.d.ts +4 -0
  16. package/dist/commands/core-eval-trigger-native.js +12 -0
  17. package/dist/commands/core-knowledge-prefs.d.ts +0 -1
  18. package/dist/commands/core-knowledge-prefs.js +1 -1
  19. package/dist/commands/core-migrate-policy-triggers-native.d.ts +21 -0
  20. package/dist/commands/core-migrate-policy-triggers-native.js +378 -0
  21. package/dist/commands/core-native-hook-helper-commands.d.ts +4 -0
  22. package/dist/commands/core-native-hook-helper-commands.js +56 -0
  23. package/dist/commands/core-native-hook-helpers-flag.d.ts +4 -0
  24. package/dist/commands/core-native-hook-helpers-flag.js +7 -0
  25. package/dist/commands/core-register-project-native.d.ts +30 -0
  26. package/dist/commands/core-register-project-native.js +347 -0
  27. package/dist/commands/core-work-mesh-live-rebind-native.d.ts +18 -0
  28. package/dist/commands/core-work-mesh-live-rebind-native.js +262 -0
  29. package/dist/commands/core-worker-share-native.d.ts +0 -1
  30. package/dist/commands/core-worker-share-native.js +1 -1
  31. package/dist/commands/core.js +7 -0
  32. package/dist/commands/lanes-message.js +3 -0
  33. package/dist/commands/lanes-questions.js +2 -1
  34. package/dist/commands/lanes-resume.js +3 -0
  35. package/dist/commands/lanes.js +151 -21
  36. package/dist/fast-core.js +10 -3
  37. package/dist/fast-scaffold.d.ts +2 -0
  38. package/dist/fast-scaffold.js +11 -0
  39. package/dist/lib/doctor/checks/work-context.js +9 -1
  40. package/dist/lib/lanes/config.d.ts +3 -0
  41. package/dist/lib/lanes/config.js +14 -0
  42. package/dist/lib/lanes/disk-guard.d.ts +66 -0
  43. package/dist/lib/lanes/disk-guard.js +172 -0
  44. package/dist/lib/lanes/flag-keys.d.ts +2 -0
  45. package/dist/lib/lanes/flag-keys.js +2 -0
  46. package/dist/lib/lanes/grants.d.ts +4 -3
  47. package/dist/lib/lanes/grants.js +12 -7
  48. package/dist/lib/lanes/hire-grant-dispatch.js +2 -3
  49. package/dist/lib/lanes/index.d.ts +1 -1
  50. package/dist/lib/lanes/index.js +1 -1
  51. package/dist/lib/lanes/reconcile.d.ts +7 -1
  52. package/dist/lib/lanes/reconcile.js +142 -11
  53. package/dist/lib/lanes/registry-flag.d.ts +6 -0
  54. package/dist/lib/lanes/registry-flag.js +25 -0
  55. package/dist/lib/lanes/spawn.d.ts +5 -0
  56. package/dist/lib/lanes/spawn.js +26 -1
  57. package/dist/lib/lanes/store.js +11 -0
  58. package/dist/lib/lanes/types.d.ts +11 -1
  59. package/dist/lib/lanes/types.js +1 -0
  60. package/dist/lib/mesh/client.js +23 -5
  61. package/dist/lib/mesh/denial-codes.d.ts +5 -0
  62. package/dist/lib/mesh/denial-codes.js +14 -0
  63. package/dist/lib/mesh/live/daemon/doctor.d.ts +2 -0
  64. package/dist/lib/mesh/live/daemon/doctor.js +11 -1
  65. package/dist/lib/search-index/embed-lock.d.ts +2 -0
  66. package/dist/lib/search-index/embed-lock.js +10 -1
  67. package/dist/lib/work-context/contract.d.ts +3 -0
  68. package/dist/lib/work-context/contract.schema.json +3 -1
  69. package/dist/lib/work-context/outbox.d.ts +14 -5
  70. package/dist/lib/work-context/outbox.js +48 -5
  71. package/dist/lib/work-context/reconcile.js +26 -5
  72. package/dist/main.d.ts +20 -0
  73. package/dist/main.js +8 -1
  74. package/dist/utils/core-eval-trigger.d.ts +4 -0
  75. package/dist/utils/core-eval-trigger.js +88 -0
  76. package/dist/utils/pack-contributions.d.ts +0 -5
  77. package/dist/utils/pack-contributions.js +1 -1
  78. package/dist/utils/redact-error-text.d.ts +0 -2
  79. package/dist/utils/redact-error-text.js +1 -1
  80. package/dist/utils/sandbox-runner-client.d.ts +2 -1
  81. 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 aliveDirectChildren<T extends LaneHireGrantRow>(lanes: readonly T[], parentLaneId: string): T[];
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>): T;
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;
@@ -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 aliveDirectChildren(lanes, parentLaneId) {
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
- if (!isTerminalLaneState(current.state))
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 aliveDirectChildren(lanes, parent.lane_id)) {
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 aliveDirectChildren(lanes, current.lane_id)) {
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";
@@ -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 { resolveLaneCompany } from "./lane-company.js";
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, spawnDepsForHost(deps)));
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, spawnDepsForHost(deps)));
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();