@indigoai-us/hq-cli 5.154.0 → 5.155.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.
@@ -34,8 +34,9 @@
34
34
  import { type LaneEnvelope, type LaneRecord, type LaneState } from "./types.js";
35
35
  import { type QuotaSignal } from "./quota.js";
36
36
  import { type ProviderRejection } from "./provider-rejection.js";
37
+ import type { AdmissionHost } from "./admission.js";
37
38
  import { type LaneMesh } from "./mesh-events.js";
38
- import { type EnvelopeRepairSpawnInput } from "./spawn.js";
39
+ import { type EnvelopeRepairSpawnInput, type SpawnLaneDeps } from "./spawn.js";
39
40
  /** Spec §5.7: no signal for 20 minutes while the process is still alive. */
40
41
  export declare const SILENCE_MS: number;
41
42
  export declare const WATCHER_POLL_MS = 5000;
@@ -206,6 +207,12 @@ export interface WatcherLoopDeps {
206
207
  meshRequestTimeoutMs?: number;
207
208
  /** Spec §4 repair spawn. Default: `spawnEnvelopeRepair`. */
208
209
  spawnRepair?: (hqRoot: string, lane: LaneRecord, input: EnvelopeRepairSpawnInput) => number;
210
+ /** Rebind and respawn after a provider quota or authentication refusal. */
211
+ retryAccount?: (hqRoot: string, lane: LaneRecord, reason: "usage_limit" | "account_unavailable") => LaneRecord | Promise<LaneRecord>;
212
+ /** Shared process/Outpost identity used by command admission and watcher retries. */
213
+ resolveAdmissionHost?: () => AdmissionHost;
214
+ /** Spawn seams for account retry; host scope is supplied by the resolver above. */
215
+ retrySpawnDeps?: Omit<SpawnLaneDeps, "admissionHost">;
209
216
  }
210
217
  /**
211
218
  * Wrap one mesh call. A failed emit is logged as `mesh_event_failed` with
@@ -40,12 +40,13 @@ import { laneStories, storyStatusForEnvelope } from "./stories.js";
40
40
  import { readLane, retryUpdateLane } from "./store.js";
41
41
  import { readWorkerRunResult } from "./delivery.js";
42
42
  import { applyDefaultQuotaHold, quotaSignalFromFields, readQuotaSignal, recordAccountQuota, } from "./quota.js";
43
- import { PROVIDER_REJECTED_REASON, QUOTA_STATUS, providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
43
+ import { PROVIDER_REJECTED_REASON, QUOTA_STATUS, isProviderLoginFailure, providerRejectionDetails, readProviderRejection, } from "./provider-rejection.js";
44
44
  import { readToolActivity, toolActivityPath } from "./dropbox.js";
45
45
  import { loadLanesConfig, providerProgressEventMinIntervalMs, providerQuotaDefaultHoldMs } from "./config.js";
46
+ import { resolveAdmissionHost as resolveProcessAdmissionHost } from "./admission-host.js";
46
47
  import { createLaneMesh, emitLaneDecisionEvents, emitLaneProgressEvent, progressEventDue, } from "./mesh-events.js";
47
48
  import { envelopeQuestionId } from "./questions.js";
48
- import { laneRunDir, missingPidProbe, probePidRecord, readPidRecord, readProcStarttime, readSpawnManifest, envelopeRepairMode, spawnEnvelopeRepair, } from "./spawn.js";
49
+ import { laneRunDir, missingPidProbe, probePidRecord, readPidRecord, readProcStarttime, readSpawnManifest, envelopeRepairMode, spawnEnvelopeRepair, retryLaneOnAlternateAccount, } from "./spawn.js";
49
50
  /** Spec §5.7: no signal for 20 minutes while the process is still alive. */
50
51
  export const SILENCE_MS = 20 * 60 * 1000;
51
52
  export const WATCHER_POLL_MS = 5_000;
@@ -89,7 +90,12 @@ export function classifyLaneExit(input) {
89
90
  throw new LanesError("unknown_envelope_decision", `Unknown envelope decision ${JSON.stringify(input.envelope.decision)}. Accepted: done, ask, blocked`, { accepted: ["done", "ask", "blocked"], value: input.envelope.decision });
90
91
  }
91
92
  if (input.providerRejection) {
92
- return { state: "failed", reason: PROVIDER_REJECTED_REASON };
93
+ return {
94
+ state: "failed",
95
+ reason: isProviderLoginFailure(input.providerRejection)
96
+ ? "account_not_logged_in"
97
+ : PROVIDER_REJECTED_REASON,
98
+ };
93
99
  }
94
100
  if (input.invalidEnvelope) {
95
101
  return { state: "failed", reason: ENVELOPE_INVALID_REASON };
@@ -613,6 +619,10 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
613
619
  }
614
620
  const progressIntervalMs = deps.progressEventMinIntervalMs ?? readProgressIntervalMs(hqRoot, readLaneSoft(hqRoot, laneId));
615
621
  const spawnRepair = deps.spawnRepair ?? spawnEnvelopeRepair;
622
+ const retryAccount = deps.retryAccount ?? ((root, lane, reason) => retryLaneOnAlternateAccount(root, lane, reason, "watcher", {
623
+ ...deps.retrySpawnDeps,
624
+ admissionHost: (deps.resolveAdmissionHost ?? resolveProcessAdmissionHost)(),
625
+ }));
616
626
  let lastSignalMs = now();
617
627
  let lastSeenMtime = 0;
618
628
  let lastAlive = null;
@@ -836,6 +846,32 @@ export async function runWatcherLoop(hqRoot, laneId, deps = {}) {
836
846
  process.stderr.write(`hq lanes watcher: quota record failed for ${laneId}: ${err instanceof Error ? err.message : String(err)}\n`);
837
847
  }
838
848
  }
849
+ const accountRetryReason = quota
850
+ ? "usage_limit"
851
+ : isProviderLoginFailure(providerRejection)
852
+ ? "account_unavailable"
853
+ : null;
854
+ if (accountRetryReason && quotaLane?.account) {
855
+ try {
856
+ const retry = await retryAccount(hqRoot, quotaLane, accountRetryReason);
857
+ logDecision({
858
+ event: "account_failover",
859
+ at: tick.emit.at,
860
+ from: quotaLane.account,
861
+ to: retry.account ?? null,
862
+ reason: accountRetryReason,
863
+ state: retry.state,
864
+ });
865
+ return;
866
+ }
867
+ catch (err) {
868
+ const message = err instanceof Error ? err.message : String(err);
869
+ process.stderr.write(`hq lanes watcher: account failover failed for ${laneId}: ${message}\n`);
870
+ logDecision({ event: "account_failover_failed", at: tick.emit.at, reason: accountRetryReason, error: message });
871
+ if (readLane(hqRoot, laneId).state !== "running")
872
+ return;
873
+ }
874
+ }
839
875
  const terminalWrite = applyTerminalToLaneWithResult(hqRoot, laneId, classification, {
840
876
  envelope,
841
877
  envelopeRef: latest?.ref,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.154.0",
3
+ "version": "5.155.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {