@indigoai-us/hq-cli 5.345.38 → 5.345.40

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 (36) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/assets/scaffold/core/scripts/derive-trigger-facts.sh +12 -3
  3. package/assets/scaffold/core/scripts/rebuild-companies-index.sh +12 -0
  4. package/dist/command-catalog.generated.d.ts +43 -0
  5. package/dist/command-catalog.generated.js +54 -0
  6. package/dist/commands/core-dm-bind-native.js +5 -4
  7. package/dist/commands/db-destroy.d.ts +15 -0
  8. package/dist/commands/db-destroy.js +143 -0
  9. package/dist/commands/db-dump.d.ts +9 -0
  10. package/dist/commands/db-dump.js +19 -11
  11. package/dist/commands/db.js +2 -0
  12. package/dist/commands/files-browse.js +1 -1
  13. package/dist/commands/lanes-message.js +14 -5
  14. package/dist/commands/lanes-resume.js +62 -14
  15. package/dist/commands/lanes.js +21 -4
  16. package/dist/commands/report.js +5 -1
  17. package/dist/lib/db/control-plane.d.ts +10 -0
  18. package/dist/lib/db/control-plane.js +18 -0
  19. package/dist/lib/deploy-api.js +2 -0
  20. package/dist/lib/doctor/fix/apply.d.ts +2 -0
  21. package/dist/lib/doctor/fix/apply.js +52 -1
  22. package/dist/lib/flag-registry.d.ts +1 -1
  23. package/dist/lib/flag-registry.js +20 -7
  24. package/dist/lib/lanes/accounts.js +24 -2
  25. package/dist/lib/lanes/delivery.d.ts +5 -2
  26. package/dist/lib/lanes/delivery.js +14 -5
  27. package/dist/lib/lanes/index.d.ts +1 -1
  28. package/dist/lib/lanes/index.js +1 -1
  29. package/dist/lib/lanes/reconcile.js +49 -5
  30. package/dist/lib/lanes/spawn.d.ts +7 -3
  31. package/dist/lib/lanes/spawn.js +97 -5
  32. package/dist/lib/lanes/store.js +54 -20
  33. package/dist/lib/lanes/story-ledger.d.ts +1 -1
  34. package/dist/lib/lanes/story-ledger.js +4 -4
  35. package/dist/lib/lanes/types.js +3 -3
  36. package/package.json +1 -1
@@ -63,7 +63,7 @@ import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, lan
63
63
  import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
64
64
  import { listLaneReviews } from "../lib/lanes/reviews.js";
65
65
  import { effectiveAccountHome } from "../lib/lanes/account-home.js";
66
- import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
66
+ import { AccountResolutionError, compareLaneQueueOrder, LANE_PROVIDERS, LANE_STATES, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, isFinishedResumableHireChild, childrenHoldingHireGrantCarve, laneHoldsHireGrantCarve, assignedChildHireGrants, hireGrantCapacityError, planHireGrantBackfill, terminalChildResumeHireGrantError, withHireGrantAdmission, laneStoryLedgerEnabled, syncLaneToProjectStoryLedger, removeLaneFromProjectStoryLedger, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listLaneIds, listQuestionIds, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseQuestionRecord, parseSeniorKind, readAllLanes, readLane, sanitizeHostComponent, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, registryWorkerRecord, runReconcilePass, DEAD_HOST_MS, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, dropboxDir, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, isFailedQuotaKeepAliveLane, sessionNotPortableMessage, withLaneAdmissionLock, withAllLaneAdmissionLocks, readWorkerRunResult, resumeCommandRefusal, withLaneHireLocks, raiseLaneQuestion, sha256Text, } from "../lib/lanes/index.js";
67
67
  import { laneStories, storyWarning, } from "../lib/lanes/stories.js";
68
68
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
69
69
  import { laneCompanyUidSource, resolveLaneCompany } from "../lib/lanes/lane-company.js";
@@ -6015,6 +6015,19 @@ async function runLaneDoctor(ctx, pendingQuestionsForLane) {
6015
6015
  const retiredEntries = loadRetiredLaneEntries(hqRoot);
6016
6016
  const findings = [];
6017
6017
  for (const lane of lanes) {
6018
+ if (lane.keep_alive === true &&
6019
+ lane.state === "failed" &&
6020
+ (lane.terminal_reason === "account_quota_exhausted" || lane.terminal_reason === "account_model_unavailable")) {
6021
+ findings.push({
6022
+ kind: "failed_keep_alive_lane",
6023
+ lane_id: lane.lane_id,
6024
+ senior: lane.senior,
6025
+ terminal_reason: lane.terminal_reason,
6026
+ ...(isFailedQuotaKeepAliveLane(lane)
6027
+ ? { suggested_command: `hq lanes resume ${lane.lane_id}` }
6028
+ : { suggested_action: "Inspect the quota reset details before authorizing a failed-lane resume." }),
6029
+ });
6030
+ }
6018
6031
  if (lane.keep_alive !== true || isTerminalLaneState(lane.state))
6019
6032
  continue;
6020
6033
  const retiredEntry = retiredEntries.find((entry) => matchesRetiredLaneEntry(entry, lane.lane_id));
@@ -7206,6 +7219,11 @@ async function runReparentOwnerTarget(ctx, laneId, opts) {
7206
7219
  changed_descendant_ids: result.changedDescendantIds,
7207
7220
  });
7208
7221
  }
