@indigoai-us/hq-cli 5.345.107 → 5.345.109
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 +15 -0
- package/dist/command-catalog.generated.d.ts +40 -1
- package/dist/command-catalog.generated.js +48 -1
- package/dist/commands/capacity.d.ts +1 -1
- package/dist/commands/capacity.js +74 -11
- package/dist/commands/core-checkpoint-stop-gate-native.js +17 -5
- package/dist/commands/lanes.js +72 -12
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +612 -460
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +38 -1
- package/dist/lib/capacity/ledger.d.ts +1 -0
- package/dist/lib/capacity/ledger.js +10 -0
- package/dist/lib/capacity/route.d.ts +146 -0
- package/dist/lib/capacity/route.js +452 -0
- 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/lanes/store.js +4 -4
- package/dist/lib/lanes/types.d.ts +20 -4
- package/dist/lib/lanes/types.js +1 -1
- package/dist/utils/package-use-lease.d.ts +15 -1
- package/dist/utils/package-use-lease.js +45 -2
- package/package.json +2 -2
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure router for `hq capacity route` and `hq lanes create --account auto`.
|
|
3
|
+
*
|
|
4
|
+
* Port of personal/skills/boss-mode/boss-route.mjs into hq-cli. Owner decision
|
|
5
|
+
* 2026-10-11: all accounts are one pool across companies; the pick names the
|
|
6
|
+
* account so the board shows which account a lane ran on.
|
|
7
|
+
*
|
|
8
|
+
* Reads the ledger via src/lib/capacity/read.ts (NowView), the lane account
|
|
9
|
+
* registry via src/lib/lanes/config.ts (LanesConfig), and the live lanes list
|
|
10
|
+
* via src/lib/lanes/store.ts (readAllLanes). No shell to `hq`, no network.
|
|
11
|
+
*
|
|
12
|
+
* Returns the same shape as boss-route.mjs:
|
|
13
|
+
* { ok, at, request, pick, ranked, skipped, moves, next_reset, capacity,
|
|
14
|
+
* lanes_error }
|
|
15
|
+
*/
|
|
16
|
+
import { canonicalizeHome } from "../lanes/config.js";
|
|
17
|
+
import { LANE_PROVIDERS } from "../lanes/types.js";
|
|
18
|
+
/** All lane providers, in the stable order boss-route uses for ranking ties. */
|
|
19
|
+
export const ROUTE_PROVIDERS = LANE_PROVIDERS;
|
|
20
|
+
/** Lane states that count against an account's live load. */
|
|
21
|
+
export const LIVE_STATES = new Set([
|
|
22
|
+
"running",
|
|
23
|
+
"queued",
|
|
24
|
+
"awaiting_input",
|
|
25
|
+
]);
|
|
26
|
+
/** Limit kinds the router scores against. */
|
|
27
|
+
export const LIMIT_KINDS = new Set(["session", "weekly_all", "weekly_scoped"]);
|
|
28
|
+
/** A reading older than this is treated as no reading at all. */
|
|
29
|
+
export const STALE_SKIP_MS = 24 * 36e5;
|
|
30
|
+
/** Headroom below this percent is "nearly used up"; the account is skipped. */
|
|
31
|
+
export const NEARLY_OUT = 5;
|
|
32
|
+
/** Score penalty per live lane already on the account. */
|
|
33
|
+
export const LANE_PENALTY = 3;
|
|
34
|
+
/** Weekly reset within this window boosts an otherwise idle account. */
|
|
35
|
+
export const RESET_SOON_MS = 24 * 36e5;
|
|
36
|
+
/** Score bonus for a weekly that resets within RESET_SOON_MS. */
|
|
37
|
+
export const RESET_SOON_BONUS = 10;
|
|
38
|
+
/** A limit reading older than this is dropped as unknown until its window resets. */
|
|
39
|
+
export const LIMIT_STALE_MS = 24 * 36e5;
|
|
40
|
+
/** Readings older than this rank below fresh ones by OLD_READING_PENALTY. */
|
|
41
|
+
export const FRESH_MS = 60 * 6e4;
|
|
42
|
+
/** Score penalty for an account whose binding limit reading is not fresh. */
|
|
43
|
+
export const OLD_READING_PENALTY = 10;
|
|
44
|
+
/** Default --floor applied by the CLI (percent headroom to clear). */
|
|
45
|
+
export const DEFAULT_FLOOR = 15;
|
|
46
|
+
/** Default --prefer order applied by the CLI. */
|
|
47
|
+
export const DEFAULT_PREFER = LANE_PROVIDERS;
|
|
48
|
+
/** Sign-in states a lane can still run on. */
|
|
49
|
+
export const RUNNABLE_AUTH = new Set(["ok", "expired"]);
|
|
50
|
+
/** Map a model id to its lane provider, or null when the model is unmapped. */
|
|
51
|
+
export function providerForModel(model) {
|
|
52
|
+
const s = model.toLowerCase();
|
|
53
|
+
if (/claude|fable|opus|sonnet|haiku/.test(s))
|
|
54
|
+
return "claude";
|
|
55
|
+
if (/gpt|codex|^o\d/.test(s))
|
|
56
|
+
return "codex";
|
|
57
|
+
if (/grok/.test(s))
|
|
58
|
+
return "grok";
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
/** True when the limit should factor into the pick for this request. */
|
|
62
|
+
export function limitApplies(provider, limit, model) {
|
|
63
|
+
if (!LIMIT_KINDS.has(limit.kind))
|
|
64
|
+
return false;
|
|
65
|
+
if (limit.kind !== "weekly_scoped")
|
|
66
|
+
return true;
|
|
67
|
+
if (provider === "grok")
|
|
68
|
+
return true;
|
|
69
|
+
if (!model || !limit.scope)
|
|
70
|
+
return false;
|
|
71
|
+
return model.toLowerCase().includes(String(limit.scope).toLowerCase());
|
|
72
|
+
}
|
|
73
|
+
/** Human label for the limit a reason or binding row quotes. */
|
|
74
|
+
export function labelLimit(limit) {
|
|
75
|
+
if (limit.kind === "session")
|
|
76
|
+
return "session (5h)";
|
|
77
|
+
if (limit.kind === "weekly_all")
|
|
78
|
+
return "weekly";
|
|
79
|
+
return `${limit.scope ?? "scoped"} weekly`;
|
|
80
|
+
}
|
|
81
|
+
/** Lane account whose canonical home matches one of the ledger account's homes. */
|
|
82
|
+
export function laneAccountFor(provider, homes, lanesConfig) {
|
|
83
|
+
const accounts = lanesConfig.providers?.[provider]?.accounts ?? {};
|
|
84
|
+
if (!homes || homes.length === 0)
|
|
85
|
+
return null;
|
|
86
|
+
const wanted = new Set(homes.map((h) => safeCanonical(h)));
|
|
87
|
+
for (const [id, cfg] of Object.entries(accounts)) {
|
|
88
|
+
if (!cfg?.home)
|
|
89
|
+
continue;
|
|
90
|
+
if (wanted.has(safeCanonical(cfg.home))) {
|
|
91
|
+
return { id, enabled: cfg.enabled !== false };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function safeCanonical(home) {
|
|
97
|
+
try {
|
|
98
|
+
return canonicalizeHome(home);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return home;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export class RouteUsageError extends Error {
|
|
105
|
+
code = "route_usage";
|
|
106
|
+
constructor(message) {
|
|
107
|
+
super(message);
|
|
108
|
+
this.name = "RouteUsageError";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Parse `--provider any|claude|codex|grok[,..]` honouring the model expansion rule. */
|
|
112
|
+
export function resolveRequestedProviders(raw, model) {
|
|
113
|
+
const values = (raw ?? ["any"]).map((p) => p.trim()).filter(Boolean);
|
|
114
|
+
if (values.length === 0)
|
|
115
|
+
return [...ROUTE_PROVIDERS];
|
|
116
|
+
if (values.includes("any")) {
|
|
117
|
+
if (model) {
|
|
118
|
+
const mapped = providerForModel(model);
|
|
119
|
+
if (mapped)
|
|
120
|
+
return [mapped];
|
|
121
|
+
}
|
|
122
|
+
return [...ROUTE_PROVIDERS];
|
|
123
|
+
}
|
|
124
|
+
const out = [];
|
|
125
|
+
for (const value of values) {
|
|
126
|
+
if (!isLaneProvider(value)) {
|
|
127
|
+
throw new RouteUsageError(`unknown provider: ${value}`);
|
|
128
|
+
}
|
|
129
|
+
if (!out.includes(value))
|
|
130
|
+
out.push(value);
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
/** Parse `--prefer claude,codex,grok`. */
|
|
135
|
+
export function resolvePreferOrder(raw) {
|
|
136
|
+
const values = (raw ?? [...ROUTE_PROVIDERS]).map((p) => p.trim()).filter(Boolean);
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const value of values) {
|
|
139
|
+
if (!isLaneProvider(value)) {
|
|
140
|
+
throw new RouteUsageError(`unknown provider in --prefer: ${value}`);
|
|
141
|
+
}
|
|
142
|
+
if (!out.includes(value))
|
|
143
|
+
out.push(value);
|
|
144
|
+
}
|
|
145
|
+
if (out.length === 0)
|
|
146
|
+
return [...ROUTE_PROVIDERS];
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
/** Parse `--floor <n>`; 0..100 inclusive, default DEFAULT_FLOOR when missing. */
|
|
150
|
+
export function resolveFloor(raw) {
|
|
151
|
+
if (raw === undefined || raw === "")
|
|
152
|
+
return DEFAULT_FLOOR;
|
|
153
|
+
const value = Number(raw);
|
|
154
|
+
if (!Number.isFinite(value) || value < 0 || value > 100) {
|
|
155
|
+
throw new RouteUsageError("--floor must be 0 to 100");
|
|
156
|
+
}
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
function isLaneProvider(value) {
|
|
160
|
+
return ROUTE_PROVIDERS.includes(value);
|
|
161
|
+
}
|
|
162
|
+
function liveLaneCount(lanes, provider, account) {
|
|
163
|
+
let count = 0;
|
|
164
|
+
for (const lane of lanes) {
|
|
165
|
+
if (lane.provider !== provider)
|
|
166
|
+
continue;
|
|
167
|
+
if (lane.account !== account)
|
|
168
|
+
continue;
|
|
169
|
+
if (!LIVE_STATES.has(lane.state))
|
|
170
|
+
continue;
|
|
171
|
+
count += 1;
|
|
172
|
+
}
|
|
173
|
+
return count;
|
|
174
|
+
}
|
|
175
|
+
function limitAgeMs(view, nowMs) {
|
|
176
|
+
if (!view.resetPassed || !view.limit.resetsAt)
|
|
177
|
+
return view.readAge;
|
|
178
|
+
const resetMs = Date.parse(view.limit.resetsAt);
|
|
179
|
+
if (!Number.isFinite(resetMs))
|
|
180
|
+
return view.readAge;
|
|
181
|
+
return Math.min(view.readAge, Math.max(0, nowMs - resetMs));
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Pure routing decision over a snapshot: scores every signed-in lane account,
|
|
185
|
+
* applies the --prefer order under the --floor headroom, returns the pick (or
|
|
186
|
+
* null when nothing has room), and the lane-move suggestions for live lanes
|
|
187
|
+
* sitting on bad accounts.
|
|
188
|
+
*/
|
|
189
|
+
export function pickAccount(input) {
|
|
190
|
+
const nowDate = input.now;
|
|
191
|
+
const nowMs = nowDate.getTime();
|
|
192
|
+
const model = (input.model ?? "").trim();
|
|
193
|
+
const requested = resolveRequestedProviders(input.providers, model);
|
|
194
|
+
const prefer = resolvePreferOrder(input.prefer);
|
|
195
|
+
const floor = resolveFloor(input.floor ?? DEFAULT_FLOOR);
|
|
196
|
+
const lanes = input.lanes ?? [];
|
|
197
|
+
const ranked = [];
|
|
198
|
+
const skipped = [];
|
|
199
|
+
for (const provider of input.view.providers) {
|
|
200
|
+
const providerId = provider.provider;
|
|
201
|
+
if (!isLaneProvider(providerId))
|
|
202
|
+
continue;
|
|
203
|
+
const accounts = provider.accounts ?? [];
|
|
204
|
+
for (const account of accounts) {
|
|
205
|
+
if (!requested.includes(providerId))
|
|
206
|
+
continue;
|
|
207
|
+
const laneMatch = laneAccountFor(providerId, account.homes, input.lanesConfig);
|
|
208
|
+
const label = account.labels?.[0] ?? account.accountId;
|
|
209
|
+
const base = {
|
|
210
|
+
provider: providerId,
|
|
211
|
+
account: laneMatch?.id ?? null,
|
|
212
|
+
label,
|
|
213
|
+
organization: account.organization ?? null,
|
|
214
|
+
};
|
|
215
|
+
if (!laneMatch) {
|
|
216
|
+
skipped.push({
|
|
217
|
+
...base,
|
|
218
|
+
reason: "no lane account uses this sign-in",
|
|
219
|
+
fix: `hq capacity login ${providerId}`,
|
|
220
|
+
});
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
const authState = account.authState ?? "unknown";
|
|
224
|
+
if (account.signedOut || !RUNNABLE_AUTH.has(authState)) {
|
|
225
|
+
skipped.push({
|
|
226
|
+
...base,
|
|
227
|
+
reason: account.reason ?? `sign-in ${authState}`,
|
|
228
|
+
fix: account.fixCommand ?? null,
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const tokenExpired = authState === "expired";
|
|
233
|
+
if (!laneMatch.enabled) {
|
|
234
|
+
skipped.push({
|
|
235
|
+
...base,
|
|
236
|
+
reason: "usage-only, lanes cannot run on it",
|
|
237
|
+
fix: `hq lanes accounts enable ${providerId} ${laneMatch.id}`,
|
|
238
|
+
});
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const observedMs = account.observedAt ? Date.parse(account.observedAt) : NaN;
|
|
242
|
+
const accountAge = Number.isFinite(observedMs)
|
|
243
|
+
? Math.max(0, nowMs - observedMs)
|
|
244
|
+
: Infinity;
|
|
245
|
+
if (accountAge > STALE_SKIP_MS) {
|
|
246
|
+
skipped.push({
|
|
247
|
+
...base,
|
|
248
|
+
reason: "no reading in the last 24 hours",
|
|
249
|
+
fix: "hq capacity doctor",
|
|
250
|
+
});
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const applicable = (account.limits ?? []).filter((l) => limitApplies(providerId, l, model));
|
|
254
|
+
const viewRows = applicable.map((limit) => {
|
|
255
|
+
const resetMs = limit.resetsAt ? Date.parse(limit.resetsAt) : NaN;
|
|
256
|
+
const resetPassed = limit.state === "reset-pending"
|
|
257
|
+
|| (Number.isFinite(resetMs) && resetMs <= nowMs);
|
|
258
|
+
const observedLimit = Date.parse(limit.observedAt || account.observedAt || "");
|
|
259
|
+
const readAge = Number.isFinite(observedLimit)
|
|
260
|
+
? Math.max(0, nowMs - observedLimit)
|
|
261
|
+
: Infinity;
|
|
262
|
+
if (resetPassed) {
|
|
263
|
+
return { limit, used: 0, readAge, resetPassed: true };
|
|
264
|
+
}
|
|
265
|
+
if (readAge > LIMIT_STALE_MS) {
|
|
266
|
+
return { limit, used: null, readAge, resetPassed: false };
|
|
267
|
+
}
|
|
268
|
+
const used = typeof limit.usedPercent === "number" ? limit.usedPercent : null;
|
|
269
|
+
return { limit, used, readAge, resetPassed: false };
|
|
270
|
+
});
|
|
271
|
+
if (tokenExpired) {
|
|
272
|
+
const readingTimes = applicable
|
|
273
|
+
.map((limit) => Date.parse(limit.observedAt || ""))
|
|
274
|
+
.filter((ms) => Number.isFinite(ms));
|
|
275
|
+
const newestReadingMs = readingTimes.length > 0 ? Math.max(...readingTimes) : -Infinity;
|
|
276
|
+
const accountObservedMs = account.observedAt ? Date.parse(account.observedAt) : NaN;
|
|
277
|
+
const noFreshReading = !Number.isFinite(newestReadingMs)
|
|
278
|
+
|| (Number.isFinite(accountObservedMs) && newestReadingMs < accountObservedMs);
|
|
279
|
+
if (noFreshReading) {
|
|
280
|
+
skipped.push({
|
|
281
|
+
...base,
|
|
282
|
+
reason: "no fresh reading (sign-in expired)",
|
|
283
|
+
fix: account.fixCommand ?? null,
|
|
284
|
+
});
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const exhausted = viewRows.find((row) => !row.resetPassed
|
|
289
|
+
&& row.readAge <= LIMIT_STALE_MS
|
|
290
|
+
&& (row.limit.state === "exhausted" || (typeof row.used === "number" && row.used >= 100)));
|
|
291
|
+
if (exhausted) {
|
|
292
|
+
skipped.push({
|
|
293
|
+
...base,
|
|
294
|
+
reason: `${labelLimit(exhausted.limit)} used up`,
|
|
295
|
+
resets_at: exhausted.limit.resetsAt ?? null,
|
|
296
|
+
});
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
const known = viewRows.filter((row) => typeof row.used === "number");
|
|
300
|
+
if (known.length === 0) {
|
|
301
|
+
skipped.push({
|
|
302
|
+
...base,
|
|
303
|
+
reason: "no usage numbers for this account in the last day",
|
|
304
|
+
fix: tokenExpired ? (account.fixCommand ?? null) : "hq capacity doctor",
|
|
305
|
+
});
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const oldest = Math.max(0, ...known.map((row) => limitAgeMs(row, nowMs)));
|
|
309
|
+
const oldReading = oldest > FRESH_MS;
|
|
310
|
+
let binding = known[0];
|
|
311
|
+
for (const row of known)
|
|
312
|
+
if (row.used > binding.used)
|
|
313
|
+
binding = row;
|
|
314
|
+
const headroom = Math.max(0, 100 - binding.used);
|
|
315
|
+
if (headroom < NEARLY_OUT) {
|
|
316
|
+
skipped.push({
|
|
317
|
+
...base,
|
|
318
|
+
reason: `${labelLimit(binding.limit)} nearly used up`,
|
|
319
|
+
resets_at: binding.limit.resetsAt ?? null,
|
|
320
|
+
});
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const live = liveLaneCount(lanes, providerId, laneMatch.id);
|
|
324
|
+
const weekly = applicable.find((l) => l.kind === "weekly_all" || (providerId === "grok" && l.kind === "weekly_scoped"));
|
|
325
|
+
const weeklyResetMs = weekly?.resetsAt ? Date.parse(weekly.resetsAt) : NaN;
|
|
326
|
+
const resetMs = Number.isFinite(weeklyResetMs) ? weeklyResetMs - nowMs : Infinity;
|
|
327
|
+
const resetSoon = resetMs > 0 && resetMs <= RESET_SOON_MS && headroom >= 30;
|
|
328
|
+
const score = headroom
|
|
329
|
+
- live * LANE_PENALTY
|
|
330
|
+
+ (resetSoon ? RESET_SOON_BONUS : 0)
|
|
331
|
+
- (oldReading ? OLD_READING_PENALTY : 0);
|
|
332
|
+
const whyParts = [
|
|
333
|
+
binding.used === 0
|
|
334
|
+
? "nothing used yet on any limit"
|
|
335
|
+
: `${headroom}% left on ${labelLimit(binding.limit)}`,
|
|
336
|
+
];
|
|
337
|
+
if (live)
|
|
338
|
+
whyParts.push(`${live} live lane${live === 1 ? "" : "s"} already on it`);
|
|
339
|
+
if (resetSoon)
|
|
340
|
+
whyParts.push("weekly resets within a day, so unused room is lost");
|
|
341
|
+
if (oldReading)
|
|
342
|
+
whyParts.push(`last reading ${Math.round(oldest / 36e5)}h old`);
|
|
343
|
+
if (tokenExpired)
|
|
344
|
+
whyParts.push("sign-in renews when the lane starts");
|
|
345
|
+
ranked.push({
|
|
346
|
+
provider: providerId,
|
|
347
|
+
account: laneMatch.id,
|
|
348
|
+
label,
|
|
349
|
+
organization: account.organization ?? null,
|
|
350
|
+
headroom,
|
|
351
|
+
score,
|
|
352
|
+
live_lanes: live,
|
|
353
|
+
reset_soon: resetSoon,
|
|
354
|
+
token_expired: tokenExpired,
|
|
355
|
+
reading_age_h: Math.round(oldest / 36e5),
|
|
356
|
+
binding: {
|
|
357
|
+
limit: labelLimit(binding.limit),
|
|
358
|
+
used_percent: binding.used,
|
|
359
|
+
resets_at: binding.limit.resetsAt ?? null,
|
|
360
|
+
},
|
|
361
|
+
weekly_resets_at: weekly?.resetsAt ?? null,
|
|
362
|
+
why: whyParts.join("; "),
|
|
363
|
+
lane_flags: `--provider ${providerId} --account ${laneMatch.id}`,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
ranked.sort((a, b) => b.score - a.score
|
|
368
|
+
|| b.headroom - a.headroom
|
|
369
|
+
|| a.reading_age_h - b.reading_age_h
|
|
370
|
+
|| a.provider.localeCompare(b.provider));
|
|
371
|
+
let pick = null;
|
|
372
|
+
for (const p of prefer) {
|
|
373
|
+
if (!requested.includes(p))
|
|
374
|
+
continue;
|
|
375
|
+
const best = ranked.find((r) => r.provider === p);
|
|
376
|
+
if (best && best.headroom >= floor) {
|
|
377
|
+
pick = best;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (!pick && ranked.length > 0)
|
|
382
|
+
pick = ranked[0];
|
|
383
|
+
const bad = new Map();
|
|
384
|
+
for (const row of skipped) {
|
|
385
|
+
if (row.account)
|
|
386
|
+
bad.set(`${row.provider}:${row.account}`, row.reason);
|
|
387
|
+
}
|
|
388
|
+
for (const row of ranked) {
|
|
389
|
+
if (row.headroom < floor) {
|
|
390
|
+
bad.set(`${row.provider}:${row.account}`, `${row.binding.limit} has ${row.headroom}% left`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const moves = [];
|
|
394
|
+
for (const lane of lanes) {
|
|
395
|
+
if (!LIVE_STATES.has(lane.state))
|
|
396
|
+
continue;
|
|
397
|
+
const providerId = lane.provider;
|
|
398
|
+
if (!isLaneProvider(providerId))
|
|
399
|
+
continue;
|
|
400
|
+
if (!lane.account)
|
|
401
|
+
continue;
|
|
402
|
+
const key = `${providerId}:${lane.account}`;
|
|
403
|
+
if (!bad.has(key))
|
|
404
|
+
continue;
|
|
405
|
+
const to = ranked.find((r) => r.provider === providerId && r.account !== lane.account && r.headroom >= floor);
|
|
406
|
+
moves.push({
|
|
407
|
+
lane_id: lane.lane_id,
|
|
408
|
+
provider: providerId,
|
|
409
|
+
from: lane.account,
|
|
410
|
+
reason: bad.get(key),
|
|
411
|
+
to: to?.account ?? null,
|
|
412
|
+
command: to
|
|
413
|
+
? `hq lanes message ${lane.lane_id} --account ${to.account} --text "Continue on account ${to.account}; ${lane.account} is out of room."`
|
|
414
|
+
: null,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
const soonest = skipped
|
|
418
|
+
.map((row) => row.resets_at ?? null)
|
|
419
|
+
.filter((v) => !!v)
|
|
420
|
+
.sort()[0] ?? null;
|
|
421
|
+
return {
|
|
422
|
+
ok: !!pick,
|
|
423
|
+
at: nowDate.toISOString(),
|
|
424
|
+
request: {
|
|
425
|
+
providers: requested,
|
|
426
|
+
model: model || null,
|
|
427
|
+
prefer,
|
|
428
|
+
floor,
|
|
429
|
+
},
|
|
430
|
+
pick,
|
|
431
|
+
ranked,
|
|
432
|
+
skipped,
|
|
433
|
+
moves,
|
|
434
|
+
next_reset: pick ? null : soonest,
|
|
435
|
+
capacity: {
|
|
436
|
+
generated_at: input.view.generatedAt ?? null,
|
|
437
|
+
stale: !!input.capacityStale,
|
|
438
|
+
collector_enabled: input.collectorEnabled ?? null,
|
|
439
|
+
},
|
|
440
|
+
lanes_error: input.lanesError ?? null,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/** One-line text output used by `--text` and by `--account auto` admission messages. */
|
|
444
|
+
export function formatRouteText(result) {
|
|
445
|
+
if (result.pick) {
|
|
446
|
+
const org = result.pick.organization || result.pick.label;
|
|
447
|
+
return `${result.pick.provider} ${result.pick.account} (${org}): ${result.pick.why}. Use ${result.pick.lane_flags}`;
|
|
448
|
+
}
|
|
449
|
+
const soonest = result.next_reset;
|
|
450
|
+
return `No account has room${soonest ? `; next reset ${soonest}` : ""}.`;
|
|
451
|
+
}
|
|
452
|
+
//# sourceMappingURL=route.js.map
|