@indigoai-us/hq-cli 5.345.187 → 5.345.189
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 +111 -0
- package/dist/command-catalog.generated.d.ts +34 -0
- package/dist/command-catalog.generated.js +42 -0
- package/dist/commands/agent-inbox.js +43 -11
- package/dist/commands/agent-mcp.js +5 -2
- package/dist/commands/bot.js +15 -3
- package/dist/commands/core-company-index-scope-flag.d.ts +2 -1
- package/dist/commands/core-company-index-scope-flag.js +2 -1
- package/dist/commands/core.js +0 -5
- package/dist/commands/dm-attach.d.ts +73 -0
- package/dist/commands/dm-attach.js +104 -0
- package/dist/commands/dm.d.ts +10 -0
- package/dist/commands/dm.js +114 -2
- package/dist/commands/sources.js +15 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +57330 -63803
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-fetch.d.ts +92 -0
- package/dist/lib/agent-kit/inbox-fetch.js +135 -0
- package/dist/lib/agent-kit/mcp/tools.d.ts +11 -2
- package/dist/lib/agent-kit/mcp/tools.js +17 -3
- package/dist/lib/bot/fleet-local.d.ts +3 -1
- package/dist/lib/bot/fleet-local.js +9 -3
- package/dist/lib/bot/fleet-python.d.ts +6 -0
- package/dist/lib/bot/fleet-python.js +24 -5
- package/dist/lib/bot/probe.d.ts +7 -1
- package/dist/lib/bot/probe.js +11 -3
- package/dist/lib/bot/run.d.ts +9 -0
- package/dist/lib/bot/run.js +114 -8
- package/dist/lib/bot/runtime-failure.d.ts +3 -0
- package/dist/lib/bot/runtime-failure.js +20 -2
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/index-render/session-scope.d.ts +3 -3
- package/dist/lib/index-render/session-scope.js +11 -9
- package/dist/lib/index-render/shared.d.ts +6 -4
- package/dist/lib/index-render/shared.js +33 -4
- package/dist/utils/cognito-session.d.ts +5 -3
- package/dist/utils/cognito-session.js +43 -46
- package/dist/utils/hq-root-resolver.d.ts +27 -1
- package/dist/utils/hq-root-resolver.js +65 -33
- package/package.json +15 -15
package/dist/lib/bot/run.js
CHANGED
|
@@ -107,6 +107,49 @@ export const API_BACKOFF_CAP_MS = 5 * 60_000;
|
|
|
107
107
|
/** Honor Retry-After up to this; a bogus 30-day header must not park the bot. */
|
|
108
108
|
export const API_RETRY_AFTER_CAP_MS = 15 * 60_000;
|
|
109
109
|
export const API_RETRY_AFTER_FLOOR_MS = 1_000;
|
|
110
|
+
/**
|
|
111
|
+
* Delays between startup identity-check tries when HQ cannot be reached
|
|
112
|
+
* (five tries over about three minutes). After the last one the bot stops in
|
|
113
|
+
* a visible failed state the person can retry.
|
|
114
|
+
*/
|
|
115
|
+
export const STARTUP_VERIFY_RETRY_DELAYS_MS = [5_000, 15_000, 45_000, 120_000];
|
|
116
|
+
/**
|
|
117
|
+
* True for failures worth retrying: transport errors (no HQ response at all),
|
|
118
|
+
* request timeouts, rate limits and server errors. Other 4xx answers are real
|
|
119
|
+
* refusals and are not retried.
|
|
120
|
+
*/
|
|
121
|
+
const TRANSIENT_NETWORK_CODES = new Set([
|
|
122
|
+
"ECONNREFUSED",
|
|
123
|
+
"ECONNRESET",
|
|
124
|
+
"ENOTFOUND",
|
|
125
|
+
"ETIMEDOUT",
|
|
126
|
+
"EAI_AGAIN",
|
|
127
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
128
|
+
"UND_ERR_SOCKET",
|
|
129
|
+
]);
|
|
130
|
+
function isNetworkShapedError(err, depth = 0) {
|
|
131
|
+
if (depth > 4 || !err || typeof err !== "object")
|
|
132
|
+
return false;
|
|
133
|
+
const e = err;
|
|
134
|
+
if (typeof e.code === "string" && TRANSIENT_NETWORK_CODES.has(e.code))
|
|
135
|
+
return true;
|
|
136
|
+
if (e.name === "AbortError" || e.name === "TimeoutError")
|
|
137
|
+
return true;
|
|
138
|
+
if (err instanceof TypeError && e.message === "fetch failed")
|
|
139
|
+
return true;
|
|
140
|
+
return isNetworkShapedError(e.cause, depth + 1);
|
|
141
|
+
}
|
|
142
|
+
export function isTransientApiError(err) {
|
|
143
|
+
if (err instanceof BotApiError)
|
|
144
|
+
return err.status === 408 || err.status === 429 || err.status >= 500;
|
|
145
|
+
// Only network-shaped failures (no HQ answer at all). Auth and programming
|
|
146
|
+
// errors fail at once.
|
|
147
|
+
return isNetworkShapedError(err);
|
|
148
|
+
}
|
|
149
|
+
/** Plain failed-state reason after the startup retries run out. */
|
|
150
|
+
export function startupUnreachableReason(name) {
|
|
151
|
+
return `could not reach HQ to start; check the network, then restart the bot (hq bot start ${name})`;
|
|
152
|
+
}
|
|
110
153
|
/**
|
|
111
154
|
* How long to wait after a failed inbox poll or heartbeat.
|
|
112
155
|
* 429/5xx (and transport errors) back off exponentially, honouring Retry-After
|
|
@@ -566,8 +609,57 @@ export async function runBot(deps) {
|
|
|
566
609
|
setStatus({ lastError: `own ${runtime.id} login missing: run hq bot login ${config.name} --runtime ${runtime.id}` });
|
|
567
610
|
}
|
|
568
611
|
deps.cliUpdate?.startup(log);
|
|
612
|
+
// Stop handling is registered before the startup identity retries, which
|
|
613
|
+
// can wait about three minutes: a SIGTERM during a wait must still release
|
|
614
|
+
// the pid lock and record "stopped" with exit 0. Once the loops start,
|
|
615
|
+
// signals route to the full stop().
|
|
616
|
+
const startupAbort = new AbortController();
|
|
617
|
+
let onStopRequest = () => {
|
|
618
|
+
stopping = true;
|
|
619
|
+
startupAbort.abort();
|
|
620
|
+
};
|
|
621
|
+
const events = deps.processEvents ?? process;
|
|
622
|
+
events.once("SIGTERM", () => onStopRequest("abort"));
|
|
623
|
+
events.once("SIGINT", () => onStopRequest("abort"));
|
|
624
|
+
events.once("SIGUSR2", () => onStopRequest("drain"));
|
|
625
|
+
const stoppedDuringStartup = async () => {
|
|
626
|
+
await finish("stopped", 0, "stop requested");
|
|
627
|
+
return { stop: async () => { }, done, status: () => null, idle: () => true };
|
|
628
|
+
};
|
|
569
629
|
try {
|
|
570
|
-
|
|
630
|
+
let record;
|
|
631
|
+
// A network drop at login used to fail the bot with exit 0, which launchd
|
|
632
|
+
// KeepAlive{SuccessfulExit:false} never restarts, so one blip kept bots
|
|
633
|
+
// down for a week. Transient failures now retry with a growing delay
|
|
634
|
+
// before the bot settles into a visible failed state.
|
|
635
|
+
for (let attempt = 0;; attempt++) {
|
|
636
|
+
try {
|
|
637
|
+
record = await api.getAgent(config.agentUid);
|
|
638
|
+
if (attempt > 0)
|
|
639
|
+
setStatus({ lastError: undefined });
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
catch (err) {
|
|
643
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
644
|
+
const delayMs = STARTUP_VERIFY_RETRY_DELAYS_MS[attempt];
|
|
645
|
+
if (stopping)
|
|
646
|
+
return await stoppedDuringStartup();
|
|
647
|
+
if (!isTransientApiError(err))
|
|
648
|
+
throw err;
|
|
649
|
+
if (delayMs === undefined) {
|
|
650
|
+
log("error", `could not reach HQ to verify bot identity: ${detail}`);
|
|
651
|
+
await finish("failed", 0, startupUnreachableReason(config.name));
|
|
652
|
+
return { stop: async () => { }, done, status: () => null, idle: () => true };
|
|
653
|
+
}
|
|
654
|
+
log("warn", `could not reach HQ to verify bot identity (try ${attempt + 1} of ${STARTUP_VERIFY_RETRY_DELAYS_MS.length + 1}): ${detail}; retrying in ${Math.round(delayMs / 1000)}s`);
|
|
655
|
+
setStatus({ lastError: "Could not reach HQ yet; retrying" });
|
|
656
|
+
await sleep(delayMs, startupAbort.signal);
|
|
657
|
+
if (stopping)
|
|
658
|
+
return await stoppedDuringStartup();
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (stopping)
|
|
662
|
+
return await stoppedDuringStartup();
|
|
571
663
|
// Promotion keeps the identity and owner. Ownership alone must not let
|
|
572
664
|
// an old local installation start consuming the cloud bot's inbox.
|
|
573
665
|
if (record.uid !== config.agentUid || record.computeMode !== "local" || record.botKind !== kind) {
|
|
@@ -650,6 +742,10 @@ export async function runBot(deps) {
|
|
|
650
742
|
// Read before the kickoff can write its own marker: whatever is here now was
|
|
651
743
|
// left by a previous run that was cut off mid-turn.
|
|
652
744
|
const leftovers = readInflightTurns(dir);
|
|
745
|
+
// A signal that arrived anywhere in the startup stretch above ends the bot
|
|
746
|
+
// here, before any outbound DM.
|
|
747
|
+
if (stopping)
|
|
748
|
+
return await stoppedDuringStartup();
|
|
653
749
|
// ── Intro DM (once) ────────────────────────────────────────────────────────
|
|
654
750
|
// The kickoff turn is tied to the intro: it runs only in the start that sent
|
|
655
751
|
// the intro, so introSentAt (persisted before the turn) guarantees it never
|
|
@@ -666,6 +762,8 @@ export async function runBot(deps) {
|
|
|
666
762
|
let offered = false;
|
|
667
763
|
if (offerTool) {
|
|
668
764
|
try {
|
|
765
|
+
if (stopping)
|
|
766
|
+
return await stoppedDuringStartup();
|
|
669
767
|
await api.sendDm({ toPersonUid: config.ownerUid, body: setupToolOfferText(offerTool, setupWelcomeOpening(config.intro)) });
|
|
670
768
|
patchBotConfig(dir, { introSentAt: now().toISOString() });
|
|
671
769
|
log("info", "intro DM sent to owner, leading into the app offer");
|
|
@@ -676,6 +774,8 @@ export async function runBot(deps) {
|
|
|
676
774
|
log("warn", `coding tool offer could not be sent as the welcome (${err instanceof Error ? err.message : String(err)}); sending the usual welcome`);
|
|
677
775
|
}
|
|
678
776
|
}
|
|
777
|
+
if (stopping)
|
|
778
|
+
return await stoppedDuringStartup();
|
|
679
779
|
if (!offered)
|
|
680
780
|
try {
|
|
681
781
|
const sent = await api.sendDm({ toPersonUid: config.ownerUid, body: introDmText(config.name, config.runtime, config.intro, { kind: effectiveBotKind(config), companies: effectiveBotCompanies(config) }) });
|
|
@@ -701,6 +801,8 @@ export async function runBot(deps) {
|
|
|
701
801
|
log("warn", `intro DM failed (will retry next start): ${err instanceof Error ? err.message : String(err)}`);
|
|
702
802
|
}
|
|
703
803
|
}
|
|
804
|
+
if (stopping)
|
|
805
|
+
return await stoppedDuringStartup();
|
|
704
806
|
// ── Heartbeat loop ─────────────────────────────────────────────────────────
|
|
705
807
|
const failures = new FailureWindow(FAILURE_WINDOW_MS, FAILURE_LIMIT);
|
|
706
808
|
let modelHealth = "ok";
|
|
@@ -2639,6 +2741,10 @@ export async function runBot(deps) {
|
|
|
2639
2741
|
// app know before any message arrives. A tool that cannot be asked changes
|
|
2640
2742
|
// nothing; the first turn then reports for itself.
|
|
2641
2743
|
await runSignInCheck("start");
|
|
2744
|
+
// A signal during the sign-in probe arrives before the full stop() is wired
|
|
2745
|
+
// up below: end here, before the kickoff can send a "needs a sign-in" DM.
|
|
2746
|
+
if (stopping)
|
|
2747
|
+
return await stoppedDuringStartup();
|
|
2642
2748
|
// The kickoff runs alongside the heartbeat, before the first inbox poll, so
|
|
2643
2749
|
// an owner message sent meanwhile is answered after it in the same session.
|
|
2644
2750
|
const kickoff = kickoffAfterIntro
|
|
@@ -2837,9 +2943,15 @@ export async function runBot(deps) {
|
|
|
2837
2943
|
// bot then exits 0 and stays down instead of being restarted.
|
|
2838
2944
|
cliUpdatePending = null;
|
|
2839
2945
|
if (stopping) {
|
|
2946
|
+
// Already stopping (an earlier stop, a startup signal, or an update
|
|
2947
|
+
// restart): still wait for the loops and finish, so this call ends the
|
|
2948
|
+
// bot too. finish() is a no-op once the bot has stopped.
|
|
2840
2949
|
if (mode === "abort")
|
|
2841
2950
|
for (const turn of currentTurns)
|
|
2842
2951
|
turn.abort();
|
|
2952
|
+
activeInboxWait?.abort();
|
|
2953
|
+
await Promise.allSettled([heartbeatLoop, inboxLoop]);
|
|
2954
|
+
await finish("stopped", 0, "stop requested");
|
|
2843
2955
|
return;
|
|
2844
2956
|
}
|
|
2845
2957
|
stopping = true;
|
|
@@ -2853,13 +2965,7 @@ export async function runBot(deps) {
|
|
|
2853
2965
|
await Promise.allSettled([heartbeatLoop, inboxLoop]);
|
|
2854
2966
|
await finish("stopped", 0, mode === "drain" ? "drain completed (running turns finished)" : "stop requested");
|
|
2855
2967
|
};
|
|
2856
|
-
|
|
2857
|
-
void stop();
|
|
2858
|
-
};
|
|
2859
|
-
const events = deps.processEvents ?? process;
|
|
2860
|
-
events.once("SIGTERM", onSignal);
|
|
2861
|
-
events.once("SIGINT", onSignal);
|
|
2862
|
-
events.once("SIGUSR2", () => { void stop("drain"); });
|
|
2968
|
+
onStopRequest = (mode) => { void stop(mode); };
|
|
2863
2969
|
void inboxLoop.then(() => { if (hasPromotionHold(dir))
|
|
2864
2970
|
void stop("drain"); });
|
|
2865
2971
|
setStatus({ canDrainForPromotion: true });
|
|
@@ -39,6 +39,9 @@ export declare const RUNTIME_REPAIR_ACTIONS: Record<RuntimeRepairClass, RuntimeR
|
|
|
39
39
|
export declare function isRuntimeRepairClass(cls: RuntimeFailureClass): cls is RuntimeRepairClass;
|
|
40
40
|
/** Sort a runtime's failure text into one class. Order matters: most specific first. */
|
|
41
41
|
export declare function classifyRuntimeFailure(text: string | undefined | null): RuntimeFailureClass;
|
|
42
|
+
export declare function isRuntimeUsageLimit(text: string | undefined | null): boolean;
|
|
43
|
+
/** When a usage limit resets, as the tool wrote it ("9pm (Asia/Dhaka)"), or undefined. */
|
|
44
|
+
export declare function usageLimitResetsAt(text: string | undefined | null): string | undefined;
|
|
42
45
|
export declare function isBunProcessError(text: string | undefined | null): boolean;
|
|
43
46
|
/** A process that exits non-zero this soon, with no stream event, never started its turn. */
|
|
44
47
|
export declare const RUNTIME_CRASH_MAX_MS = 5000;
|
|
@@ -44,7 +44,15 @@ const VERSION_TOO_OLD_MODEL = /\bthe ['"`]([^'"`\s]{1,128})['"`] model requires
|
|
|
44
44
|
* Codex with a ChatGPT account".
|
|
45
45
|
*/
|
|
46
46
|
const MODEL_UNSUPPORTED = /there'?s an issue with the selected model|\bnot_found_error\b[^\n]{0,200}\bmodel:\s|\bmodel_not_found\b|\b(?:unknown|invalid|unsupported) model\b|\bthe (?:requested )?model ['"`]?[^'"`\s]{1,128}['"`]? (?:does not exist|is not supported|is not available|was not found|has been deprecated)\b|\bmodel ['"`][^'"`\s]{1,128}['"`] (?:does not exist|is not supported|is not available|not found)\b|\bdo(?:es)? not have access to (?:the )?model\b|\bmodel is not supported when using codex with a chatgpt account\b/i;
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The account's plan ran out of usage. Claude Code: "You've hit your weekly
|
|
49
|
+
* limit · resets 9pm (Asia/Dhaka)", "You've hit your session limit". Codex:
|
|
50
|
+
* "You've hit your usage limit".
|
|
51
|
+
*/
|
|
52
|
+
const USAGE_LIMIT = /\byou'?ve (?:hit|reached) your (?:[\w-]+ ){0,3}limit\b|\b(?:usage|session|weekly|daily|monthly|5-hour) limit (?:reached|exceeded)\b/i;
|
|
53
|
+
/** "9pm (Asia/Dhaka)" from "... · resets 9pm (Asia/Dhaka)". */
|
|
54
|
+
const USAGE_LIMIT_RESET = /\bresets?(?: at| on)? ([^\n|·]{1,60}?)\s*(?:$|[\n|·])/im;
|
|
55
|
+
const TRANSIENT = /\brate[_ ]limit|\busage[_ ]limit|\btoo many requests\b|\b429\b|\boverloaded\b|\b(?:502|503|504|529)\b|\bservice unavailable\b|\bbad gateway\b|\bgateway time-?out\b|\binternal server error\b|\bECONNRESET\b|\bECONNREFUSED\b|\bETIMEDOUT\b|\bENOTFOUND\b|\bEAI_AGAIN\b|\bENETUNREACH\b|\bsocket hang up\b|\bnetwork (?:error|is unreachable)\b|\bstream (?:disconnected|error)\b|\bconnection (?:reset|refused|closed|error)\b|\btimed out\b|\bexceeded \d+ms \(HQ_BOT_TURN_TIMEOUT_MS\)/i;
|
|
48
56
|
/** Sort a runtime's failure text into one class. Order matters: most specific first. */
|
|
49
57
|
export function classifyRuntimeFailure(text) {
|
|
50
58
|
const t = (text ?? "").trim();
|
|
@@ -58,10 +66,20 @@ export function classifyRuntimeFailure(text) {
|
|
|
58
66
|
return "signed-out";
|
|
59
67
|
if (MODEL_UNSUPPORTED.test(t))
|
|
60
68
|
return "model-unsupported";
|
|
61
|
-
if (TRANSIENT.test(t))
|
|
69
|
+
if (TRANSIENT.test(t) || USAGE_LIMIT.test(t))
|
|
62
70
|
return "transient";
|
|
63
71
|
return "unknown";
|
|
64
72
|
}
|
|
73
|
+
export function isRuntimeUsageLimit(text) {
|
|
74
|
+
return USAGE_LIMIT.test((text ?? "").trim());
|
|
75
|
+
}
|
|
76
|
+
/** When a usage limit resets, as the tool wrote it ("9pm (Asia/Dhaka)"), or undefined. */
|
|
77
|
+
export function usageLimitResetsAt(text) {
|
|
78
|
+
const t = text ?? "";
|
|
79
|
+
if (!USAGE_LIMIT.test(t))
|
|
80
|
+
return undefined;
|
|
81
|
+
return USAGE_LIMIT_RESET.exec(t)?.[1]?.trim() || undefined;
|
|
82
|
+
}
|
|
65
83
|
/**
|
|
66
84
|
* Bun's process-level error line (Claude Code is a Bun-compiled binary):
|
|
67
85
|
* "error: An unknown error occurred (Unexpected)". The excerpt joins stderr
|