7222
+ function assertLaneNotRunningForReparent(lane) {
7223
+ if (lane.state === "running") {
7224
+ throw new LanesError("lane_running", `Cannot re-parent running lane ${lane.lane_id}; wait until it stops first`, { lane_id: lane.lane_id, state: lane.state });
7225
+ }
7226
+ }
7209
7227
  function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false) {
7210
7228
  const hqRoot = ctx.hqRoot();
7211
7229
  if (!opts.senior) {
@@ -7215,9 +7233,6 @@ function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false
7215
7233
  assertSeniorExists(hqRoot, nextSenior, ctx);
7216
7234
  const lane = readLane(hqRoot, laneId);
7217
7235
  assertLaneRunsHereForOperatorMutation(ctx, lane, "reparent");
7218
- if (isTerminalLaneState(lane.state)) {
7219
- throw new LanesError("lane_terminal", `Cannot re-parent a lane in terminal state ${lane.state}`, { lane_id: laneId, state: lane.state });
7220
- }
7221
7236
  const seniorCompany = companyOfSenior(hqRoot, nextSenior, ctx);
7222
7237
  if (lane.company === null && seniorCompany.binding === "company") {
7223
7238
  const company = seniorCompany.slug ?? seniorCompany.uid ?? "<company>";
@@ -7257,6 +7272,7 @@ function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false
7257
7272
  chain.push({ lane_id: lane.lane_id, depth: newDepth });
7258
7273
  throw maxLaneDepthError(newDepth, chain, `Re-parent would put lane at depth ${newDepth}`, depthCap);
7259
7274
  }
7275
+ assertLaneNotRunningForReparent(lane);
7260
7276
  const pending = pendingQuestionsForLane(hqRoot, laneId);
7261
7277
  const oldDepth = derivedLaneDepth(hqRoot, lane.senior, ctx);
7262
7278
  if (newDepth > oldDepth && pending.length > 0) {
@@ -7295,6 +7311,7 @@ function runReparentLocked(ctx, laneId, opts, finishedChildrenDoNotCount = false
7295
7311
  const at = ctx.now().toISOString();
7296
7312
  const updated = casUpdateLane(hqRoot, laneId, (current) => {
7297
7313
  assertLaneRunsHereForOperatorMutation(ctx, current, "reparent");
7314
+ assertLaneNotRunningForReparent(current);
7298
7315
  const already = current.senior_history.some((entry) => entry.at === at && entry.reason === (opts.reason ?? "reparent"));
7299
7316
  if (already && current.depth === newDepth)
7300
7317
  return current;
@@ -10,6 +10,8 @@ import { listDeployApps } from "../lib/deploy-apps.js";
10
10
  import { listReportTemplates } from "../lib/report-templates.js";
11
11
  import { adoptReport, buildReportTarball, conventionReportDir, deployOrgContext, ReportPublishError, plainFailureReason, publishReport, } from "../lib/report-publish.js";
12
12
  const REPORT_CHANNEL_STATE_FILE = "report-channels.json";
13
+ const REPORT_UPLOAD_TIMEOUT_MS = 120_000;
14
+ const REPORT_CLEANUP_TIMEOUT_MS = 30_000;
13
15
  const MAX_REPORT_DISCOVERY_CHANNELS = 50;
14
16
  const JOB_EXECUTION_DISABLED_MESSAGE = "Scheduled jobs are turned off for this company. A company owner can turn them back on.";
15
17
  function reportPath(channelId, reportId) {
@@ -305,7 +307,7 @@ async function deployReportStatic(token, input) {
305
307
  throw new ReportPublishError("hq-deploy did not create an upload.");
306
308
  let upload;
307
309
  try {
308
- upload = await fetch(presignedUrl, { method: "PUT", headers: { "Content-Type": "application/gzip" }, body: tarball.bytes });
310
+ upload = await fetch(presignedUrl, { method: "PUT", headers: { "Content-Type": "application/gzip" }, body: tarball.bytes, signal: AbortSignal.timeout(REPORT_UPLOAD_TIMEOUT_MS) });
309
311
  }
310
312
  catch {
311
313
  throw new ReportPublishError("Report upload failed.");
@@ -365,6 +367,7 @@ async function discardDraftAction(reportId, options) {
365
367
  const res = await fetch(`${DEPLOY_API_URL.replace(/\/+$/, "")}/api/apps/${encodeURIComponent(report.draft.appId)}`, {
366
368
  method: "DELETE",
367
369
  headers: deployHeaders(token, false, org.headers),
370
+ signal: AbortSignal.timeout(REPORT_CLEANUP_TIMEOUT_MS),
368
371
  });
369
372
  if (!res.ok && res.status !== 404)
370
373
  throw new ReportPublishError("Could not remove the draft deployment.");
@@ -487,6 +490,7 @@ async function decommissionAction(reportId, options) {
487
490
  const res = await fetch(`${DEPLOY_API_URL.replace(/\/+$/, "")}/api/apps/${encodeURIComponent(id)}`, {
488
491
  method: "DELETE",
489
492
  headers: deployHeaders(token, false, org.headers),
493
+ signal: AbortSignal.timeout(REPORT_CLEANUP_TIMEOUT_MS),
490
494
  });
491
495
  if (res.status === 404)
492
496
  leftover.push("The published page was already gone.");
@@ -51,6 +51,12 @@ export interface RemoteAppSession {
51
51
  expiration?: string;
52
52
  };
53
53
  }
54
+ export interface RemoteDestroyAppResponse {
55
+ ok: boolean;
56
+ appId: string;
57
+ mode: "cluster" | "schema";
58
+ destroyedAt: string;
59
+ }
54
60
  export interface ControlPlaneClientOptions {
55
61
  baseUrl: string;
56
62
  /** Bearer access token */
@@ -73,5 +79,9 @@ export declare class ControlPlaneDbClient {
73
79
  companyUid: string;
74
80
  appId: string;
75
81
  }): Promise<RemoteAppSession>;
82
+ destroyApp(input: {
83
+ companyUid: string;
84
+ appId: string;
85
+ }): Promise<RemoteDestroyAppResponse>;
76
86
  }
77
87
  //# sourceMappingURL=control-plane.d.ts.map
@@ -109,6 +109,24 @@ export class ControlPlaneDbClient {
109
109
  throw httpError("session", res.status, text);
110
110
  return JSON.parse(text);
111
111
  }
112
+ async destroyApp(input) {
113
+ const token = await this.getAccessToken();
114
+ const url = `${this.baseUrl}/v1/db/apps/${encodeURIComponent(input.appId)}/destroy`;
115
+ const res = await this.fetchImpl(url, {
116
+ method: "POST",
117
+ headers: {
118
+ Authorization: `Bearer ${token}`,
119
+ "Content-Type": "application/json",
120
+ },
121
+ body: JSON.stringify({ companyUid: input.companyUid }),
122
+ signal: AbortSignal.timeout(DB_PROVISION_TIMEOUT_MS),
123
+ });
124
+ const text = await res.text();
125
+ assertNoPostgresUrl("destroy", text);
126
+ if (!res.ok)
127
+ throw httpError("destroy", res.status, text);
128
+ return JSON.parse(text);
129
+ }
112
130
  }
113
131
  function httpError(label, status, text) {
114
132
  let msg = `${label} failed (${status})`;
@@ -1,5 +1,6 @@
1
1
  /** Shared authenticated request path for the hq-deploy control plane. */
2
2
  export const DEPLOY_API_URL = process.env.HQ_DEPLOY_API_URL?.trim() || "https://api.indigo-hq.com";
3
+ const DEPLOY_API_REQUEST_TIMEOUT_MS = 30_000;
3
4
  export function deployHeaders(token, json = true, extra = {}) {
4
5
  return {
5
6
  Authorization: `Bearer ${token}`,
@@ -16,6 +17,7 @@ export async function deployApiRequest(token, route, method, body, extraHeaders
16
17
  method,
17
18
  headers: deployHeaders(token, body !== undefined, extraHeaders),
18
19
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
20
+ signal: AbortSignal.timeout(DEPLOY_API_REQUEST_TIMEOUT_MS),
19
21
  });
20
22
  }
21
23
  //# sourceMappingURL=deploy-api.js.map
@@ -127,6 +127,8 @@ export interface AppliedFixBackup {
127
127
  }
128
128
  /** A newly unlocked safe repair can be picked up for at most this many passes. */
129
129
  export declare const MAX_FIX_PASSES = 3;
130
+ /** Two 90s pulls leave 120s for startup and check overhead under the hook's 300s ceiling. */
131
+ export declare const MAX_REFRESH_SYNC_PLANS_PER_RUN = 2;
130
132
  /** The outcome of an {@link applyFixes} run. */
131
133
  export interface ApplyFixesResult {
132
134
  /** 0 on success, decline, or a clean no-op; 1 when a dirty tree is refused. */
@@ -47,6 +47,43 @@ export const HOOK_CONFIG_DIRS = [".claude", ".codex", ".grok"];
47
47
  export const REFRESH_SYNC_TIMEOUT_MS = 90_000;
48
48
  /** A newly unlocked safe repair can be picked up for at most this many passes. */
49
49
  export const MAX_FIX_PASSES = 3;
50
+ /** Two 90s pulls leave 120s for startup and check overhead under the hook's 300s ceiling. */
51
+ export const MAX_REFRESH_SYNC_PLANS_PER_RUN = 2;
52
+ const REFRESH_SYNC_PROGRESS_RELATIVE_PATH = path.join("workspace", "doctor", "refresh-sync-progress.json");
53
+ function readRefreshSyncProgress(hqRoot) {
54
+ let contents;
55
+ try {
56
+ contents = fs.readFileSync(path.join(hqRoot, REFRESH_SYNC_PROGRESS_RELATIVE_PATH), "utf8");
57
+ }
58
+ catch (error) {
59
+ if (error.code === "ENOENT")
60
+ return null;
61
+ throw error;
62
+ }
63
+ const parsed = JSON.parse(contents);
64
+ if (typeof parsed === "object" && parsed !== null &&
65
+ parsed.version === 1 &&
66
+ typeof parsed.lastAttemptedSlug === "string") {
67
+ return parsed;
68
+ }
69
+ throw new Error(`Invalid refresh-sync progress cursor at ${REFRESH_SYNC_PROGRESS_RELATIVE_PATH}`);
70
+ }
71
+ function writeRefreshSyncProgress(hqRoot, slug) {
72
+ const progressPath = path.join(hqRoot, REFRESH_SYNC_PROGRESS_RELATIVE_PATH);
73
+ const directory = path.dirname(progressPath);
74
+ fs.mkdirSync(directory, { recursive: true });
75
+ const temporaryPath = `${progressPath}.${process.pid}.tmp`;
76
+ fs.writeFileSync(temporaryPath, `${JSON.stringify({ version: 1, lastAttemptedSlug: slug })}\n`, { mode: 0o600 });
77
+ fs.renameSync(temporaryPath, progressPath);
78
+ }
79
+ function rotateRefreshSyncPlans(plans, afterSlug) {
80
+ const ordered = [...plans].sort((a, b) => a.target < b.target ? -1 : a.target > b.target ? 1 : 0);
81
+ if (!afterSlug || ordered.length < 2)
82
+ return ordered;
83
+ const next = ordered.findIndex((plan) => plan.target > afterSlug);
84
+ const start = next === -1 ? 0 : next;
85
+ return [...ordered.slice(start), ...ordered.slice(0, start)];
86
+ }
50
87
  /**
51
88
  * Apply every auto-fixable finding, honouring the dirty-tree refusal, the
52
89
  * preview/confirmation gate, the pre-write backup, and the post-fix re-check.
@@ -87,6 +124,8 @@ export async function applyFixes(options) {
87
124
  // Identity is deliberately the check id + target pair: an unsuccessful
88
125
  // repair must not be applied repeatedly in one invocation.
89
126
  let findings = await collect(hqRoot);
127
+ let lastAttemptedRefreshSyncSlug = readRefreshSyncProgress(hqRoot)?.lastAttemptedSlug ?? null;
128
+ let refreshSyncAppliedThisRun = 0;
90
129
  const appliedKeys = new Set();
91
130
  const planned = [];
92
131
  const backups = [];
@@ -114,7 +153,11 @@ export async function applyFixes(options) {
114
153
  keysThisPass.add(key);
115
154
  plans.push(plan);
116
155
  }
117
- return plans;
156
+ const refreshPlans = rotateRefreshSyncPlans(plans.filter((plan) => plan.fixClass === "refresh-sync"), lastAttemptedRefreshSyncSlug).slice(0, Math.max(0, MAX_REFRESH_SYNC_PLANS_PER_RUN - refreshSyncAppliedThisRun));
157
+ return [
158
+ ...plans.filter((plan) => plan.fixClass !== "refresh-sync"),
159
+ ...refreshPlans,
160
+ ];
118
161
  };
119
162
  for (let pass = 1; pass <= MAX_FIX_PASSES; pass += 1) {
120
163
  const plans = buildPlans(findings);
@@ -154,6 +197,14 @@ export async function applyFixes(options) {
154
197
  // 5. Apply each plan once. Later findings with this check id + target are
155
198
  // excluded even if this repair did not make its check pass.
156
199
  for (const plan of plans) {
200
+ if (plan.fixClass === "refresh-sync") {
201
+ // Persist before starting the bounded pull. If this process is killed,
202
+ // the next invocation resumes after this company instead of starving
203
+ // every later journal behind it.
204
+ writeRefreshSyncProgress(hqRoot, plan.target);
205
+ lastAttemptedRefreshSyncSlug = plan.target;
206
+ refreshSyncAppliedThisRun += 1;
207
+ }
157
208
  appliedKeys.add(JSON.stringify([plan.checkId, plan.target]));
158
209
  planned.push({ plan, pass });
159
210
  await plan.apply();
@@ -63,7 +63,7 @@ export declare function resolveFlagGate(reader: FlagReader | null | undefined, f
63
63
  /** Resolve one explicit company's flag without relying on process-wide binding. */
64
64
  export declare function resolveCompanyFlagGate(companyUid: string, idToken: string, flagKey: string, lookup: FlagLookupOptions, legacyFallback: () => boolean, dependencies?: CompanyFlagGateDependencies): Promise<boolean>;
65
65
  /** Resolve a process gate against the shared snapshot reader, when configured. */
66
- export declare function resolveProcessFlagGate(flagKey: string, lookup: FlagLookupOptions, legacyFallback: () => boolean): boolean;
66
+ export declare function resolveProcessFlagGate(flagKey: string, lookup: FlagLookupOptions, legacyFallback: () => boolean, companyUid?: string): boolean;
67
67
  /**
68
68
  * The registry snapshot held by this process, or null when none has arrived
69
69
  * (no endpoint configured, offline, or still fetching). Callers that must
@@ -11,7 +11,7 @@ import { resolveFlagRegistryEndpoint, } from "./flag-registry-endpoint.js";
11
11
  import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.generated.js";
12
12
  import { resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
13
13
  import { registerFlagRegistryRefreshWaiter } from "./flag-registry-refresh-wait.js";
14
- import { flagSnapshotWriteFailureTags, writeFlagSnapshotFile } from "./flag-snapshot-file.js";
14
+ import { flagSnapshotWriteFailureTags, readFlagSnapshotFile, writeFlagSnapshotFile, } from "./flag-snapshot-file.js";
15
15
  function captureFlagRegistryException(error, tags, sentry) {
16
16
  if (!sentry)
17
17
  return;
@@ -298,13 +298,26 @@ export async function resolveCompanyFlagGate(companyUid, idToken, flagKey, looku
298
298
  }
299
299
  }
300
300
  /** Resolve a process gate against the shared snapshot reader, when configured. */
301
- export function resolveProcessFlagGate(flagKey, lookup, legacyFallback) {
302
- // The endpoint refresh is deliberately asynchronous. Until a validated
303
- // snapshot arrives, preserve each caller's exact legacy fallback.
304
- if (!processReader || !heldSnapshot) {
305
- return localOverrideOrFallback(flagKey, legacyFallback);
301
+ export function resolveProcessFlagGate(flagKey, lookup, legacyFallback, companyUid) {
302
+ const targetCompanyUid = companyUid ?? (processReader
303
+ ? refreshCompanyUid
304
+ : resolveProcessFlagCompanyContext().companyUid);
305
+ const processSnapshotMatchesTarget = refreshCompanyUid === targetCompanyUid;
306
+ if (processReader && heldSnapshot && processSnapshotMatchesTarget) {
307
+ return resolveFlagGate(processReader, flagKey, lookup, legacyFallback);
308
+ }
309
+ // A command can reach a synchronous gate before the startup worker returns.
310
+ // Use the last validated, fresh company snapshot in that window, and when
311
+ // the process reader was initialized for a different company scope.
312
+ if (targetCompanyUid) {
313
+ const cached = readFlagSnapshotFile(targetCompanyUid);
314
+ if (cached) {
315
+ const snapshot = { version: 0, flags: cached.flags };
316
+ const reader = createProcessSnapshotReader([targetCompanyUid], () => snapshot);
317
+ return resolveFlagGate(reader, flagKey, lookup, legacyFallback);
318
+ }
306
319
  }
307
- return resolveFlagGate(processReader, flagKey, lookup, legacyFallback);
320
+ return localOverrideOrFallback(flagKey, legacyFallback);
308
321
  }
309
322
  /**
310
323
  * The registry snapshot held by this process, or null when none has arrived
@@ -233,9 +233,31 @@ export function selectBalancedAccount(provider, config, ctx = {}) {
233
233
  })
234
234
  : proven;
235
235
  if (quota && runnable.length === 0) {
236
+ const blocked = proven.map((id) => ({
237
+ id,
238
+ block: quotaBlockFor(quota, provider, id, modelForAccount(provider, id, config, ctx), now),
239
+ }));
240
+ const resetTimes = blocked
241
+ .map(({ block }) => block.quota_reset_at)
242
+ .filter((value) => typeof value === "string" && Number.isFinite(Date.parse(value)))
243
+ .sort((a, b) => Date.parse(a) - Date.parse(b));
244
+ const earliest = blocked.find(({ block }) => block.reason === "account_quota_exhausted");
245
+ if (earliest !== undefined) {
246
+ const resetsByAccount = blocked.map(({ id, block }) => ({
247
+ account: id,
248
+ reason: block.reason,
249
+ quota_reset_at: block.quota_reset_at ?? null,
250
+ }));
251
+ throw new AccountResolutionError("account_quota_exhausted", `All logged-in accounts for ${provider} are quota-limited for the selected model`, {
252
+ provider,
253
+ account: earliest.id,
254
+ model: earliest.block.model ?? null,
255
+ quota_reset_at: resetTimes[0] ?? null,
256
+ account_reasons: resetsByAccount,
257
+ });
258
+ }
236
259
  const sample = proven[0];
237
- const block = quotaBlockFor(quota, provider, sample, modelForAccount(provider, sample, config, ctx), now);
238
- throwIfQuotaBlocked(block, provider, sample);
260
+ throwIfQuotaBlocked(blocked[0].block, provider, sample);
239
261
  }
240
262
  const underCap = runnable.filter((id) => {
241
263
  const cap = accountMaxRunning(providerCfg, id);
@@ -68,14 +68,17 @@ export declare const LANE_DELIVERY_MECHANISMS: readonly ["dropbox", "resume", "n
68
68
  export type LaneDeliveryMechanism = (typeof LANE_DELIVERY_MECHANISMS)[number];
69
69
  /** Additional presentation-only result used by `hq lanes show`. */
70
70
  export type LaneShowDeliveryMechanism = LaneDeliveryMechanism | "questions";
71
- export type DeliveryLane = Pick<LaneRecord, "state" | "provider" | "provider_session_id" | "terminal_reason" | "envelope_repair">;
71
+ export type DeliveryLane = Pick<LaneRecord, "state" | "provider" | "provider_session_id" | "terminal_reason" | "envelope_repair" | "keep_alive" | "quota_reset_at" | "terminal_details">;
72
+ export declare function isFailedQuotaKeepAliveLane(lane: DeliveryLane): boolean;
72
73
  /**
73
74
  * `dropbox`: a process is (or is about to be) alive to drain the inbox.
74
75
  * `resume`: the process has exited, the provider can resume, the state allows
75
76
  * it, and a session id was recorded from the worker's own result.
76
77
  * `none`: nothing can deliver; the caller must refuse and write nothing.
77
78
  */
78
- export declare function laneDeliveryMechanism(lane: DeliveryLane): LaneDeliveryMechanism;
79
+ export declare function laneDeliveryMechanism(lane: DeliveryLane, options?: {
80
+ forMessage?: boolean;
81
+ }): LaneDeliveryMechanism;
79
82
  export declare function hasRecordedSessionId(lane: Pick<LaneRecord, "provider_session_id">): lane is Pick<LaneRecord, "provider_session_id"> & {
80
83
  provider_session_id: string;
81
84
  };
@@ -94,17 +94,25 @@ function isFailedEnvelopeRepairLane(lane) {
94
94
  lane.envelope_repair?.attempted === true &&
95
95
  lane.envelope_repair.succeeded === false);
96
96
  }
97
+ export function isFailedQuotaKeepAliveLane(lane) {
98
+ const detailsReset = lane.terminal_details?.quota_reset_at;
99
+ const resetAt = lane.quota_reset_at ?? (typeof detailsReset === "string" ? detailsReset : undefined);
100
+ return lane.state === "failed" && lane.keep_alive === true &&
101
+ (lane.terminal_reason === "account_quota_exhausted" || lane.terminal_reason === "account_model_unavailable") &&
102
+ typeof resetAt === "string" && Number.isFinite(Date.parse(resetAt));
103
+ }
97
104
  /**
98
105
  * `dropbox`: a process is (or is about to be) alive to drain the inbox.
99
106
  * `resume`: the process has exited, the provider can resume, the state allows
100
107
  * it, and a session id was recorded from the worker's own result.
101
108
  * `none`: nothing can deliver; the caller must refuse and write nothing.
102
109
  */
103
- export function laneDeliveryMechanism(lane) {
110
+ export function laneDeliveryMechanism(lane, options = {}) {
104
111
  if (lane.state === "queued" || lane.state === "running")
105
112
  return "dropbox";
106
113
  if (providerSupportsSessionResume(lane.provider) &&
107
- (isResumableLaneState(lane.state) || lane.state === "awaiting_input" || isFailedEnvelopeRepairLane(lane)) &&
114
+ (isResumableLaneState(lane.state) || lane.state === "awaiting_input" || isFailedEnvelopeRepairLane(lane) ||
115
+ (!options.forMessage && isFailedQuotaKeepAliveLane(lane))) &&
108
116
  hasRecordedSessionId(lane)) {
109
117
  return "resume";
110
118
  }
@@ -350,6 +358,7 @@ export function resumeCommandRefusal(lane, options = {}) {
350
358
  if (!isResumeCommandState(lane.state) &&
351
359
  !(options.allowAwaitingInput === true && lane.state === "awaiting_input") &&
352
360
  !isFailedEnvelopeRepairLane(lane) &&
361
+ !isFailedQuotaKeepAliveLane(lane) &&
353
362
  !(options.allowFailed === true && lane.state === "failed")) {
354
363
  return {
355
364
  error: "lane_not_resumable",
@@ -393,14 +402,14 @@ export function planLaneLaunch(lane, trigger) {
393
402
  }
394
403
  return { mode: "fresh" };
395
404
  case "message": {
396
- if (laneDeliveryMechanism(lane) !== "resume" || !hasRecordedSessionId(lane)) {
405
+ if (laneDeliveryMechanism(lane, { forMessage: true }) !== "resume" || !hasRecordedSessionId(lane)) {
397
406
  throw new LanesError("lane_not_accepting_messages", deliveryRefusalReason(lane) || `Lane is ${lane.state} and cannot be resumed`, { state: lane.state, provider: lane.provider });
398
407
  }
399
408
  return { mode: "resume", session_id: lane.provider_session_id };
400
409
  }
401
410
  case "resume": {
402
411
  const refusal = resumeCommandRefusal(lane, {
403
- allowFailed: trigger.fromFailed === true,
412
+ allowFailed: trigger.fromFailed === true || isFailedQuotaKeepAliveLane(lane),
404
413
  allowAwaitingInput: trigger.allowAwaitingInput === true,
405
414
  });
406
415
  if (refusal !== null || !hasRecordedSessionId(lane)) {
@@ -429,7 +438,7 @@ export function claimableStatesFor(trigger, lane) {
429
438
  ? [...RESUMABLE_LANE_STATES, "awaiting_input"]
430
439
  : RESUMABLE_LANE_STATES;
431
440
  case "resume": {
432
- const states = trigger.fromFailed === true || envelopeRepairFailed
441
+ const states = trigger.fromFailed === true || envelopeRepairFailed || (lane !== undefined && isFailedQuotaKeepAliveLane(lane))
433
442
  ? [...RESUME_COMMAND_STATES, "failed"]
434
443
  : [...RESUME_COMMAND_STATES];
435
444
  return lane?.state === "awaiting_input" ? [...states, "awaiting_input"] : states;
@@ -63,7 +63,7 @@ export type { RegistryPersona } from "./registry-persona.js";
63
63
  export type { RegistryWorkerEntry, ResolvedLaneWorker, WorkerTenancy, WorkerYaml, } from "./registry.js";
64
64
  export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
65
65
  export type { QuestionBody, RaiseLaneQuestionInput } from "./questions.js";
66
- export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
66
+ export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isFailedQuotaKeepAliveLane, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
67
67
  export type { DeliveryLane, LaneDeliveryMechanism, LaneShowDeliveryMechanism, LaunchPlan, LaunchTrigger, LaunchTriggerKind, ResumableLaneState, ResumeCommandState, SessionIdAbsentReason, WorkerRunResult, } from "./delivery.js";
68
68
  export { DROPBOX_DIR, INBOX_NAME, appendDropboxMessage, answerDropboxMessage, drainDropbox, dropboxDir, dropboxInboxPath, formatOperatorInstruction, hookAdditionalContextJson, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneIdForAnswerDelivery, operatorMessageRefusalReason, readToolActivity, recordToolActivity, runDrainHook, toolActivityPath, TOOL_ACTIVITY_NAME, } from "./dropbox.js";
69
69
  export type { DrainDeps, DrainHookResult, DropboxMessage, ToolActivityRecord, } from "./dropbox.js";
@@ -34,7 +34,7 @@ export { WORKER_REGISTRY_REL, personalWorkerDir, personalWorkerExists, readWorke
34
34
  export { ignoredWorkerProfileFields, loadWorkerProfile, loadWorkerProfileWithRegistry, materializeWorkerProfile, personalWorkerProfileDir, REGISTRY_PERSONA_PROMPT_MAX_CHARS, workerProfileMarker, workerProfilePrompt, } from "./profile.js";
35
35
  export { loadRegistryPersonaFromRecord, registryWorkerRecord, resolveRegistryPersona, } from "./registry-persona.js";
36
36
  export { bodyStoreForLane, envelopeQuestionId, parseQuestionBodyStore, questionBodyAbsPath, questionBodyRef, raiseEnvelopeQuestions, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "./questions.js";
37
- export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
37
+ export { LANE_DELIVERY_MECHANISMS, LAUNCH_TRIGGERS, PROVIDER_SESSION_RESUME, PROVIDER_SESSION_PORTABLE, providerSessionPortable, sessionNotPortableMessage, parseGrokResultOutput, RESUMABLE_LANE_STATES, RESUME_COMMAND_STATES, SESSION_ID_ABSENT_REASONS, claimableStatesFor, deliveryRefusalReason, hasRecordedSessionId, isFailedQuotaKeepAliveLane, isResumableLaneState, isResumeCommandState, laneDeliveryMechanism, parseClaudeResultOutput, parseCodexEventOutput, parseWorkerRunOutput, planLaneLaunch, providerSupportsSessionResume, readWorkerRunResult, resumeCommandRefusal, } from "./delivery.js";
38
38
  export { DROPBOX_DIR, INBOX_NAME, appendDropboxMessage, answerDropboxMessage, drainDropbox, dropboxDir, dropboxInboxPath, formatOperatorInstruction, hookAdditionalContextJson, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneIdForAnswerDelivery, operatorMessageRefusalReason, readToolActivity, recordToolActivity, runDrainHook, toolActivityPath, TOOL_ACTIVITY_NAME, } from "./dropbox.js";
39
39
  export { DRAIN_HOOK_BASENAME, DRAIN_HOOK_REGISTRATION_KINDS, DRAIN_HOOK_TIMEOUT_SECONDS, PROVIDER_REGISTRATION_KIND, drainHookCommand, drainHookRegistrationPath, drainHookShPath, installDrainHook, lanesHookAssetsDir, mergeClaudeSettingsHook, mergeCodexConfigHook, probeDrainHookRegistration, renderDrainHookJson, } from "./hooks-install.js";
40
40
  export { EFFORT_SOURCES, PROVIDER_EFFORT_LEVELS, appendEffortFlag, defaultEffortForModel, parseCliEffort, parseConfigEffort, resolveLaneEffort, } from "./effort.js";
@@ -29,7 +29,7 @@ import { laneCompanyUidSource, resolveLaneCompany } from "./lane-company.js";
29
29
  import { appendDropboxMessage } from "./dropbox.js";
30
30
  import { DEFAULT_RUNAWAY_SEARCH_GUARD_CONFIG, listHostProcesses, runRunawayToolGuard, RUNAWAY_TOOL_GUARD_FLAG, } from "./runaway-tool-guard.js";
31
31
  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, dueKeepAliveNextCheckMs, hasPendingKeepAliveWork, keepAliveNextCheckAtMs, keepAliveExclusion, loadRetiredLaneIds, leadLaneKeepAliveBackoffMs, undeliveredAnsweredQuestions, } from "./keep-alive.js";
32
- import { providerSupportsSessionResume } from "./delivery.js";
32
+ import { isFailedQuotaKeepAliveLane, providerSupportsSessionResume } from "./delivery.js";
33
33
  import { isIsoDateTime } from "./types.js";
34
34
  import { applyDefaultQuotaHold, clearAccountQuotaAfterSuccessfulTurnBestEffort, loadQuotaState, readQuotaSignal, recordAccountQuota, } from "./quota.js";
35
35
  import { answeredQuestionEntries, questionRecordRefusalCode, raiseEnvelopeQuestions, resolveQuestionCompanyWithFallback, } from "./questions.js";
@@ -2586,7 +2586,7 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2586
2586
  dueAdmissionRan = true;
2587
2587
  await admitDueCandidates(new Set(queuedDueCandidateIds));
2588
2588
  };
2589
- for (const originalLane of candidates) {
2589
+ for (let originalLane of candidates) {
2590
2590
  if (admitDueCandidates !== undefined && dueCandidateIds.size > 0 && !dueCandidateIds.has(originalLane.lane_id)) {
2591
2591
  await admitQueuedDueCandidates();
2592
2592
  }
@@ -2596,14 +2596,50 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2596
2596
  continue;
2597
2597
  if (!laneRunsHere(originalLane, deps.host))
2598
2598
  continue;
2599
+ const failedQuotaHold = isFailedQuotaKeepAliveLane(originalLane);
2599
2600
  if (originalLane.state === "killed" ||
2600
2601
  originalLane.state === "cancelled" ||
2601
- originalLane.state === "failed") {
2602
+ (originalLane.state === "failed" && !failedQuotaHold)) {
2602
2603
  if (originalLane.keep_alive === true &&
2603
2604
  flagByCompany.get(keepAliveCompanyKey(companyForLane(originalLane))) === true)
2604
2605
  recordKeepAliveSkip(hqRoot, originalLane, "terminal_state", summary, deps);
2605
2606
  continue;
2606
2607
  }
2608
+ if (failedQuotaHold) {
2609
+ const detailsReset = originalLane.terminal_details?.quota_reset_at;
2610
+ const resetAt = originalLane.quota_reset_at ??
2611
+ (typeof detailsReset === "string" ? detailsReset : undefined);
2612
+ const companyKey = keepAliveCompanyKey(companyForLane(originalLane));
2613
+ if (resetAt === undefined ||
2614
+ Date.parse(resetAt) > nowMs ||
2615
+ flagByCompany.get(companyKey) !== true)
2616
+ continue;
2617
+ const resetAtMs = Date.parse(resetAt);
2618
+ const priorInterruptedAtMs = Date.parse(originalLane.interrupted_at ?? "");
2619
+ const optedInAtMs = Date.parse(originalLane.keep_alive_opted_in_at ?? "");
2620
+ const recoveredInterruptedAt = new Date(Math.max(Number.isFinite(priorInterruptedAtMs) ? priorInterruptedAtMs : resetAtMs, Number.isFinite(optedInAtMs) ? optedInAtMs + 1 : resetAtMs)).toISOString();
2621
+ const recovered = casLaneIf(hqRoot, originalLane.lane_id, (current) => current.version === originalLane.version &&
2622
+ current.state === "failed" &&
2623
+ current.keep_alive === true &&
2624
+ current.terminal_reason === originalLane.terminal_reason &&
2625
+ (current.quota_reset_at ?? current.terminal_details?.quota_reset_at) === resetAt, (current) => ({
2626
+ ...current,
2627
+ state: "interrupted",
2628
+ interrupted_at: recoveredInterruptedAt,
2629
+ quota_reset_at: resetAt,
2630
+ keep_alive_next_eligible_at: resetAt,
2631
+ keep_alive_resume_count: 0,
2632
+ keep_alive_last_resume_at: undefined,
2633
+ keep_alive_exhausted_at: undefined,
2634
+ queued_reason: undefined,
2635
+ queued_details: undefined,
2636
+ queued_at: undefined,
2637
+ admission_reserved_at: undefined,
2638
+ }), deps.dryRun);
2639
+ if (!recovered.changed)
2640
+ continue;
2641
+ originalLane = recovered.record;
2642
+ }
2607
2643
  if (originalLane.state === "blocked" && originalLane.keep_alive === true) {
2608
2644
  await resumeBlockedKeepAlive(hqRoot, originalLane, questions, summary, deps, now, flagByCompany, blockedResumeFlagByCompany, companyForLane, lanes, retiredLaneIds, supersessionLineageAwareByCompany);
2609
2645
  continue;
@@ -2714,7 +2750,11 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2714
2750
  continue;
2715
2751
  }
2716
2752
  }
2717
- if (initialState === "interrupted" && lane.terminal_reason !== "exited_with_partial_output") {
2753
+ const quotaHold = lane.keep_alive === true &&
2754
+ (lane.terminal_reason === "account_quota_exhausted" || lane.terminal_reason === "account_model_unavailable") &&
2755
+ typeof lane.quota_reset_at === "string" &&
2756
+ Number.isFinite(Date.parse(lane.quota_reset_at));
2757
+ if (initialState === "interrupted" && lane.terminal_reason !== "exited_with_partial_output" && !quotaHold) {
2718
2758
  recordKeepAliveSkip(hqRoot, lane, "terminal_state", summary, deps, lane.terminal_reason ?? "missing interruption reason");
2719
2759
  continue;
2720
2760
  }
@@ -2816,7 +2856,11 @@ async function resumeKeepAlive(hqRoot, lanes, questions, summary, deps, now, fla
2816
2856
  recordKeepAliveSkip(hqRoot, current, "not_opted_in", summary, deps);
2817
2857
  continue;
2818
2858
  }
2819
- if (initialState === "interrupted" && current.terminal_reason !== "exited_with_partial_output") {
2859
+ const currentQuotaHold = current.keep_alive === true &&
2860
+ (current.terminal_reason === "account_quota_exhausted" || current.terminal_reason === "account_model_unavailable") &&
2861
+ typeof current.quota_reset_at === "string" &&
2862
+ Number.isFinite(Date.parse(current.quota_reset_at));
2863
+ if (initialState === "interrupted" && current.terminal_reason !== "exited_with_partial_output" && !currentQuotaHold) {
2820
2864
  recordKeepAliveSkip(hqRoot, current, "terminal_state", summary, deps, current.terminal_reason ?? "missing interruption reason");
2821
2865
  continue;
2822
2866
  }
@@ -15,11 +15,11 @@ import { type LaunchTrigger } from "./delivery.js";
15
15
  import { type MachineIdentity } from "./machine-identity.js";
16
16
  import { type AdmissionHost } from "./admission.js";
17
17
  import { type StatFs } from "./disk-guard.js";
18
- import { type LaneCompanyRef, type LaneProvider, type LaneRecord, type AccountHistoryEntry } from "./types.js";
18
+ import { type LaneCompanyRef, type LaneProvider, type LaneRecord, type AccountHistoryEntry, type PendingResume } from "./types.js";
19
19
  export declare const LANES_RUNS_REL: string;
20
20
  /** Written by reconcile before a resume spawn; the worker prompt names this path. */
21
21
  export declare const ANSWERED_QUESTIONS_FILE = "answered-questions.json";
22
- export declare const SPAWN_TERMINAL_REASONS: readonly ["account_not_logged_in", "unknown_account", "account_disabled", "no_enabled_account", "spawn_failed", "not_detached", "watcher_not_armed", "model_unsupported", "account_quota_exhausted"];
22
+ export declare const SPAWN_TERMINAL_REASONS: readonly ["account_not_logged_in", "unknown_account", "account_disabled", "no_enabled_account", "spawn_failed", "not_detached", "watcher_not_armed", "model_unsupported", "account_quota_exhausted", "account_model_unavailable"];
23
23
  export type SpawnTerminalReason = (typeof SPAWN_TERMINAL_REASONS)[number];
24
24
  declare const PROVIDER_COMMAND: Record<LaneProvider, string>;
25
25
  export declare function providerCommandOnPath(command: string, pathEnv: string | undefined): boolean;
@@ -244,7 +244,11 @@ export declare function workerEnv(base: NodeJS.ProcessEnv, resolved: {
244
244
  export declare const WORKER_POLICY_HOOK_CAPABILITY = "# hq-capability: worker-policy-dir v1";
245
245
  /** Grok does not run HQ hooks; Claude and Codex use the installed capability marker. */
246
246
  export declare function workerPolicyDeliveryMode(hqRoot: string, provider: LaneProvider): "hook" | "prompt";
247
- export declare function markSpawnFailure(hqRoot: string, laneId: string, reasonRaw: string, details: Record<string, unknown>): LaneRecord;
247
+ export declare function markSpawnFailure(hqRoot: string, laneId: string, reasonRaw: string, details: Record<string, unknown>, options?: {
248
+ resumeTrigger?: "resume" | "message";
249
+ pendingResume?: PendingResume;
250
+ at?: string;
251
+ }): LaneRecord;
248
252
  /**
249
253
  * Spec §5.2 executing-side check. Compares this machine's identity to
250
254
  * `origin_host` (never to `host.id`). Unresolvable identity is a cancel.