@indigoai-us/hq-cli 5.187.0 → 5.188.1
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/command-catalog.generated.d.ts +65 -0
- package/dist/command-catalog.generated.js +85 -0
- package/dist/commands/lanes.d.ts +7 -1
- package/dist/commands/lanes.js +894 -45
- package/dist/lib/lanes/claude-account-balance.d.ts +82 -0
- package/dist/lib/lanes/claude-account-balance.js +253 -0
- package/dist/lib/lanes/hire-grant-dispatch.d.ts +21 -0
- package/dist/lib/lanes/hire-grant-dispatch.js +167 -0
- package/dist/lib/lanes/hire-grants.d.ts +3 -2
- package/dist/lib/lanes/host-capacity.d.ts +19 -0
- package/dist/lib/lanes/host-capacity.js +68 -0
- package/dist/lib/lanes/index.d.ts +1 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +11 -1
- package/dist/lib/lanes/outpost-dispatch.js +20 -0
- package/dist/lib/lanes/ssm-command-context.d.ts +12 -0
- package/dist/lib/lanes/ssm-command-context.js +52 -0
- package/dist/lib/lanes/store.js +86 -0
- package/dist/lib/lanes/types.d.ts +48 -0
- package/dist/lib/lanes/types.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { HostCapacityAccount, HostCapacitySnapshot } from "./host-capacity.js";
|
|
2
|
+
import type { LaneHost, LaneRecord, LanesConfig } from "./types.js";
|
|
3
|
+
export declare const CLAUDE_ACCOUNT_BALANCE_FLAG = "lanes.claude-account-balance";
|
|
4
|
+
export declare const BALANCE_LIVE_STATES: readonly ["running", "queued", "awaiting_input"];
|
|
5
|
+
export declare const BALANCE_MOVABLE_STATES: readonly ["interrupted", "done", "awaiting_input"];
|
|
6
|
+
export interface BalanceHostSnapshot {
|
|
7
|
+
capacity: HostCapacitySnapshot;
|
|
8
|
+
lanes: readonly LaneRecord[];
|
|
9
|
+
/** False means the lane list could not be read; capacity counts may be partial. */
|
|
10
|
+
laneListAvailable: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface BalanceHostGap {
|
|
13
|
+
host: LaneHost | {
|
|
14
|
+
kind: "unknown";
|
|
15
|
+
id: "unknown";
|
|
16
|
+
};
|
|
17
|
+
reason: string;
|
|
18
|
+
}
|
|
19
|
+
export interface AccountHostLoad {
|
|
20
|
+
host: LaneHost;
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
logged_in: boolean | null;
|
|
23
|
+
live_lanes: number | null;
|
|
24
|
+
lane_counts: {
|
|
25
|
+
running: number | null;
|
|
26
|
+
queued: number | null;
|
|
27
|
+
awaiting_input: number | null;
|
|
28
|
+
};
|
|
29
|
+
count_source: "lane_list" | "capacity" | "capacity_partial" | "unavailable";
|
|
30
|
+
}
|
|
31
|
+
export interface ClaudeAccountLoad {
|
|
32
|
+
account: string;
|
|
33
|
+
target_enabled: boolean;
|
|
34
|
+
target_logged_in: boolean | null;
|
|
35
|
+
eligible: boolean;
|
|
36
|
+
ineligible_reason?: string;
|
|
37
|
+
live_lanes: number;
|
|
38
|
+
counts_complete: boolean;
|
|
39
|
+
usage: NonNullable<HostCapacityAccount["usage"]> | null;
|
|
40
|
+
hosts: AccountHostLoad[];
|
|
41
|
+
}
|
|
42
|
+
export interface ClaudeAccountSelection {
|
|
43
|
+
account: string;
|
|
44
|
+
signal: "usage" | "lane_count" | "explicit";
|
|
45
|
+
reason: "least_loaded" | "explicit";
|
|
46
|
+
target_host: LaneHost;
|
|
47
|
+
considered: ClaudeAccountLoad[];
|
|
48
|
+
skipped_accounts: Array<{
|
|
49
|
+
account: string;
|
|
50
|
+
host: LaneHost;
|
|
51
|
+
reason: string;
|
|
52
|
+
}>;
|
|
53
|
+
unavailable_hosts: BalanceHostGap[];
|
|
54
|
+
}
|
|
55
|
+
export interface BalanceMoveSuggestion {
|
|
56
|
+
lane_id: string;
|
|
57
|
+
host: LaneHost;
|
|
58
|
+
state: LaneRecord["state"];
|
|
59
|
+
from: string;
|
|
60
|
+
to: string;
|
|
61
|
+
reason: "account_load" | "source_unavailable";
|
|
62
|
+
apply: boolean;
|
|
63
|
+
skip_reason?: "running_turn" | "state_not_movable" | "no_provider_session" | "pending_resume";
|
|
64
|
+
}
|
|
65
|
+
export declare function selectClaudeAccount(input: {
|
|
66
|
+
hosts: readonly BalanceHostSnapshot[];
|
|
67
|
+
targetHost: LaneHost;
|
|
68
|
+
config: LanesConfig;
|
|
69
|
+
allowedAccounts?: readonly string[];
|
|
70
|
+
explicitAccount?: string;
|
|
71
|
+
unavailableHosts?: readonly BalanceHostGap[];
|
|
72
|
+
}): ClaudeAccountSelection;
|
|
73
|
+
export declare function accountLoadsForReport(input: {
|
|
74
|
+
hosts: readonly BalanceHostSnapshot[];
|
|
75
|
+
config: LanesConfig;
|
|
76
|
+
}): ClaudeAccountLoad[];
|
|
77
|
+
export declare function planClaudeBalanceMoves(input: {
|
|
78
|
+
hosts: readonly BalanceHostSnapshot[];
|
|
79
|
+
config: LanesConfig;
|
|
80
|
+
}): BalanceMoveSuggestion[];
|
|
81
|
+
export declare function assertBalanceApplyEnabled(enabled: boolean): void;
|
|
82
|
+
//# sourceMappingURL=claude-account-balance.d.ts.map
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { LanesError } from "./errors.js";
|
|
2
|
+
export const CLAUDE_ACCOUNT_BALANCE_FLAG = "lanes.claude-account-balance";
|
|
3
|
+
export const BALANCE_LIVE_STATES = ["running", "queued", "awaiting_input"];
|
|
4
|
+
export const BALANCE_MOVABLE_STATES = ["interrupted", "done", "awaiting_input"];
|
|
5
|
+
function sameHost(a, b) {
|
|
6
|
+
return a.kind === b.kind && a.id === b.id;
|
|
7
|
+
}
|
|
8
|
+
function isBalanceLive(lane) {
|
|
9
|
+
return BALANCE_LIVE_STATES.includes(lane.state);
|
|
10
|
+
}
|
|
11
|
+
function countLaneStates(lanes, account) {
|
|
12
|
+
const rows = lanes.filter((lane) => lane.provider === "claude" && lane.account === account && isBalanceLive(lane));
|
|
13
|
+
const running = rows.filter((lane) => lane.state === "running").length;
|
|
14
|
+
const queued = rows.filter((lane) => lane.state === "queued").length;
|
|
15
|
+
const awaiting_input = rows.filter((lane) => lane.state === "awaiting_input").length;
|
|
16
|
+
return { running, queued, awaiting_input, live: running + queued + awaiting_input };
|
|
17
|
+
}
|
|
18
|
+
function accountHostLoad(host, accountId) {
|
|
19
|
+
const hostCapacity = host.capacity.providers.claude.accounts[accountId];
|
|
20
|
+
if (!hostCapacity) {
|
|
21
|
+
return {
|
|
22
|
+
host: host.capacity.host,
|
|
23
|
+
enabled: false,
|
|
24
|
+
logged_in: false,
|
|
25
|
+
live_lanes: host.laneListAvailable ? countLaneStates(host.lanes, accountId).live : null,
|
|
26
|
+
lane_counts: host.laneListAvailable
|
|
27
|
+
? countLaneStates(host.lanes, accountId)
|
|
28
|
+
: { running: null, queued: null, awaiting_input: null },
|
|
29
|
+
count_source: host.laneListAvailable ? "lane_list" : "unavailable",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (host.laneListAvailable) {
|
|
33
|
+
const counts = countLaneStates(host.lanes, accountId);
|
|
34
|
+
return {
|
|
35
|
+
host: host.capacity.host,
|
|
36
|
+
enabled: hostCapacity.enabled,
|
|
37
|
+
logged_in: hostCapacity.logged_in,
|
|
38
|
+
live_lanes: counts.live,
|
|
39
|
+
lane_counts: { running: counts.running, queued: counts.queued, awaiting_input: counts.awaiting_input },
|
|
40
|
+
count_source: "lane_list",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (hostCapacity.lane_counts !== undefined) {
|
|
44
|
+
return {
|
|
45
|
+
host: host.capacity.host,
|
|
46
|
+
enabled: hostCapacity.enabled,
|
|
47
|
+
logged_in: hostCapacity.logged_in,
|
|
48
|
+
live_lanes: hostCapacity.lane_counts.live,
|
|
49
|
+
lane_counts: {
|
|
50
|
+
running: hostCapacity.lane_counts.running,
|
|
51
|
+
queued: hostCapacity.lane_counts.queued,
|
|
52
|
+
awaiting_input: hostCapacity.lane_counts.awaiting_input,
|
|
53
|
+
},
|
|
54
|
+
count_source: "capacity",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
host: host.capacity.host,
|
|
59
|
+
enabled: hostCapacity.enabled,
|
|
60
|
+
logged_in: hostCapacity.logged_in,
|
|
61
|
+
live_lanes: hostCapacity.running + hostCapacity.queued,
|
|
62
|
+
lane_counts: { running: hostCapacity.running, queued: hostCapacity.queued, awaiting_input: null },
|
|
63
|
+
count_source: "capacity_partial",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function usageScore(usage) {
|
|
67
|
+
if (usage?.five_hour_percent === undefined || usage.weekly_percent === undefined)
|
|
68
|
+
return null;
|
|
69
|
+
return Math.max(usage.five_hour_percent, usage.weekly_percent);
|
|
70
|
+
}
|
|
71
|
+
function selectionSignal(loads) {
|
|
72
|
+
return loads.length > 0 && loads.every((load) => usageScore(load.usage) !== null)
|
|
73
|
+
? "usage"
|
|
74
|
+
: "lane_count";
|
|
75
|
+
}
|
|
76
|
+
function targetIneligibilityReason(accountId, globalEnabled, allowedAccounts, target) {
|
|
77
|
+
if (!globalEnabled || !target.enabled)
|
|
78
|
+
return "disabled";
|
|
79
|
+
if (allowedAccounts !== undefined && !allowedAccounts.includes(accountId))
|
|
80
|
+
return "not_allowed_by_lane";
|
|
81
|
+
if (target.logged_in === false)
|
|
82
|
+
return "not_logged_in";
|
|
83
|
+
if (target.logged_in === null)
|
|
84
|
+
return "login_status_unknown";
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
function buildLoads(input) {
|
|
88
|
+
const configAccounts = input.config.providers.claude.accounts;
|
|
89
|
+
const targetHost = input.target.capacity.host;
|
|
90
|
+
const skipped = [];
|
|
91
|
+
const loads = Object.keys(configAccounts).sort().map((account) => {
|
|
92
|
+
const hosts = input.hosts.map((host) => accountHostLoad(host, account));
|
|
93
|
+
const targetLoad = hosts.find((host) => sameHost(host.host, targetHost));
|
|
94
|
+
const ineligible = targetIneligibilityReason(account, configAccounts[account]?.enabled === true, input.allowedAccounts, targetLoad);
|
|
95
|
+
if (ineligible !== undefined)
|
|
96
|
+
skipped.push({ account, host: targetHost, reason: ineligible });
|
|
97
|
+
const knownCounts = hosts.flatMap((host) => host.live_lanes === null ? [] : [host.live_lanes]);
|
|
98
|
+
return {
|
|
99
|
+
account,
|
|
100
|
+
target_enabled: targetLoad.enabled && configAccounts[account]?.enabled === true,
|
|
101
|
+
target_logged_in: targetLoad.logged_in,
|
|
102
|
+
eligible: ineligible === undefined,
|
|
103
|
+
...(ineligible !== undefined ? { ineligible_reason: ineligible } : {}),
|
|
104
|
+
live_lanes: knownCounts.reduce((sum, count) => sum + count, 0),
|
|
105
|
+
counts_complete: hosts.every((host) => host.live_lanes !== null && host.count_source !== "capacity_partial"),
|
|
106
|
+
usage: input.target.capacity.providers.claude.accounts[account]?.usage ?? null,
|
|
107
|
+
hosts,
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
return { loads, skipped };
|
|
111
|
+
}
|
|
112
|
+
export function selectClaudeAccount(input) {
|
|
113
|
+
const target = input.hosts.find((host) => sameHost(host.capacity.host, input.targetHost));
|
|
114
|
+
if (!target) {
|
|
115
|
+
throw new LanesError("claude_account_balance_target_host_unavailable", `Cannot verify Claude accounts on target host ${input.targetHost.kind}:${input.targetHost.id}`, {
|
|
116
|
+
target_host: input.targetHost,
|
|
117
|
+
unavailable_hosts: input.unavailableHosts ?? [],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const { loads, skipped } = buildLoads({ ...input, target });
|
|
121
|
+
if (input.explicitAccount !== undefined) {
|
|
122
|
+
const selected = loads.find((load) => load.account === input.explicitAccount);
|
|
123
|
+
if (!selected || !selected.eligible) {
|
|
124
|
+
throw new LanesError("claude_account_balance_account_unavailable", `Explicit Claude account ${JSON.stringify(input.explicitAccount)} is unavailable on ${input.targetHost.kind}:${input.targetHost.id}`, {
|
|
125
|
+
account: input.explicitAccount,
|
|
126
|
+
target_host: input.targetHost,
|
|
127
|
+
reason: selected?.ineligible_reason ?? "unknown_account",
|
|
128
|
+
account_reasons: skipped,
|
|
129
|
+
unavailable_hosts: input.unavailableHosts ?? [],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
account: input.explicitAccount,
|
|
134
|
+
signal: "explicit",
|
|
135
|
+
reason: "explicit",
|
|
136
|
+
target_host: input.targetHost,
|
|
137
|
+
considered: loads,
|
|
138
|
+
skipped_accounts: skipped,
|
|
139
|
+
unavailable_hosts: [...(input.unavailableHosts ?? [])],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const eligible = loads.filter((load) => load.eligible);
|
|
143
|
+
if (eligible.length === 0) {
|
|
144
|
+
const unknownAccounts = loads
|
|
145
|
+
.filter((load) => load.ineligible_reason === "login_status_unknown")
|
|
146
|
+
.map((load) => load.account);
|
|
147
|
+
const unknownLoginDetail = unknownAccounts.length > 0
|
|
148
|
+
? `; login status is unknown for ${unknownAccounts.join(", ")}`
|
|
149
|
+
: "";
|
|
150
|
+
throw new LanesError("claude_account_balance_no_eligible_account", `No enabled, logged-in Claude account is available on ${input.targetHost.kind}:${input.targetHost.id}${unknownLoginDetail}`, {
|
|
151
|
+
provider: "claude",
|
|
152
|
+
target_host: input.targetHost,
|
|
153
|
+
account_reasons: loads.map((load) => ({
|
|
154
|
+
account: load.account,
|
|
155
|
+
reason: load.ineligible_reason ?? "unavailable",
|
|
156
|
+
})),
|
|
157
|
+
unavailable_hosts: input.unavailableHosts ?? [],
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const signal = selectionSignal(eligible);
|
|
161
|
+
eligible.sort((a, b) => {
|
|
162
|
+
if (signal === "usage") {
|
|
163
|
+
const byUsage = usageScore(a.usage) - usageScore(b.usage);
|
|
164
|
+
if (byUsage !== 0)
|
|
165
|
+
return byUsage;
|
|
166
|
+
}
|
|
167
|
+
return a.live_lanes - b.live_lanes || a.account.localeCompare(b.account);
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
account: eligible[0].account,
|
|
171
|
+
signal,
|
|
172
|
+
reason: "least_loaded",
|
|
173
|
+
target_host: input.targetHost,
|
|
174
|
+
considered: loads,
|
|
175
|
+
skipped_accounts: skipped,
|
|
176
|
+
unavailable_hosts: [...(input.unavailableHosts ?? [])],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export function accountLoadsForReport(input) {
|
|
180
|
+
const first = input.hosts[0];
|
|
181
|
+
if (!first)
|
|
182
|
+
return [];
|
|
183
|
+
return buildLoads({ hosts: input.hosts, target: first, config: input.config }).loads.map((load) => ({
|
|
184
|
+
...load,
|
|
185
|
+
eligible: false,
|
|
186
|
+
ineligible_reason: undefined,
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
export function planClaudeBalanceMoves(input) {
|
|
190
|
+
const loads = accountLoadsForReport(input);
|
|
191
|
+
const projected = new Map(loads.map((load) => [load.account, load.live_lanes]));
|
|
192
|
+
const lanes = input.hosts.flatMap((host) => host.lanes
|
|
193
|
+
.filter((lane) => lane.provider === "claude" && lane.account && (isBalanceLive(lane) || BALANCE_MOVABLE_STATES.includes(lane.state)))
|
|
194
|
+
.map((lane) => ({ lane, host: host.capacity.host })))
|
|
195
|
+
.sort((a, b) => a.lane.created_at.localeCompare(b.lane.created_at) || a.lane.lane_id.localeCompare(b.lane.lane_id));
|
|
196
|
+
const suggestions = [];
|
|
197
|
+
for (const { lane, host } of lanes) {
|
|
198
|
+
const source = lane.account;
|
|
199
|
+
const sourceLoad = projected.get(source) ?? 0;
|
|
200
|
+
const hostSnapshot = input.hosts.find((row) => sameHost(row.capacity.host, host));
|
|
201
|
+
if (!hostSnapshot)
|
|
202
|
+
continue;
|
|
203
|
+
const candidates = Object.keys(input.config.providers.claude.accounts)
|
|
204
|
+
.filter((account) => account !== source && input.config.providers.claude.accounts[account]?.enabled === true)
|
|
205
|
+
.map((account) => ({
|
|
206
|
+
account,
|
|
207
|
+
targetStatus: hostSnapshot.capacity.providers.claude.accounts[account],
|
|
208
|
+
load: projected.get(account) ?? 0,
|
|
209
|
+
}))
|
|
210
|
+
.filter((candidate) => candidate.targetStatus?.enabled === true && candidate.targetStatus.logged_in === true)
|
|
211
|
+
.sort((a, b) => a.load - b.load || a.account.localeCompare(b.account));
|
|
212
|
+
const sourceStatus = hostSnapshot.capacity.providers.claude.accounts[source];
|
|
213
|
+
const sourceUnavailable = sourceStatus?.enabled !== true || sourceStatus.logged_in !== true;
|
|
214
|
+
const target = candidates[0];
|
|
215
|
+
const laneIsLive = isBalanceLive(lane);
|
|
216
|
+
if (!target || (!sourceUnavailable && (!laneIsLive || target.load >= sourceLoad)))
|
|
217
|
+
continue;
|
|
218
|
+
const movable = BALANCE_MOVABLE_STATES.includes(lane.state);
|
|
219
|
+
const sessionId = lane.provider_session_id ?? lane.last_resume?.session_id;
|
|
220
|
+
const skipReason = lane.state === "running"
|
|
221
|
+
? "running_turn"
|
|
222
|
+
: !movable
|
|
223
|
+
? "state_not_movable"
|
|
224
|
+
: lane.pending_resume !== undefined
|
|
225
|
+
? "pending_resume"
|
|
226
|
+
: typeof sessionId !== "string" || sessionId.trim() === ""
|
|
227
|
+
? "no_provider_session"
|
|
228
|
+
: undefined;
|
|
229
|
+
suggestions.push({
|
|
230
|
+
lane_id: lane.lane_id,
|
|
231
|
+
host,
|
|
232
|
+
state: lane.state,
|
|
233
|
+
from: source,
|
|
234
|
+
to: target.account,
|
|
235
|
+
reason: sourceUnavailable ? "source_unavailable" : "account_load",
|
|
236
|
+
apply: skipReason === undefined,
|
|
237
|
+
...(skipReason !== undefined ? { skip_reason: skipReason } : {}),
|
|
238
|
+
});
|
|
239
|
+
if (skipReason === undefined) {
|
|
240
|
+
if (laneIsLive) {
|
|
241
|
+
projected.set(source, Math.max(0, sourceLoad - 1));
|
|
242
|
+
projected.set(target.account, target.load + 1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return suggestions;
|
|
247
|
+
}
|
|
248
|
+
export function assertBalanceApplyEnabled(enabled) {
|
|
249
|
+
if (!enabled) {
|
|
250
|
+
throw new LanesError("claude_account_balance_disabled", `Applying Claude account balance moves requires hq-flags gate ${CLAUDE_ACCOUNT_BALANCE_FLAG}`, { flag: CLAUDE_ACCOUNT_BALANCE_FLAG });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=claude-account-balance.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type LaneRecord, type LaneState, type SeniorRef } from "./types.js";
|
|
2
|
+
declare const HIRE_GRANT_CONTEXT_SCHEMA: "hq-lane-hire-grant-context.v1";
|
|
3
|
+
export interface HireGrantLaneSnapshot {
|
|
4
|
+
lane_id: string;
|
|
5
|
+
state: LaneState;
|
|
6
|
+
senior: SeniorRef;
|
|
7
|
+
hire_grant?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface OutpostHireGrantContext {
|
|
10
|
+
schema_version: typeof HIRE_GRANT_CONTEXT_SCHEMA;
|
|
11
|
+
enforcement_enabled: boolean;
|
|
12
|
+
expected_senior: SeniorRef;
|
|
13
|
+
hierarchy: HireGrantLaneSnapshot[];
|
|
14
|
+
}
|
|
15
|
+
/** Encode only the owner-relevant hierarchy from the primary's complete store. */
|
|
16
|
+
export declare function createOutpostHireGrantContext(target: LaneRecord, lanes: readonly LaneRecord[], enforcementEnabled: boolean): string;
|
|
17
|
+
/** Decode and validate an owner grant context, binding it to the target lane. */
|
|
18
|
+
export declare function parseOutpostHireGrantContext(encoded: string, targetLaneId: string): OutpostHireGrantContext;
|
|
19
|
+
export declare function sameHireGrantSenior(a: SeniorRef, b: SeniorRef): boolean;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=hire-grant-dispatch.d.ts.map
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { aliveDirectChildren } from "./hire-grants.js";
|
|
2
|
+
import { LanesError } from "./errors.js";
|
|
3
|
+
import { isLaneState, isSeniorKind, } from "./types.js";
|
|
4
|
+
const HIRE_GRANT_CONTEXT_SCHEMA = "hq-lane-hire-grant-context.v1";
|
|
5
|
+
const MAX_CONTEXT_BYTES = 48 * 1024;
|
|
6
|
+
const MAX_CONTEXT_LANES = 256;
|
|
7
|
+
function contextError(message) {
|
|
8
|
+
return new LanesError("hire_grant_context_invalid", message, {});
|
|
9
|
+
}
|
|
10
|
+
function sameSenior(a, b) {
|
|
11
|
+
return a.kind === b.kind && a.id === b.id;
|
|
12
|
+
}
|
|
13
|
+
function copySenior(senior) {
|
|
14
|
+
return senior.id === undefined
|
|
15
|
+
? { kind: senior.kind }
|
|
16
|
+
: { kind: senior.kind, id: senior.id };
|
|
17
|
+
}
|
|
18
|
+
function snapshot(lane) {
|
|
19
|
+
return {
|
|
20
|
+
lane_id: lane.lane_id,
|
|
21
|
+
state: lane.state,
|
|
22
|
+
senior: copySenior(lane.senior),
|
|
23
|
+
...(lane.hire_grant === undefined ? {} : { hire_grant: lane.hire_grant }),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Encode only the owner-relevant hierarchy from the primary's complete store. */
|
|
27
|
+
export function createOutpostHireGrantContext(target, lanes, enforcementEnabled) {
|
|
28
|
+
const current = lanes.find((lane) => lane.lane_id === target.lane_id);
|
|
29
|
+
if (!current || !sameSenior(current.senior, target.senior)) {
|
|
30
|
+
throw new LanesError("hire_grant_hierarchy_unavailable", `Cannot build an authoritative hire-grant hierarchy for lane ${target.lane_id}`, { lane_id: target.lane_id });
|
|
31
|
+
}
|
|
32
|
+
const relevant = new Map();
|
|
33
|
+
relevant.set(current.lane_id, current);
|
|
34
|
+
if (enforcementEnabled) {
|
|
35
|
+
if (current.senior.kind === "lane") {
|
|
36
|
+
const parentId = current.senior.id;
|
|
37
|
+
const parent = parentId ? lanes.find((lane) => lane.lane_id === parentId) : undefined;
|
|
38
|
+
if (!parent) {
|
|
39
|
+
throw new LanesError("hire_grant_senior_invalid", `Lane ${target.lane_id} has no senior lane in the primary hierarchy`, { lane_id: target.lane_id, senior_lane_id: parentId ?? null });
|
|
40
|
+
}
|
|
41
|
+
relevant.set(parent.lane_id, parent);
|
|
42
|
+
for (const sibling of aliveDirectChildren(lanes, parent.lane_id)) {
|
|
43
|
+
relevant.set(sibling.lane_id, sibling);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
for (const child of aliveDirectChildren(lanes, current.lane_id)) {
|
|
47
|
+
relevant.set(child.lane_id, child);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const hierarchy = [...relevant.values()]
|
|
51
|
+
.map(snapshot)
|
|
52
|
+
.sort((a, b) => a.lane_id.localeCompare(b.lane_id));
|
|
53
|
+
if (hierarchy.length > MAX_CONTEXT_LANES) {
|
|
54
|
+
throw new LanesError("hire_grant_hierarchy_too_large", `Lane ${target.lane_id} has too many related lanes to dispatch a bounded grant check`, { lane_id: target.lane_id, related_lane_count: hierarchy.length });
|
|
55
|
+
}
|
|
56
|
+
const context = {
|
|
57
|
+
schema_version: HIRE_GRANT_CONTEXT_SCHEMA,
|
|
58
|
+
enforcement_enabled: enforcementEnabled,
|
|
59
|
+
expected_senior: copySenior(current.senior),
|
|
60
|
+
hierarchy,
|
|
61
|
+
};
|
|
62
|
+
const encoded = Buffer.from(JSON.stringify(context), "utf8").toString("base64url");
|
|
63
|
+
if (Buffer.byteLength(encoded, "utf8") > MAX_CONTEXT_BYTES) {
|
|
64
|
+
throw new LanesError("hire_grant_hierarchy_too_large", `Lane ${target.lane_id} has too much related state to dispatch a bounded grant check`, { lane_id: target.lane_id, encoded_bytes: Buffer.byteLength(encoded, "utf8") });
|
|
65
|
+
}
|
|
66
|
+
return encoded;
|
|
67
|
+
}
|
|
68
|
+
function parseSenior(value) {
|
|
69
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
70
|
+
return null;
|
|
71
|
+
const record = value;
|
|
72
|
+
if (Object.keys(record).some((key) => key !== "kind" && key !== "id"))
|
|
73
|
+
return null;
|
|
74
|
+
if (!isSeniorKind(record.kind))
|
|
75
|
+
return null;
|
|
76
|
+
if (record.id !== undefined &&
|
|
77
|
+
(typeof record.id !== "string" || !record.id.trim() || record.id.trim() !== record.id))
|
|
78
|
+
return null;
|
|
79
|
+
if ((record.kind === "session" || record.kind === "lane") && record.id === undefined)
|
|
80
|
+
return null;
|
|
81
|
+
if (record.kind === "user" && record.id !== undefined)
|
|
82
|
+
return null;
|
|
83
|
+
return record.id === undefined
|
|
84
|
+
? { kind: record.kind }
|
|
85
|
+
: { kind: record.kind, id: record.id };
|
|
86
|
+
}
|
|
87
|
+
function parseLaneSnapshot(value) {
|
|
88
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
89
|
+
return null;
|
|
90
|
+
const record = value;
|
|
91
|
+
if (Object.keys(record).some((key) => !["lane_id", "state", "senior", "hire_grant"].includes(key)))
|
|
92
|
+
return null;
|
|
93
|
+
if (typeof record.lane_id !== "string" || !record.lane_id.trim())
|
|
94
|
+
return null;
|
|
95
|
+
if (!isLaneState(record.state))
|
|
96
|
+
return null;
|
|
97
|
+
const senior = parseSenior(record.senior);
|
|
98
|
+
if (!senior)
|
|
99
|
+
return null;
|
|
100
|
+
if (record.hire_grant !== undefined &&
|
|
101
|
+
(typeof record.hire_grant !== "number" || !Number.isSafeInteger(record.hire_grant) || record.hire_grant < 0)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
lane_id: record.lane_id,
|
|
106
|
+
state: record.state,
|
|
107
|
+
senior,
|
|
108
|
+
...(record.hire_grant === undefined ? {} : { hire_grant: record.hire_grant }),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/** Decode and validate an owner grant context, binding it to the target lane. */
|
|
112
|
+
export function parseOutpostHireGrantContext(encoded, targetLaneId) {
|
|
113
|
+
if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded) ||
|
|
114
|
+
Buffer.byteLength(encoded, "utf8") > MAX_CONTEXT_BYTES ||
|
|
115
|
+
Buffer.from(encoded, "base64url").toString("base64url") !== encoded) {
|
|
116
|
+
throw contextError("Outpost grant context is not canonical base64url");
|
|
117
|
+
}
|
|
118
|
+
let raw;
|
|
119
|
+
try {
|
|
120
|
+
const decoded = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(encoded, "base64url"));
|
|
121
|
+
raw = JSON.parse(decoded);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
throw contextError("Outpost grant context is not valid UTF-8 JSON");
|
|
125
|
+
}
|
|
126
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
127
|
+
throw contextError("Outpost grant context must be an object");
|
|
128
|
+
}
|
|
129
|
+
const record = raw;
|
|
130
|
+
if (Object.keys(record).some((key) => !["schema_version", "enforcement_enabled", "expected_senior", "hierarchy"].includes(key))) {
|
|
131
|
+
throw contextError("Outpost grant context contains unknown fields");
|
|
132
|
+
}
|
|
133
|
+
if (record.schema_version !== HIRE_GRANT_CONTEXT_SCHEMA ||
|
|
134
|
+
typeof record.enforcement_enabled !== "boolean" ||
|
|
135
|
+
!Array.isArray(record.hierarchy) ||
|
|
136
|
+
record.hierarchy.length === 0 || record.hierarchy.length > MAX_CONTEXT_LANES) {
|
|
137
|
+
throw contextError("Outpost grant context has an unsupported or incomplete shape");
|
|
138
|
+
}
|
|
139
|
+
const expectedSenior = parseSenior(record.expected_senior);
|
|
140
|
+
const hierarchy = record.hierarchy.map(parseLaneSnapshot);
|
|
141
|
+
if (!expectedSenior || hierarchy.some((lane) => lane === null)) {
|
|
142
|
+
throw contextError("Outpost grant context contains an invalid senior or lane snapshot");
|
|
143
|
+
}
|
|
144
|
+
const snapshots = hierarchy;
|
|
145
|
+
const byId = new Map(snapshots.map((lane) => [lane.lane_id, lane]));
|
|
146
|
+
if (byId.size !== snapshots.length) {
|
|
147
|
+
throw contextError("Outpost grant context contains duplicate lane snapshots");
|
|
148
|
+
}
|
|
149
|
+
const target = byId.get(targetLaneId);
|
|
150
|
+
if (!target || !sameSenior(target.senior, expectedSenior)) {
|
|
151
|
+
throw contextError("Outpost grant context does not match the target's expected senior");
|
|
152
|
+
}
|
|
153
|
+
if (record.enforcement_enabled && expectedSenior.kind === "lane" &&
|
|
154
|
+
(!expectedSenior.id || !byId.has(expectedSenior.id))) {
|
|
155
|
+
throw contextError("Outpost grant context is missing the target's senior lane");
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
schema_version: HIRE_GRANT_CONTEXT_SCHEMA,
|
|
159
|
+
enforcement_enabled: record.enforcement_enabled,
|
|
160
|
+
expected_senior: expectedSenior,
|
|
161
|
+
hierarchy: snapshots,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export function sameHireGrantSenior(a, b) {
|
|
165
|
+
return sameSenior(a, b);
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=hire-grant-dispatch.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type LaneRecord } from "./types.js";
|
|
2
2
|
export declare const LANE_HIRE_GRANTS_FLAG = "lanes.hire-grants";
|
|
3
|
-
export
|
|
4
|
-
export declare function
|
|
3
|
+
export type LaneHireGrantRow = Pick<LaneRecord, "lane_id" | "state" | "senior" | "hire_grant">;
|
|
4
|
+
export declare function aliveDirectChildren<T extends LaneHireGrantRow>(lanes: readonly T[], parentLaneId: string): T[];
|
|
5
|
+
export declare function assignedChildHireGrants(children: readonly LaneHireGrantRow[], exceptLaneId?: string): number;
|
|
5
6
|
export interface HireGrantAdmissionSnapshot {
|
|
6
7
|
parent: LaneRecord;
|
|
7
8
|
children: LaneRecord[];
|
|
@@ -5,6 +5,19 @@ export interface HostCapacityAccount {
|
|
|
5
5
|
max_running: number;
|
|
6
6
|
running: number;
|
|
7
7
|
queued: number;
|
|
8
|
+
/** Exact lane states used for Claude account balancing; absent on older Outposts. */
|
|
9
|
+
lane_counts?: {
|
|
10
|
+
running: number;
|
|
11
|
+
queued: number;
|
|
12
|
+
awaiting_input: number;
|
|
13
|
+
live: number;
|
|
14
|
+
};
|
|
15
|
+
/** Provider-reported usage, when a host can read a recent sample. */
|
|
16
|
+
usage?: {
|
|
17
|
+
five_hour_percent?: number;
|
|
18
|
+
weekly_percent?: number;
|
|
19
|
+
sampled_at?: string;
|
|
20
|
+
};
|
|
8
21
|
}
|
|
9
22
|
export interface HostCapacityProvider {
|
|
10
23
|
max_running: number;
|
|
@@ -30,6 +43,12 @@ export declare function buildHostCapacitySnapshot(input: {
|
|
|
30
43
|
lanes: readonly LaneRecord[];
|
|
31
44
|
config: LanesConfig;
|
|
32
45
|
existsSync: (path: string) => boolean;
|
|
46
|
+
probe?: (command: string, args: string[], env: NodeJS.ProcessEnv) => {
|
|
47
|
+
status: number | null;
|
|
48
|
+
stdout: string;
|
|
49
|
+
stderr: string;
|
|
50
|
+
};
|
|
51
|
+
env?: NodeJS.ProcessEnv;
|
|
33
52
|
now: Date;
|
|
34
53
|
}): HostCapacitySnapshot;
|
|
35
54
|
/** Reject malformed remote snapshots before they can influence host selection. */
|
|
@@ -21,13 +21,23 @@ export function buildHostCapacitySnapshot(input) {
|
|
|
21
21
|
for (const [id, account] of Object.entries(providerConfig.accounts)) {
|
|
22
22
|
const status = accountStatus(provider, id, input.config, {
|
|
23
23
|
existsSync: input.existsSync,
|
|
24
|
+
...(input.probe !== undefined ? { probe: input.probe } : {}),
|
|
25
|
+
...(input.env !== undefined ? { env: input.env } : {}),
|
|
24
26
|
});
|
|
27
|
+
const accountLanes = providerLanes.filter((lane) => lane.account === id);
|
|
28
|
+
const liveLanes = accountLanes.filter((lane) => lane.state === "running" || lane.state === "queued" || lane.state === "awaiting_input");
|
|
25
29
|
accounts[id] = {
|
|
26
30
|
enabled: account.enabled,
|
|
27
31
|
logged_in: status.logged_in,
|
|
28
32
|
max_running: accountMaxRunning(providerConfig, id),
|
|
29
33
|
running: running.filter((lane) => lane.account === id).length,
|
|
30
34
|
queued: queued.filter((lane) => lane.account === id).length,
|
|
35
|
+
lane_counts: {
|
|
36
|
+
running: liveLanes.filter((lane) => lane.state === "running").length,
|
|
37
|
+
queued: liveLanes.filter((lane) => lane.state === "queued").length,
|
|
38
|
+
awaiting_input: liveLanes.filter((lane) => lane.state === "awaiting_input").length,
|
|
39
|
+
live: liveLanes.length,
|
|
40
|
+
},
|
|
31
41
|
};
|
|
32
42
|
}
|
|
33
43
|
providers[provider] = {
|
|
@@ -101,6 +111,12 @@ export function parseHostCapacitySnapshot(raw) {
|
|
|
101
111
|
max_running: requireCount(account.max_running, `${provider}.${id}.max_running`, true),
|
|
102
112
|
running: requireCount(account.running, `${provider}.${id}.running`),
|
|
103
113
|
queued: requireCount(account.queued, `${provider}.${id}.queued`),
|
|
114
|
+
...(account.lane_counts !== undefined
|
|
115
|
+
? { lane_counts: parseLaneCounts(account.lane_counts, `${provider}.${id}.lane_counts`) }
|
|
116
|
+
: {}),
|
|
117
|
+
...(account.usage !== undefined
|
|
118
|
+
? { usage: parseUsage(account.usage, `${provider}.${id}.usage`) }
|
|
119
|
+
: {}),
|
|
104
120
|
};
|
|
105
121
|
}
|
|
106
122
|
providers[provider] = {
|
|
@@ -118,6 +134,58 @@ export function parseHostCapacitySnapshot(raw) {
|
|
|
118
134
|
providers,
|
|
119
135
|
};
|
|
120
136
|
}
|
|
137
|
+
function parseLaneCounts(value, field) {
|
|
138
|
+
const counts = objectValue(value);
|
|
139
|
+
if (counts === undefined) {
|
|
140
|
+
throw new LanesError("host_capacity_invalid", `Invalid host capacity field ${field}`, { field });
|
|
141
|
+
}
|
|
142
|
+
const running = requireCount(counts.running, `${field}.running`);
|
|
143
|
+
const queued = requireCount(counts.queued, `${field}.queued`);
|
|
144
|
+
const awaiting_input = requireCount(counts.awaiting_input, `${field}.awaiting_input`);
|
|
145
|
+
const live = requireCount(counts.live, `${field}.live`);
|
|
146
|
+
if (running + queued + awaiting_input !== live) {
|
|
147
|
+
throw new LanesError("host_capacity_invalid", `Inconsistent host capacity field ${field}`, {
|
|
148
|
+
field,
|
|
149
|
+
running,
|
|
150
|
+
queued,
|
|
151
|
+
awaiting_input,
|
|
152
|
+
live,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return { running, queued, awaiting_input, live };
|
|
156
|
+
}
|
|
157
|
+
function parseUsage(value, field) {
|
|
158
|
+
const usage = objectValue(value);
|
|
159
|
+
if (usage === undefined) {
|
|
160
|
+
throw new LanesError("host_capacity_invalid", `Invalid host capacity field ${field}`, { field });
|
|
161
|
+
}
|
|
162
|
+
const parsed = {};
|
|
163
|
+
for (const key of ["five_hour_percent", "weekly_percent"]) {
|
|
164
|
+
const percentage = usage[key];
|
|
165
|
+
if (percentage !== undefined) {
|
|
166
|
+
if (typeof percentage !== "number" || !Number.isFinite(percentage) || percentage < 0 || percentage > 100) {
|
|
167
|
+
throw new LanesError("host_capacity_invalid", `Invalid host capacity field ${field}.${key}`, {
|
|
168
|
+
field: `${field}.${key}`,
|
|
169
|
+
value: percentage ?? null,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
parsed[key] = percentage;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (Object.keys(parsed).length === 0) {
|
|
176
|
+
throw new LanesError("host_capacity_invalid", `Empty host capacity field ${field}`, { field });
|
|
177
|
+
}
|
|
178
|
+
if (usage.sampled_at !== undefined) {
|
|
179
|
+
if (typeof usage.sampled_at !== "string" || !Number.isFinite(Date.parse(usage.sampled_at))) {
|
|
180
|
+
throw new LanesError("host_capacity_invalid", `Invalid host capacity field ${field}.sampled_at`, {
|
|
181
|
+
field: `${field}.sampled_at`,
|
|
182
|
+
value: usage.sampled_at ?? null,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
parsed.sampled_at = usage.sampled_at;
|
|
186
|
+
}
|
|
187
|
+
return parsed;
|
|
188
|
+
}
|
|
121
189
|
/**
|
|
122
190
|
* Choose the least-loaded eligible host/account from advisory snapshots. The
|
|
123
191
|
* selected host still runs the shared admission check under its own lock.
|