@indigoai-us/hq-cli 5.223.0 → 5.225.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 +20 -1
- package/dist/command-catalog.generated.d.ts +24 -0
- package/dist/command-catalog.generated.js +31 -0
- package/dist/commands/lanes-import.d.ts +15 -0
- package/dist/commands/lanes-import.js +1213 -0
- package/dist/commands/lanes.js +8 -0
- package/dist/lib/bot/inbox-poll-backoff-flag.d.ts +24 -0
- package/dist/lib/bot/inbox-poll-backoff-flag.js +132 -0
- package/dist/lib/bot/run.d.ts +3 -1
- package/dist/lib/bot/run.js +89 -11
- package/dist/lib/daemon/hq-cloud.d.ts +2 -0
- package/dist/lib/daemon/hq-cloud.js +7 -1
- package/dist/lib/daemon/run.js +8 -0
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -15,6 +15,7 @@ import { Option } from "commander";
|
|
|
15
15
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
16
16
|
import { registerLaneChecksCommand } from "./lanes-checks.js";
|
|
17
17
|
import { registerLaneReportCommands } from "./lanes-report.js";
|
|
18
|
+
import { registerLaneTrackerImportCommand } from "./lanes-import.js";
|
|
18
19
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
19
20
|
import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
|
|
20
21
|
import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
|
|
@@ -8611,6 +8612,13 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8611
8612
|
now: ctx.now,
|
|
8612
8613
|
actor: ctx.trackerActor,
|
|
8613
8614
|
}, wrap);
|
|
8615
|
+
registerLaneTrackerImportCommand(lanes, {
|
|
8616
|
+
hqRoot: ctx.hqRoot,
|
|
8617
|
+
resolveCompany: ctx.resolveCompany,
|
|
8618
|
+
trackerEnabled: ctx.laneTrackerEnabled,
|
|
8619
|
+
trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
|
|
8620
|
+
now: ctx.now,
|
|
8621
|
+
}, wrap);
|
|
8614
8622
|
registerLaneChecksCommand(lanes, {
|
|
8615
8623
|
hqRoot: ctx.hqRoot,
|
|
8616
8624
|
resolveCompany: ctx.resolveCompany,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type FlagClient, type FlagClientOptions } from "@indigoai-us/hq-flags-client";
|
|
2
|
+
import { type BotConfig } from "./config.js";
|
|
3
|
+
export declare const INBOX_POLL_BACKOFF_FLAG = "agents.inbox-poll-backoff-local-cli";
|
|
4
|
+
export declare const INBOX_POLL_BACKOFF_FLAG_FALLBACK = false;
|
|
5
|
+
export declare const INBOX_POLL_BACKOFF_FLAG_TIMEOUT_MS = 150;
|
|
6
|
+
export declare const INBOX_POLL_BACKOFF_FLAG_CACHE_MS: number;
|
|
7
|
+
type InboxPollBackoffFlagClient = Pick<FlagClient, "ready" | "isEnabled" | "close">;
|
|
8
|
+
export interface InboxPollBackoffFlagReaderDependencies {
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
createClient?: (options: FlagClientOptions) => InboxPollBackoffFlagClient;
|
|
11
|
+
getToken?: FlagClientOptions["getToken"];
|
|
12
|
+
fetch?: typeof fetch;
|
|
13
|
+
now?: () => number;
|
|
14
|
+
reportFailure?: (error: unknown) => void;
|
|
15
|
+
}
|
|
16
|
+
export declare function safeErrorClass(error: unknown): string;
|
|
17
|
+
/**
|
|
18
|
+
* Create a per-bot reader. The flag client is opened at most once per three
|
|
19
|
+
* minutes; a missing identity, failed request, or malformed snapshot keeps
|
|
20
|
+
* idle polling at its existing two-second interval.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createInboxPollBackoffFlagReader(config: Pick<BotConfig, "kind" | "workerId" | "companies" | "companySlug" | "hqRoot">, dependencies?: InboxPollBackoffFlagReaderDependencies): () => Promise<boolean>;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=inbox-poll-backoff-flag.d.ts.map
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { loadCachedTokens } from "@indigoai-us/hq-cloud";
|
|
2
|
+
import { createFlagClient, } from "@indigoai-us/hq-flags-client";
|
|
3
|
+
import { resolveFlagGate, resolveProcessFlagCompanyContext } from "../flag-registry.js";
|
|
4
|
+
import { resolveFlagRegistryEndpoint } from "../flag-registry-endpoint.js";
|
|
5
|
+
import { effectiveBotCompanies, effectiveBotKind } from "./config.js";
|
|
6
|
+
export const INBOX_POLL_BACKOFF_FLAG = "agents.inbox-poll-backoff-local-cli";
|
|
7
|
+
export const INBOX_POLL_BACKOFF_FLAG_FALLBACK = false;
|
|
8
|
+
export const INBOX_POLL_BACKOFF_FLAG_TIMEOUT_MS = 150;
|
|
9
|
+
export const INBOX_POLL_BACKOFF_FLAG_CACHE_MS = 3 * 60_000;
|
|
10
|
+
const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
|
|
11
|
+
export function safeErrorClass(error) {
|
|
12
|
+
const name = error instanceof Error ? error.name : "UnknownError";
|
|
13
|
+
return /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(name) ? name : "UnknownError";
|
|
14
|
+
}
|
|
15
|
+
function reportFlagFailure(error) {
|
|
16
|
+
process.stderr.write(`HQ local bot inbox poll backoff flag lookup failed (${safeErrorClass(error)}); using default off.\n`);
|
|
17
|
+
}
|
|
18
|
+
function companyContexts(config, env) {
|
|
19
|
+
if (effectiveBotKind(config) !== "company")
|
|
20
|
+
return [{}];
|
|
21
|
+
const slugs = effectiveBotCompanies(config);
|
|
22
|
+
const fallbackSlug = config.companySlug?.trim();
|
|
23
|
+
if (slugs.length === 0 && fallbackSlug)
|
|
24
|
+
slugs.push(fallbackSlug);
|
|
25
|
+
if (slugs.length === 0)
|
|
26
|
+
return null;
|
|
27
|
+
const contexts = [];
|
|
28
|
+
for (const slug of slugs) {
|
|
29
|
+
const company = resolveProcessFlagCompanyContext({
|
|
30
|
+
...env,
|
|
31
|
+
HQ_COMPANY_UID: "",
|
|
32
|
+
HQ_COMPANY_SLUG: slug,
|
|
33
|
+
HQ_ROOT: config.hqRoot,
|
|
34
|
+
});
|
|
35
|
+
if (!company.companyUid || !COMPANY_UID_RE.test(company.companyUid))
|
|
36
|
+
return null;
|
|
37
|
+
contexts.push({ companyUid: company.companyUid, companyIdentifiers: [slug] });
|
|
38
|
+
}
|
|
39
|
+
return contexts;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Create a per-bot reader. The flag client is opened at most once per three
|
|
43
|
+
* minutes; a missing identity, failed request, or malformed snapshot keeps
|
|
44
|
+
* idle polling at its existing two-second interval.
|
|
45
|
+
*/
|
|
46
|
+
export function createInboxPollBackoffFlagReader(config, dependencies = {}) {
|
|
47
|
+
const env = dependencies.env ?? process.env;
|
|
48
|
+
const contexts = companyContexts(config, env);
|
|
49
|
+
const endpoint = resolveFlagRegistryEndpoint(undefined, env);
|
|
50
|
+
const now = dependencies.now ?? Date.now;
|
|
51
|
+
const createClient = dependencies.createClient ?? createFlagClient;
|
|
52
|
+
const getToken = dependencies.getToken ?? (() => loadCachedTokens()?.idToken ?? "");
|
|
53
|
+
const doFetch = dependencies.fetch ?? globalThis.fetch;
|
|
54
|
+
const report = dependencies.reportFailure ?? reportFlagFailure;
|
|
55
|
+
let cachedValue = INBOX_POLL_BACKOFF_FLAG_FALLBACK;
|
|
56
|
+
let lastReadAt = Number.NEGATIVE_INFINITY;
|
|
57
|
+
let pending = null;
|
|
58
|
+
const readContext = async (context) => {
|
|
59
|
+
let client;
|
|
60
|
+
let failed = false;
|
|
61
|
+
let failureReported = false;
|
|
62
|
+
let enabled = INBOX_POLL_BACKOFF_FLAG_FALLBACK;
|
|
63
|
+
const reportFailureOnce = (error) => {
|
|
64
|
+
failed = true;
|
|
65
|
+
if (failureReported)
|
|
66
|
+
return;
|
|
67
|
+
failureReported = true;
|
|
68
|
+
report(error);
|
|
69
|
+
};
|
|
70
|
+
try {
|
|
71
|
+
client = createClient({
|
|
72
|
+
endpoint,
|
|
73
|
+
...(context.companyUid ? { companyUid: context.companyUid } : {}),
|
|
74
|
+
...(context.companyIdentifiers ? { companyIdentifiers: context.companyIdentifiers } : {}),
|
|
75
|
+
env: {},
|
|
76
|
+
getToken,
|
|
77
|
+
...(doFetch ? { fetch: doFetch } : {}),
|
|
78
|
+
refreshIntervalMs: 0,
|
|
79
|
+
requestTimeoutMs: INBOX_POLL_BACKOFF_FLAG_TIMEOUT_MS,
|
|
80
|
+
onError: reportFailureOnce,
|
|
81
|
+
});
|
|
82
|
+
await client.ready();
|
|
83
|
+
if (!failed) {
|
|
84
|
+
enabled = resolveFlagGate(client, INBOX_POLL_BACKOFF_FLAG, { fallback: INBOX_POLL_BACKOFF_FLAG_FALLBACK }, () => INBOX_POLL_BACKOFF_FLAG_FALLBACK);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
reportFailureOnce(error);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
try {
|
|
92
|
+
client?.close();
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
reportFailureOnce(error);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return failed ? INBOX_POLL_BACKOFF_FLAG_FALLBACK : enabled;
|
|
99
|
+
};
|
|
100
|
+
const read = async () => {
|
|
101
|
+
if (!contexts || contexts.length === 0) {
|
|
102
|
+
report(new Error("local bot inbox poll backoff flag has no resolvable company identity"));
|
|
103
|
+
return INBOX_POLL_BACKOFF_FLAG_FALLBACK;
|
|
104
|
+
}
|
|
105
|
+
const values = await Promise.all(contexts.map(readContext));
|
|
106
|
+
return values.every(Boolean);
|
|
107
|
+
};
|
|
108
|
+
return () => {
|
|
109
|
+
const currentTime = now();
|
|
110
|
+
if (pending)
|
|
111
|
+
return pending;
|
|
112
|
+
if (currentTime - lastReadAt < INBOX_POLL_BACKOFF_FLAG_CACHE_MS) {
|
|
113
|
+
return Promise.resolve(cachedValue);
|
|
114
|
+
}
|
|
115
|
+
lastReadAt = currentTime;
|
|
116
|
+
pending = read()
|
|
117
|
+
.then((value) => {
|
|
118
|
+
cachedValue = value;
|
|
119
|
+
return value;
|
|
120
|
+
})
|
|
121
|
+
.catch((error) => {
|
|
122
|
+
report(error);
|
|
123
|
+
cachedValue = INBOX_POLL_BACKOFF_FLAG_FALLBACK;
|
|
124
|
+
return cachedValue;
|
|
125
|
+
})
|
|
126
|
+
.finally(() => {
|
|
127
|
+
pending = null;
|
|
128
|
+
});
|
|
129
|
+
return pending;
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=inbox-poll-backoff-flag.js.map
|
package/dist/lib/bot/run.d.ts
CHANGED
|
@@ -104,7 +104,7 @@ export declare const UNDELIVERABLE_JOB_REPORT_TEXT = "This scheduled run finishe
|
|
|
104
104
|
* instead of waiting, with no explanation, behind two long jobs.
|
|
105
105
|
*/
|
|
106
106
|
export declare const MAX_PARALLEL_JOB_TURNS = 1;
|
|
107
|
-
export type SleepFn = (ms: number) => Promise<void>;
|
|
107
|
+
export type SleepFn = (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
108
108
|
export interface BotRunDeps {
|
|
109
109
|
dir: string;
|
|
110
110
|
config: BotConfig;
|
|
@@ -118,6 +118,8 @@ export interface BotRunDeps {
|
|
|
118
118
|
pid?: number;
|
|
119
119
|
isProcessAlive?: (pid: number) => boolean;
|
|
120
120
|
inboxIntervalMs?: number;
|
|
121
|
+
/** Cached, default-off reader for the local inbox polling gate. */
|
|
122
|
+
inboxPollBackoffEnabled?: () => Promise<boolean>;
|
|
121
123
|
heartbeatIntervalMs?: number;
|
|
122
124
|
/** Override the rendered system prompt (tests). */
|
|
123
125
|
systemPromptOverride?: string;
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -76,6 +76,7 @@ import { clearBotSession, readBotSession, writeBotSession } from "./session.js";
|
|
|
76
76
|
import { defaultBotStatus, patchBotStatus, readBotStatus } from "./status.js";
|
|
77
77
|
import { CLI_VERSION } from "../../cli-version.js";
|
|
78
78
|
import { CLI_UPDATE_RESTART_EXIT_CODE } from "./cli-update.js";
|
|
79
|
+
import { createInboxPollBackoffFlagReader, safeErrorClass } from "./inbox-poll-backoff-flag.js";
|
|
79
80
|
export const INBOX_POLL_INTERVAL_MS = 2_000;
|
|
80
81
|
export const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
81
82
|
/** First 429/5xx wait when Retry-After is absent (inbox polls at 2s). */
|
|
@@ -224,8 +225,20 @@ class FailureWindow {
|
|
|
224
225
|
return this.count(nowMs) >= this.limit;
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
|
-
function defaultSleep(ms) {
|
|
228
|
-
return new Promise((resolve) =>
|
|
228
|
+
function defaultSleep(ms, signal) {
|
|
229
|
+
return new Promise((resolve) => {
|
|
230
|
+
if (signal?.aborted) {
|
|
231
|
+
resolve();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const timer = setTimeout(done, ms);
|
|
235
|
+
function done() {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
signal?.removeEventListener("abort", done);
|
|
238
|
+
resolve();
|
|
239
|
+
}
|
|
240
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
241
|
+
});
|
|
229
242
|
}
|
|
230
243
|
/**
|
|
231
244
|
* A body posted as several parts where an early part got through and a later
|
|
@@ -262,6 +275,7 @@ export async function runBot(deps) {
|
|
|
262
275
|
const { dir, config, api, runtime } = deps;
|
|
263
276
|
const runTurn = deps.runTurn ?? ((rt, input) => runRuntimeTurn(rt, input));
|
|
264
277
|
const inboxIntervalMs = deps.inboxIntervalMs ?? INBOX_POLL_INTERVAL_MS;
|
|
278
|
+
const inboxPollBackoffEnabled = deps.inboxPollBackoffEnabled ?? createInboxPollBackoffFlagReader(config);
|
|
265
279
|
const heartbeatIntervalMs = deps.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS;
|
|
266
280
|
const exit = deps.exit ?? ((code) => process.exit(code));
|
|
267
281
|
const fallback = () => defaultBotStatus(pid, config.runtime, now);
|
|
@@ -312,6 +326,16 @@ export async function runBot(deps) {
|
|
|
312
326
|
const cliVersion = deps.cliUpdate?.runningVersion ?? CLI_VERSION;
|
|
313
327
|
/** Set once an update restart is due: no new turns, then exit for the supervisor. */
|
|
314
328
|
let cliUpdatePending = null;
|
|
329
|
+
const pollForCliUpdate = () => {
|
|
330
|
+
const update = deps.cliUpdate?.poll(log) ?? null;
|
|
331
|
+
if (!update)
|
|
332
|
+
return false;
|
|
333
|
+
cliUpdatePending = update;
|
|
334
|
+
stopping = true;
|
|
335
|
+
const busy = running.size;
|
|
336
|
+
log("info", `hq-cli updated ${update.from} -> ${update.to}; restarting to load it${busy ? ` after ${busy} running turn${busy === 1 ? "" : "s"} finish${busy === 1 ? "es" : ""}` : ""}`);
|
|
337
|
+
return true;
|
|
338
|
+
};
|
|
315
339
|
// ── Preflight: creds, pid lock, owner verification ─────────────────────────
|
|
316
340
|
const credsProblem = preflightCreds(dir, config);
|
|
317
341
|
if (credsProblem) {
|
|
@@ -506,6 +530,45 @@ export async function runBot(deps) {
|
|
|
506
530
|
})();
|
|
507
531
|
// ── Inbox loop ─────────────────────────────────────────────────────────────
|
|
508
532
|
const attempts = new Map();
|
|
533
|
+
const inboxBackoffSteps = [inboxIntervalMs, 10_000, 20_000, 30_000];
|
|
534
|
+
let inboxBackoffIndex = 0;
|
|
535
|
+
let inboxWaitGeneration = 0;
|
|
536
|
+
let activeInboxWait = null;
|
|
537
|
+
const resetInboxBackoff = () => {
|
|
538
|
+
inboxBackoffIndex = 0;
|
|
539
|
+
inboxWaitGeneration += 1;
|
|
540
|
+
activeInboxWait?.abort();
|
|
541
|
+
};
|
|
542
|
+
const waitForInbox = async (delayMs, adaptive) => {
|
|
543
|
+
if (!adaptive) {
|
|
544
|
+
await sleep(delayMs);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
let remainingMs = delayMs;
|
|
548
|
+
let observedGeneration = inboxWaitGeneration;
|
|
549
|
+
while (remainingMs > 0 && !stopping && state === "running") {
|
|
550
|
+
const controller = new AbortController();
|
|
551
|
+
activeInboxWait = controller;
|
|
552
|
+
const chunkMs = Math.min(inboxIntervalMs, remainingMs);
|
|
553
|
+
await sleep(chunkMs, controller.signal);
|
|
554
|
+
if (activeInboxWait === controller)
|
|
555
|
+
activeInboxWait = null;
|
|
556
|
+
if (stopping || state !== "running")
|
|
557
|
+
break;
|
|
558
|
+
if (hasPromotionHold(dir))
|
|
559
|
+
break;
|
|
560
|
+
if (observedGeneration !== inboxWaitGeneration || controller.signal.aborted) {
|
|
561
|
+
observedGeneration = inboxWaitGeneration;
|
|
562
|
+
remainingMs = inboxIntervalMs;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
remainingMs -= chunkMs;
|
|
566
|
+
// Keep the install watcher at its original two-second cadence even when
|
|
567
|
+
// the inbox itself is waiting longer between empty polls.
|
|
568
|
+
if (remainingMs > 0 && pollForCliUpdate())
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
};
|
|
509
572
|
/** Build the user turn for a room trigger: header + recent transcript + the message. */
|
|
510
573
|
const buildRoomTurn = async (item, target, text) => {
|
|
511
574
|
const name = (item.channelName ?? "").trim() || target.channelId;
|
|
@@ -1551,6 +1614,7 @@ export async function runBot(deps) {
|
|
|
1551
1614
|
log("warn", `inbox item ${item.messageId} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1552
1615
|
})
|
|
1553
1616
|
.finally(() => {
|
|
1617
|
+
resetInboxBackoff();
|
|
1554
1618
|
busyLanes.delete(lane);
|
|
1555
1619
|
pending.delete(item.messageId);
|
|
1556
1620
|
running.delete(task);
|
|
@@ -1588,14 +1652,8 @@ export async function runBot(deps) {
|
|
|
1588
1652
|
// The install under this process was updated: take nothing new, let the
|
|
1589
1653
|
// turns already running finish, then exit so the supervisor loads it.
|
|
1590
1654
|
// Queued items were never started or acked; the next process gets them.
|
|
1591
|
-
|
|
1592
|
-
if (update) {
|
|
1593
|
-
cliUpdatePending = update;
|
|
1594
|
-
stopping = true;
|
|
1595
|
-
const busy = running.size;
|
|
1596
|
-
log("info", `hq-cli updated ${update.from} -> ${update.to}; restarting to load it${busy ? ` after ${busy} running turn${busy === 1 ? "" : "s"} finish${busy === 1 ? "es" : ""}` : ""}`);
|
|
1655
|
+
if (pollForCliUpdate())
|
|
1597
1656
|
break;
|
|
1598
|
-
}
|
|
1599
1657
|
// A kickoff held for a sign-in retries on the same timer as messages.
|
|
1600
1658
|
if (busyLanes.size === 0 &&
|
|
1601
1659
|
(!signInIssue || now().getTime() >= nextSignInRetryAt) &&
|
|
@@ -1609,17 +1667,26 @@ export async function runBot(deps) {
|
|
|
1609
1667
|
// Completions the cloud has not accepted yet: re-send the report (never
|
|
1610
1668
|
// the work), alongside the poll rather than in front of it.
|
|
1611
1669
|
kickCompletionRetries();
|
|
1670
|
+
const useInboxBackoff = await inboxPollBackoffEnabled().catch((error) => {
|
|
1671
|
+
log("warn", `inbox poll backoff flag lookup failed (${safeErrorClass(error)}); using default-off`);
|
|
1672
|
+
return false;
|
|
1673
|
+
});
|
|
1674
|
+
let items;
|
|
1612
1675
|
try {
|
|
1613
|
-
|
|
1676
|
+
items = await api.pullInbox(config.agentUid);
|
|
1614
1677
|
inboxFails = 0;
|
|
1615
1678
|
setStatus({ lastInboxPollAt: now().toISOString(), lastInboxPollOk: true, lastInboxError: undefined });
|
|
1616
1679
|
// One catch-up per job per wake: when several runs of the same job are
|
|
1617
1680
|
// pending, the newest runs and the older ones are reported skipped.
|
|
1618
1681
|
for (const id of planJobRuns(items, handledJobFires).superseded)
|
|
1619
1682
|
supersededJobRuns.add(id);
|
|
1683
|
+
if (items.length > 0)
|
|
1684
|
+
resetInboxBackoff();
|
|
1620
1685
|
for (const item of items)
|
|
1621
1686
|
enqueue(item);
|
|
1622
1687
|
pump();
|
|
1688
|
+
if (stopping || state !== "running")
|
|
1689
|
+
break;
|
|
1623
1690
|
}
|
|
1624
1691
|
catch (err) {
|
|
1625
1692
|
inboxFails += 1;
|
|
@@ -1633,7 +1700,17 @@ export async function runBot(deps) {
|
|
|
1633
1700
|
}
|
|
1634
1701
|
if (stopping || state !== "running")
|
|
1635
1702
|
break;
|
|
1636
|
-
|
|
1703
|
+
if (!useInboxBackoff) {
|
|
1704
|
+
inboxBackoffIndex = 0;
|
|
1705
|
+
await waitForInbox(inboxIntervalMs, false);
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
const delayMs = items.length > 0
|
|
1709
|
+
? inboxIntervalMs
|
|
1710
|
+
: inboxBackoffSteps[Math.min(inboxBackoffIndex, inboxBackoffSteps.length - 1)];
|
|
1711
|
+
if (items.length === 0)
|
|
1712
|
+
inboxBackoffIndex = Math.min(inboxBackoffIndex + 1, inboxBackoffSteps.length - 1);
|
|
1713
|
+
await waitForInbox(delayMs, true);
|
|
1637
1714
|
}
|
|
1638
1715
|
// Stop, drain and promotion wait for the turns already running. Queued
|
|
1639
1716
|
// messages were never started or acked, so the inbox delivers them again.
|
|
@@ -1655,6 +1732,7 @@ export async function runBot(deps) {
|
|
|
1655
1732
|
return;
|
|
1656
1733
|
}
|
|
1657
1734
|
stopping = true;
|
|
1735
|
+
activeInboxWait?.abort();
|
|
1658
1736
|
// End the turn in progress; its message gets "did not finish", not silence.
|
|
1659
1737
|
// Promotion instead drains that turn, including its reply and ACK, before
|
|
1660
1738
|
// the coordinator snapshots memory and starts the cloud consumer.
|
|
@@ -14,6 +14,8 @@ export interface HqCloudCopy {
|
|
|
14
14
|
}
|
|
15
15
|
export interface PickedHqCloud extends HqCloudCopy {
|
|
16
16
|
source: "bundled" | "managed";
|
|
17
|
+
/** Set when a version is pinned but that version is not the installed managed copy. */
|
|
18
|
+
pinnedVersionMissing?: string;
|
|
17
19
|
}
|
|
18
20
|
export declare function managedHqCloudDir(daemonRoot: string): string;
|
|
19
21
|
/** The package folder inside the updater's install prefix. */
|
|
@@ -44,8 +44,14 @@ export function pickHqCloud(bundled, managed, usable = () => true, pinnedVersion
|
|
|
44
44
|
if (managed && managed.version === pinnedVersion && b && m && m[0] === b[0] && usable(managed)) {
|
|
45
45
|
return { ...managed, source: "managed" };
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
// The pinned version is not installed yet (the updater installs it). Keep
|
|
48
|
+
// running the newest usable copy meanwhile instead of dropping to an older
|
|
49
|
+
// bundled one, and report the missing pin.
|
|
50
|
+
return { ...pickNewest(bundled, managed, b, m, usable), pinnedVersionMissing: pinnedVersion };
|
|
48
51
|
}
|
|
52
|
+
return pickNewest(bundled, managed, b, m, usable);
|
|
53
|
+
}
|
|
54
|
+
function pickNewest(bundled, managed, b, m, usable) {
|
|
49
55
|
if (managed && b && m && m[0] === b[0] && newer(m, b) && usable(managed))
|
|
50
56
|
return { ...managed, source: "managed" };
|
|
51
57
|
return { ...bundled, source: "bundled" };
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -243,6 +243,7 @@ export async function runDaemon(opts) {
|
|
|
243
243
|
}
|
|
244
244
|
return legacyCache.units;
|
|
245
245
|
};
|
|
246
|
+
let warnedMissingPin = null;
|
|
246
247
|
const plan = () => {
|
|
247
248
|
const config = readDaemonConfig(paths.configPath);
|
|
248
249
|
const legacy = legacyUnits();
|
|
@@ -250,6 +251,13 @@ export async function runDaemon(opts) {
|
|
|
250
251
|
const hqRoot = configured && fs.existsSync(configured) ? configured : undefined;
|
|
251
252
|
// Re-picked every reconcile, so a newer hq-cloud from the updater is used as soon as it lands.
|
|
252
253
|
const hqCloud = currentHqCloud(paths.root);
|
|
254
|
+
const missingPin = hqCloud?.pinnedVersionMissing ?? null;
|
|
255
|
+
if (missingPin !== warnedMissingPin) {
|
|
256
|
+
warnedMissingPin = missingPin;
|
|
257
|
+
if (missingPin && hqCloud) {
|
|
258
|
+
log("warn", `hq-cloud is pinned to ${missingPin}, which is not installed; running ${hqCloud.version} (${hqCloud.source}) until the updater installs it`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
253
261
|
const runners = hqCloud ? hqCloudRunners(hqCloud.root) : { syncRunner: null, heartbeat: null, sessionHost: null };
|
|
254
262
|
return planUnits(enabledServices(machineType, config), {
|
|
255
263
|
machineType,
|