@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.
- package/CHANGELOG.md +25 -0
- package/dist/commands/lanes.d.ts +3 -11
- package/dist/commands/lanes.js +68 -37
- package/dist/lib/lanes/accounts.d.ts +16 -11
- package/dist/lib/lanes/accounts.js +50 -8
- package/dist/lib/lanes/admission-host.d.ts +19 -0
- package/dist/lib/lanes/admission-host.js +33 -0
- package/dist/lib/lanes/config.js +25 -0
- package/dist/lib/lanes/provider-rejection.d.ts +4 -4
- package/dist/lib/lanes/provider-rejection.js +24 -6
- package/dist/lib/lanes/reconcile.js +30 -1
- package/dist/lib/lanes/spawn.d.ts +9 -1
- package/dist/lib/lanes/spawn.js +420 -14
- package/dist/lib/lanes/types.d.ts +7 -3
- package/dist/lib/lanes/types.js +3 -2
- package/dist/lib/lanes/watcher.d.ts +8 -1
- package/dist/lib/lanes/watcher.js +39 -3
- package/package.json +1 -1
|
@@ -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 {
|
|
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,
|