@indigoai-us/hq-cli 5.342.0 → 5.342.2

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.
@@ -18,7 +18,7 @@ import { AccountResolutionError, LanesError, isLaneVersionConflict } from "./err
18
18
  import { loadWorkerProfile } from "./profile.js";
19
19
  import { terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
20
20
  import { classifySessionLiveness, defaultLivenessDeps, loadLivenessIndex, readDaemonPresenceConnected, unindexedClaudeSessionLiveness, } from "./liveness.js";
21
- import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, readPidRecord, readProcStarttime, readSpawnManifest, markSpawnFailure, stopLaneProcesses, spawnEnvelopeRepair, spawnLaneWorker, retryLaneOnAlternateAccount, } from "./spawn.js";
21
+ import { ANSWERED_QUESTIONS_FILE, armLaneWatcher, envelopeRepairMode, isPidRecordAlive, laneRunDir, nextEnvelopePath, 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
24
  import { readLanesRegistryFlag, readLanesRegistryFlagFailOff } from "./registry-flag.js";
@@ -87,6 +87,27 @@ function markDirty(summary, skip) {
87
87
  summary.ok = false;
88
88
  logSkip(skip);
89
89
  }
90
+ function isMissingPathError(error) {
91
+ const code = error?.code;
92
+ return code === "ENOENT" || code === "ENOTDIR";
93
+ }
94
+ function recordMissingCodexHomeNote(summary, home) {
95
+ const note = `Codex rollout retention skipped home absent on this host: ${home}`;
96
+ summary.notes ??= [];
97
+ summary.notes.push(note);
98
+ process.stderr.write(`hq lanes reconcile: note: ${note}\n`);
99
+ }
100
+ function homeIsMissingAfterError(home, error) {
101
+ if (!isMissingPathError(error))
102
+ return false;
103
+ try {
104
+ fs.statSync(home);
105
+ return false;
106
+ }
107
+ catch (statError) {
108
+ return isMissingPathError(statError);
109
+ }
110
+ }
90
111
  function terminalizeModelUnsupported(hqRoot, lane, err) {
91
112
  if (!(err instanceof LanesError) || err.error !== "model_unsupported")
92
113
  return;
@@ -940,14 +961,28 @@ export async function runPassBody(hqRoot, deps) {
940
961
  companyRefs.set(company.uid ?? company.slug, company);
941
962
  }
942
963
  const readFlag = deps.readRegistryFlag ?? readLanesRegistryFlag;
943
- const homes = configuredCodexRolloutHomes(hqRoot, config, deps.host.hostname);
964
+ const configuredHomes = configuredCodexRolloutHomes(hqRoot, config, deps.host.hostname);
944
965
  let retentionEnabled = false;
945
- for (const company of homes.length > 0 ? companyRefs.values() : []) {
966
+ for (const company of configuredHomes.length > 0 ? companyRefs.values() : []) {
946
967
  retentionEnabled ||= await readFlag("lanes.codex-rollout-retention", { company });
947
968
  if (retentionEnabled)
948
969
  break;
949
970
  }
950
971
  if (retentionEnabled) {
972
+ const presentHomes = [];
973
+ for (const home of configuredHomes) {
974
+ try {
975
+ fs.statSync(home);
976
+ presentHomes.push(home);
977
+ }
978
+ catch (err) {
979
+ if (isMissingPathError(err)) {
980
+ recordMissingCodexHomeNote(summary, home);
981
+ continue;
982
+ }
983
+ throw err;
984
+ }
985
+ }
951
986
  const statePath = path.join(hqRoot, "workspace", "lanes-runs", `.rollout-retention-${deps.host.hostname.replaceAll(/[^a-zA-Z0-9_-]/g, "_")}.json`);
952
987
  let lastRunAt;
953
988
  try {
@@ -961,20 +996,38 @@ export async function runPassBody(hqRoot, deps) {
961
996
  if (err.code !== "ENOENT")
962
997
  throw err;
963
998
  }
964
- const lowDisk = homes.some((home) => checkLaneDiskSpace(hqRoot, home, deps.spawnLaneDeps?.statfs, config.low_disk_min_free_bytes).low);
999
+ let lowDisk = false;
1000
+ const retentionHomes = [];
1001
+ for (const home of presentHomes) {
1002
+ try {
1003
+ const disk = checkLaneDiskSpace(hqRoot, home, deps.spawnLaneDeps?.statfs, config.low_disk_min_free_bytes);
1004
+ lowDisk = lowDisk || disk.low;
1005
+ retentionHomes.push(home);
1006
+ }
1007
+ catch (err) {
1008
+ if (homeIsMissingAfterError(home, err)) {
1009
+ recordMissingCodexHomeNote(summary, home);
1010
+ continue;
1011
+ }
1012
+ throw err;
1013
+ }
1014
+ }
965
1015
  if (lowDisk || lastRunAt === undefined || nowMs - lastRunAt >= 60 * 60 * 1000) {
966
1016
  const result = await retainCodexRollouts({
967
- homes,
1017
+ homes: retentionHomes,
968
1018
  lanes,
969
1019
  nowMs,
1020
+ dryRun: deps.dryRun,
970
1021
  compressAfterMs: config.codex_rollout_compress_after_hours * 60 * 60 * 1000,
971
1022
  deleteAfterMs: config.codex_rollout_delete_after_days * 24 * 60 * 60 * 1000,
972
1023
  });
973
1024
  summary.rollout_retention = result;
974
- fs.mkdirSync(path.dirname(statePath), { recursive: true });
975
- const tempPath = `${statePath}.${process.pid}.tmp`;
976
- fs.writeFileSync(tempPath, `${JSON.stringify({ last_run_at: nowIso }, null, 2)}\n`, { flag: "wx" });
977
- fs.renameSync(tempPath, statePath);
1025
+ if (!deps.dryRun) {
1026
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
1027
+ const tempPath = `${statePath}.${process.pid}.tmp`;
1028
+ fs.writeFileSync(tempPath, `${JSON.stringify({ last_run_at: nowIso }, null, 2)}\n`, { flag: "wx" });
1029
+ fs.renameSync(tempPath, statePath);
1030
+ }
978
1031
  }
979
1032
  }
980
1033
  }
@@ -1848,6 +1901,9 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds, exclu
1848
1901
  }
1849
1902
  try {
1850
1903
  if (deps.dryRun) {
1904
+ // Spawn reserves an envelope path. Probe it without writing so a dry
1905
+ // run does not promise admission when the run cannot start.
1906
+ nextEnvelopePath(laneRunDir(hqRoot, lane.lane_id));
1851
1907
  summary.admitted.push({
1852
1908
  id: lane.lane_id,
1853
1909
  from: "queued",
@@ -1907,6 +1963,18 @@ async function admitQueued(hqRoot, lanes, summary, deps, now, onlyLaneIds, exclu
1907
1963
  }
1908
1964
  }
1909
1965
  terminalizeModelUnsupported(hqRoot, lane, err);
1966
+ if (err instanceof LanesError && err.error !== "model_unsupported") {
1967
+ const queueRecorded = deps.dryRun || casLaneIf(hqRoot, lane.lane_id, (current) => current.state === "queued", (current) => ({
1968
+ ...current,
1969
+ queued_reason: "admit_failed",
1970
+ queued_details: { reason: "admit_failed", error: err.error, message: err.message },
1971
+ admission_reserved_at: undefined,
1972
+ }), false).changed;
1973
+ if (queueRecorded) {
1974
+ summary.skipped.push({ kind: "lane", id: lane.lane_id, why: `admit_failed:${err.error}` });
1975
+ continue;
1976
+ }
1977
+ }
1910
1978
  markDirty(summary, {
1911
1979
  kind: "lane",
1912
1980
  id: lane.lane_id,
@@ -983,16 +983,21 @@ function proveDetachedViaPs(pid) {
983
983
  }
984
984
  export function nextEnvelopePath(runDir) {
985
985
  const dir = path.join(runDir, "envelopes");
986
+ let names = new Set();
987
+ try {
988
+ names = new Set(fs.readdirSync(dir));
989
+ }
990
+ catch (err) {
991
+ if (err.code !== "ENOENT")
992
+ throw err;
993
+ }
986
994
  let n = 1;
987
995
  while (true) {
988
996
  const name = `${String(n).padStart(3, "0")}.json`;
989
997
  const filePath = path.join(dir, name);
990
- if (!fs.existsSync(filePath))
998
+ if (!names.has(name))
991
999
  return filePath;
992
1000
  n += 1;
993
- if (n > 999) {
994
- throw new LanesError("envelope_sequence_exhausted", `No free envelope slot under ${dir}`, { dir });
995
- }
996
1001
  }
997
1002
  }
998
1003
  export function envelopeInstruction(envelopePath) {
@@ -623,6 +623,16 @@ export function parseLaneRecord(raw, filePath) {
623
623
  }
624
624
  requireTimestamp(detailsRecord.measured_at, "queued_details.measured_at", filePath);
625
625
  }
626
+ else if (detailsRecord.reason === "admit_failed") {
627
+ if (raw.state !== "queued" || raw.queued_reason !== "admit_failed" ||
628
+ typeof detailsRecord.error !== "string" || detailsRecord.error.trim() === "" ||
629
+ typeof detailsRecord.message !== "string" || detailsRecord.message.trim() === "") {
630
+ throw new LaneSchemaError("queued_details must describe a failed lane admission", {
631
+ path: filePath,
632
+ field: "queued_details",
633
+ });
634
+ }
635
+ }
626
636
  else {
627
637
  throw new LaneSchemaError("queued_details must describe a supported queue condition", {
628
638
  path: filePath,
@@ -177,7 +177,7 @@ export declare const TERMINAL_LANE_STATES: readonly ["done", "failed", "interrup
177
177
  export type TerminalLaneState = (typeof TERMINAL_LANE_STATES)[number];
178
178
  export declare const LANE_PROVIDERS: readonly ["claude", "codex", "grok"];
179
179
  export type LaneProvider = (typeof LANE_PROVIDERS)[number];
180
- export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing", "account_retry", "worker_profile_changed", "low_disk"];
180
+ export declare const QUEUED_REASONS: readonly ["cap", "provider_cap", "account_cap", "profile_cap", "queue_position", "throttle", "provider_binary_missing", "account_retry", "worker_profile_changed", "low_disk", "admit_failed"];
181
181
  export type QueuedReason = (typeof QUEUED_REASONS)[number];
182
182
  export type QueuedDetails = {
183
183
  reason: "worker_profile_changed";
@@ -193,6 +193,10 @@ export type QueuedDetails = {
193
193
  hq_free_bytes: number;
194
194
  provider_free_bytes: number;
195
195
  measured_at: string;
196
+ } | {
197
+ reason: "admit_failed";
198
+ error: string;
199
+ message: string;
196
200
  };
197
201
  export declare function isQueuedReason(value: unknown): value is QueuedReason;
198
202
  export declare const HOST_KINDS: readonly ["local", "outpost"];
@@ -113,6 +113,7 @@ export const QUEUED_REASONS = [
113
113
  "account_retry",
114
114
  "worker_profile_changed",
115
115
  "low_disk",
116
+ "admit_failed",
116
117
  ];
117
118
  export function isQueuedReason(value) {
118
119
  return typeof value === "string" && QUEUED_REASONS.includes(value);
@@ -34,6 +34,7 @@
34
34
  import * as fs from "node:fs";
35
35
  import * as path from "node:path";
36
36
  import { LanesError } from "./errors.js";
37
+ import { compareEnvelopeNames } from "./envelope-sequence.js";
37
38
  import { questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
38
39
  import { isTerminalLaneState, ENVELOPE_INVALID_REASON, parseLaneEnvelope, STORY_STATUSES, LANE_PROVIDERS, } from "./types.js";
39
40
  import { laneStories, terminalEnvelopeStoryStatus } from "./stories.js";
@@ -619,8 +620,8 @@ export function readLatestEnvelopeFile(hqRoot, laneId, minName) {
619
620
  names = fs
620
621
  .readdirSync(dir)
621
622
  .filter((name) => name.endsWith(".json"))
622
- .filter((name) => minName === undefined || name >= minName)
623
- .sort();
623
+ .filter((name) => minName === undefined || compareEnvelopeNames(name, minName) >= 0)
624
+ .sort(compareEnvelopeNames);
624
625
  }
625
626
  catch (err) {
626
627
  const code = err.code;
@@ -0,0 +1,103 @@
1
+ import { writeSync } from "node:fs";
2
+ /**
3
+ * A browser login is safe only when a person can answer it. This mirrors the
4
+ * repository's existing terminal convention in `canOfferTeamUpgrade`: both
5
+ * stdin and stderr must be TTYs, and CI is always non-interactive.
6
+ */
7
+ export declare function secretsCredentialOptions(): {
8
+ interactive: boolean;
9
+ };
10
+ export interface SecretsScopeOpts {
11
+ company?: string;
12
+ personal?: boolean;
13
+ }
14
+ export declare function scopeOpts(opts: SecretsScopeOpts): {
15
+ personal: boolean;
16
+ companySlug: string | undefined;
17
+ };
18
+ export type SecretTier = "standard" | "sensitive" | "nuclear";
19
+ export type SecretScriptLockMode = "off" | "enforced";
20
+ export type SecretUsageChannel = "run" | "exec" | "env" | "sandbox" | "reveal" | "submit-link";
21
+ export interface SecretScriptUsage {
22
+ scriptId: string;
23
+ path: string;
24
+ sha256: string;
25
+ attestationLevel: string;
26
+ }
27
+ export interface SecretUsage {
28
+ channel: SecretUsageChannel;
29
+ reason?: string;
30
+ script?: SecretScriptUsage;
31
+ }
32
+ export interface SecretMetadata {
33
+ tier?: SecretTier;
34
+ scriptLock?: {
35
+ mode?: SecretScriptLockMode;
36
+ requiredAttestation?: string;
37
+ };
38
+ cacheTtlMs?: number;
39
+ revokedAt?: string | null;
40
+ }
41
+ export interface SecretLoadSuccessRow extends SecretMetadata {
42
+ name: string;
43
+ value?: string;
44
+ version?: number;
45
+ }
46
+ export interface SecretLoadResponse {
47
+ secrets: SecretLoadSuccessRow[];
48
+ errors: Array<{
49
+ name: string;
50
+ code: string;
51
+ message?: string;
52
+ }>;
53
+ }
54
+ export declare function highSecuritySandboxOnlyMessage(secretName?: string): string;
55
+ export declare function normalizeSecretTier(tier?: string): SecretTier;
56
+ export declare function normalizeScriptLockMode(mode?: string): SecretScriptLockMode;
57
+ export declare function extractApiMessage(body: Record<string, unknown>, fallback: string): string;
58
+ export declare function buildSecretUsage(channel: SecretUsageChannel, scriptPath?: string, scriptId?: string, attestationLevel?: string): Promise<SecretUsage>;
59
+ export declare function parseSecretNameList(input: string[] | undefined): string[];
60
+ export declare function loadRevealedSecrets(token: string, companyUid: string, keys: string[], usage?: SecretUsage, options?: {
61
+ allowOfflineFallback?: boolean;
62
+ }): Promise<Map<string, string>>;
63
+ /**
64
+ * Emit `hq secrets env` output and terminate deterministically.
65
+ *
66
+ * The process MUST exit even when the consuming pipe has gone away. The
67
+ * orphaned-process leak (personal/projects/hq-secrets-process-leak) was caused
68
+ * by relying on `process.stdout.write(payload, () => process.exit(0))`: when the
69
+ * parent shell reading `source <(hq secrets env …)` is killed on timeout, the
70
+ * stdout pipe loses its reader, the write callback never fires, `process.exit(0)`
71
+ * is never reached, and — because `env` forces no other exit — the process parks
72
+ * in `epoll_pwait(-1)` forever, reparented to PID 1. Hundreds accumulated this
73
+ * way, exhausting swap and driving load into triple digits.
74
+ *
75
+ * Writing synchronously to fd 1 and then exiting unconditionally removes every
76
+ * dependency on the event loop draining and on the reader still being attached:
77
+ * - a live reader receives the export lines (synchronous write reaches the
78
+ * kernel pipe buffer before we exit, so nothing is truncated);
79
+ * - a dead reader surfaces as EPIPE, which we swallow — the secret is already
80
+ * resolved and there is nothing left to deliver, so a clean exit is correct;
81
+ * - EAGAIN (stdout in non-blocking mode with a momentarily full buffer) is
82
+ * retried a bounded number of times. `env` payloads are a few short export
83
+ * lines, far under the 64 KiB pipe buffer, so this effectively never spins.
84
+ *
85
+ * `exit` and `write` are injectable for tests; production uses the real ones.
86
+ */
87
+ export declare function emitEnvExportsAndExit(payload: string, exit?: (code: number) => never, write?: typeof writeSync): never;
88
+ /**
89
+ * Shared `secrets env` action for Commander and the default-off lightweight
90
+ * entrypoint. The fast entrypoint still runs the same credential, usage-audit,
91
+ * server authorization, cache-fallback, redaction, and output code.
92
+ */
93
+ export declare function runSecretsEnvCommand(opts: {
94
+ only: string[];
95
+ script?: string;
96
+ scriptId?: string;
97
+ }, parentScope: {
98
+ company?: string;
99
+ personal?: boolean;
100
+ }, options?: {
101
+ fallbackOnAuthFailure?: boolean;
102
+ }): Promise<"handled" | "fallback">;
103
+ //# sourceMappingURL=secrets-env-action.d.ts.map