@indigoai-us/hq-cli 5.187.0 → 5.188.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 +15 -0
- package/dist/command-catalog.generated.d.ts +43 -0
- package/dist/command-catalog.generated.js +56 -0
- package/dist/commands/lanes.d.ts +2 -0
- package/dist/commands/lanes.js +633 -10
- 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/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 +9 -0
- package/dist/lib/lanes/outpost-dispatch.js +19 -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
package/dist/commands/lanes.js
CHANGED
|
@@ -18,8 +18,9 @@ import { resolveProcessFlagGate } from "../lib/flag-registry.js";
|
|
|
18
18
|
import { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity, resolveAdmissionHost, } from "../lib/lanes/admission-host.js";
|
|
19
19
|
export { OUTPOST_DIR, OUTPOST_ID_PATH, readOutpostIdentity } from "../lib/lanes/admission-host.js";
|
|
20
20
|
export { LANE_HIRE_GRANTS_FLAG };
|
|
21
|
-
import { buildOutpostCapacityCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
21
|
+
import { buildOutpostCapacityCommand, buildOutpostBalanceMoveCommand, buildOutpostIdentityBackfillCommand, buildOutpostLaneCommand, buildOutpostMoveTargetCommand, buildOutpostPickupCommand, createOutpostsClient, dispatchErrorMessage, dispatchOutpostPickup, buildOutpostLaneListCommand, parseOutpostPickupPayload, stageOutpostPayload, } from "../lib/lanes/outpost-dispatch.js";
|
|
22
22
|
import { buildHostCapacitySnapshot, chooseHostForLane, parseHostCapacitySnapshot, } from "../lib/lanes/host-capacity.js";
|
|
23
|
+
import { BALANCE_MOVABLE_STATES, CLAUDE_ACCOUNT_BALANCE_FLAG, accountLoadsForReport, assertBalanceApplyEnabled, planClaudeBalanceMoves, selectClaudeAccount, } from "../lib/lanes/claude-account-balance.js";
|
|
23
24
|
import { parseUserYaml } from "../utils/user-yaml-error.js";
|
|
24
25
|
import { backfillOutpostIdentityMarker as backfillOutpostIdentityMarkerImpl, createOutpostIdentityMarkerIfMissing, validateOutpostIdentityId, } from "../lib/lanes/outpost-identity.js";
|
|
25
26
|
import { vaultApiFetch } from "../utils/vault-api.js";
|
|
@@ -53,6 +54,9 @@ export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
|
53
54
|
function defaultLanePlacementEnabled() {
|
|
54
55
|
return resolveProcessFlagGate(LANE_OUTPOST_LOAD_BALANCING_FLAG, { fallback: false }, () => false);
|
|
55
56
|
}
|
|
57
|
+
function defaultLaneClaudeAccountBalanceEnabled() {
|
|
58
|
+
return resolveProcessFlagGate(CLAUDE_ACCOUNT_BALANCE_FLAG, { fallback: false }, () => false);
|
|
59
|
+
}
|
|
56
60
|
function defaultLaneHireGrantsEnabled(company) {
|
|
57
61
|
return readLanesRegistryFlag(LANE_HIRE_GRANTS_FLAG, { company });
|
|
58
62
|
}
|
|
@@ -251,6 +255,7 @@ function resolveDeps(deps = {}) {
|
|
|
251
255
|
fetchText: deps.fetchText ?? defaultFetchText,
|
|
252
256
|
lanePlacementEnabled: deps.lanePlacementEnabled ?? defaultLanePlacementEnabled,
|
|
253
257
|
laneHireGrantsEnabled: deps.laneHireGrantsEnabled ?? defaultLaneHireGrantsEnabled,
|
|
258
|
+
laneClaudeAccountBalanceEnabled: deps.laneClaudeAccountBalanceEnabled ?? defaultLaneClaudeAccountBalanceEnabled,
|
|
254
259
|
};
|
|
255
260
|
}
|
|
256
261
|
function livenessDepsFromCtx(ctx) {
|
|
@@ -755,6 +760,198 @@ async function loadHostCapacitySnapshots(ctx, hqRoot, config = loadLanesConfig(h
|
|
|
755
760
|
skippedHosts: unavailable.map(({ outpost_id, error }) => ({ outpost_id, error })),
|
|
756
761
|
};
|
|
757
762
|
}
|
|
763
|
+
async function readOutpostLaneRecords(ctx, outpostId) {
|
|
764
|
+
let result;
|
|
765
|
+
try {
|
|
766
|
+
result = await ctx.dispatchOutpostExec({
|
|
767
|
+
outpostId,
|
|
768
|
+
command: buildOutpostLaneListCommand(),
|
|
769
|
+
timeoutSeconds: CAPACITY_PROBE_TIMEOUT_MS / 1000,
|
|
770
|
+
waitMs: CAPACITY_PROBE_TIMEOUT_MS,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
catch (err) {
|
|
774
|
+
throw new LanesError("outpost_lane_list_unavailable", `Could not read lanes from Outpost ${outpostId}: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId });
|
|
775
|
+
}
|
|
776
|
+
if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
|
|
777
|
+
throw new LanesError("outpost_lane_list_unavailable", `Outpost ${outpostId} did not return a successful lane list`, { outpost_id: outpostId, status: result.status, done: result.done ?? false, exit_code: result.exitCode });
|
|
778
|
+
}
|
|
779
|
+
let body;
|
|
780
|
+
try {
|
|
781
|
+
body = JSON.parse(result.stdout);
|
|
782
|
+
}
|
|
783
|
+
catch (err) {
|
|
784
|
+
throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} returned invalid lane-list JSON: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId });
|
|
785
|
+
}
|
|
786
|
+
if (body === null || typeof body !== "object" || Array.isArray(body) ||
|
|
787
|
+
body.ok !== true ||
|
|
788
|
+
body.action !== "list" ||
|
|
789
|
+
!Array.isArray(body.lanes)) {
|
|
790
|
+
throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} returned a malformed lane list`, {
|
|
791
|
+
outpost_id: outpostId,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
const listed = body.lanes;
|
|
795
|
+
const lanes = [];
|
|
796
|
+
for (const [index, value] of listed.entries()) {
|
|
797
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
798
|
+
throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} lane row ${index} is malformed`, {
|
|
799
|
+
outpost_id: outpostId,
|
|
800
|
+
index,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
const raw = value;
|
|
804
|
+
const host = raw.host;
|
|
805
|
+
if (host === null || typeof host !== "object" || Array.isArray(host)) {
|
|
806
|
+
throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} lane row ${index} has no host`, {
|
|
807
|
+
outpost_id: outpostId,
|
|
808
|
+
index,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
const rowHost = host;
|
|
812
|
+
const belongsToOutpost = rowHost.kind === "outpost" && rowHost.id === outpostId;
|
|
813
|
+
// Lanes created before an Outpost received its identity marker retain the
|
|
814
|
+
// real local hostname in their record. The remote list comes from this
|
|
815
|
+
// Outpost's own store, so preserve those local identities for accounting.
|
|
816
|
+
const hasLocalHostIdentity = rowHost.kind === "local" &&
|
|
817
|
+
typeof rowHost.id === "string" && rowHost.id.trim() !== "";
|
|
818
|
+
if (!belongsToOutpost && !hasLocalHostIdentity)
|
|
819
|
+
continue;
|
|
820
|
+
const normalized = raw.state === "remote" && raw.remote_state !== undefined
|
|
821
|
+
? { ...raw, state: raw.remote_state }
|
|
822
|
+
: raw;
|
|
823
|
+
try {
|
|
824
|
+
lanes.push(parseLaneRecord(normalized));
|
|
825
|
+
}
|
|
826
|
+
catch (err) {
|
|
827
|
+
throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} returned an invalid owned lane record: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId, index });
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return lanes;
|
|
831
|
+
}
|
|
832
|
+
async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
|
|
833
|
+
const localHost = laneHostFromCtx(ctx);
|
|
834
|
+
const localLanes = loadAllLanes(hqRoot);
|
|
835
|
+
const local = buildHostCapacitySnapshot({
|
|
836
|
+
host: localHost,
|
|
837
|
+
lanes: localLanes,
|
|
838
|
+
config,
|
|
839
|
+
existsSync: ctx.existsSync,
|
|
840
|
+
probe: ctx.probeProvider,
|
|
841
|
+
env: ctx.env,
|
|
842
|
+
now: ctx.now(),
|
|
843
|
+
});
|
|
844
|
+
const snapshots = [{
|
|
845
|
+
capacity: local,
|
|
846
|
+
lanes: localLanes.filter((lane) => lane.host.kind === localHost.kind && lane.host.id === localHost.id),
|
|
847
|
+
laneListAvailable: true,
|
|
848
|
+
}];
|
|
849
|
+
if (localHost.kind === "outpost") {
|
|
850
|
+
const hostname = ctx.hostname().trim();
|
|
851
|
+
const actualLocalHost = { kind: "local", id: hostname };
|
|
852
|
+
const actualLocalLanes = hostname
|
|
853
|
+
? localLanes.filter((lane) => lane.host.kind === "local" && lane.host.id === hostname)
|
|
854
|
+
: [];
|
|
855
|
+
if (actualLocalLanes.length > 0) {
|
|
856
|
+
snapshots.push({
|
|
857
|
+
capacity: buildHostCapacitySnapshot({
|
|
858
|
+
host: actualLocalHost,
|
|
859
|
+
lanes: actualLocalLanes,
|
|
860
|
+
config,
|
|
861
|
+
existsSync: ctx.existsSync,
|
|
862
|
+
probe: ctx.probeProvider,
|
|
863
|
+
env: ctx.env,
|
|
864
|
+
now: ctx.now(),
|
|
865
|
+
}),
|
|
866
|
+
lanes: actualLocalLanes,
|
|
867
|
+
laneListAvailable: true,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const unavailableHosts = [];
|
|
872
|
+
let outpostIds;
|
|
873
|
+
try {
|
|
874
|
+
outpostIds = await ctx.listOutpostIds();
|
|
875
|
+
}
|
|
876
|
+
catch (err) {
|
|
877
|
+
unavailableHosts.push({
|
|
878
|
+
host: { kind: "unknown", id: "unknown" },
|
|
879
|
+
reason: `outpost_list_unavailable: ${dispatchErrorMessage(err)}`,
|
|
880
|
+
});
|
|
881
|
+
return { snapshots, unavailableHosts };
|
|
882
|
+
}
|
|
883
|
+
if (outpostIds.length > MAX_PLACEMENT_OUTPOSTS ||
|
|
884
|
+
outpostIds.some((id) => typeof id !== "string" || id.trim() === "") ||
|
|
885
|
+
new Set(outpostIds).size !== outpostIds.length ||
|
|
886
|
+
new Set(outpostIds.map((id) => id.trim())).size !== outpostIds.length) {
|
|
887
|
+
throw new LanesError("outpost_list_invalid", "Outpost list is malformed or exceeds the placement limit", {
|
|
888
|
+
count: outpostIds.length,
|
|
889
|
+
maximum: MAX_PLACEMENT_OUTPOSTS,
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
const remoteIds = outpostIds.map((id) => id.trim())
|
|
893
|
+
.filter((id) => id !== "primary" || (localHost.kind === "outpost" && localHost.id !== "primary"))
|
|
894
|
+
.filter((id) => localHost.kind !== "outpost" || id !== localHost.id)
|
|
895
|
+
.sort();
|
|
896
|
+
const remoteResults = await mapWithConcurrency(remoteIds, OUTPOST_HIRE_SUMMARY_CONCURRENCY, async (id) => {
|
|
897
|
+
let capacity;
|
|
898
|
+
try {
|
|
899
|
+
capacity = await readOutpostCapacity(ctx, id);
|
|
900
|
+
}
|
|
901
|
+
catch (err) {
|
|
902
|
+
if (isLanesError(err) && err.error !== "outpost_capacity_unavailable") {
|
|
903
|
+
throw new LanesError("claude_account_balance_host_invalid", `Outpost ${id} returned an invalid account-capacity snapshot: ${err.message}`, { outpost_id: id, source_error: err.error });
|
|
904
|
+
}
|
|
905
|
+
return { gap: { host: { kind: "outpost", id }, reason: isLanesError(err) ? err.error : "outpost_capacity_unavailable" } };
|
|
906
|
+
}
|
|
907
|
+
let lanes = [];
|
|
908
|
+
let laneListAvailable = false;
|
|
909
|
+
try {
|
|
910
|
+
lanes = await readOutpostLaneRecords(ctx, id);
|
|
911
|
+
laneListAvailable = true;
|
|
912
|
+
}
|
|
913
|
+
catch (err) {
|
|
914
|
+
if (isLanesError(err) && err.error === "outpost_lane_list_invalid")
|
|
915
|
+
throw err;
|
|
916
|
+
unavailableHosts.push({
|
|
917
|
+
host: { kind: "outpost", id },
|
|
918
|
+
reason: isLanesError(err) ? err.error : "outpost_lane_list_unavailable",
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
const hostSnapshots = [{
|
|
922
|
+
capacity,
|
|
923
|
+
lanes: laneListAvailable
|
|
924
|
+
? lanes.filter((lane) => lane.host.kind === "outpost" && lane.host.id === id)
|
|
925
|
+
: lanes,
|
|
926
|
+
laneListAvailable,
|
|
927
|
+
}];
|
|
928
|
+
if (laneListAvailable) {
|
|
929
|
+
const localGroups = new Map();
|
|
930
|
+
for (const lane of lanes) {
|
|
931
|
+
if (lane.host.kind !== "local")
|
|
932
|
+
continue;
|
|
933
|
+
const group = localGroups.get(lane.host.id) ?? [];
|
|
934
|
+
group.push(lane);
|
|
935
|
+
localGroups.set(lane.host.id, group);
|
|
936
|
+
}
|
|
937
|
+
for (const [hostname, localHostLanes] of localGroups) {
|
|
938
|
+
hostSnapshots.push({
|
|
939
|
+
capacity: { ...capacity, host: { kind: "local", id: hostname } },
|
|
940
|
+
lanes: localHostLanes,
|
|
941
|
+
laneListAvailable: true,
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return { snapshots: hostSnapshots };
|
|
946
|
+
});
|
|
947
|
+
for (const result of remoteResults) {
|
|
948
|
+
if ("snapshots" in result)
|
|
949
|
+
snapshots.push(...result.snapshots);
|
|
950
|
+
else
|
|
951
|
+
unavailableHosts.push(result.gap);
|
|
952
|
+
}
|
|
953
|
+
return { snapshots, unavailableHosts };
|
|
954
|
+
}
|
|
758
955
|
function parsePlacementPolicy(raw) {
|
|
759
956
|
const value = raw.trim();
|
|
760
957
|
if (value === "local")
|
|
@@ -1955,6 +2152,8 @@ async function runCreate(ctx, opts) {
|
|
|
1955
2152
|
let placementHostOverride;
|
|
1956
2153
|
let autoPlacementChoice;
|
|
1957
2154
|
let autoPlacementSkippedHosts = [];
|
|
2155
|
+
let balanceHostLoad;
|
|
2156
|
+
let balanceTargetHost;
|
|
1958
2157
|
let inheritedPlacement;
|
|
1959
2158
|
const callerLaneId = ctx.env.HQ_LANE_ID?.trim() || undefined;
|
|
1960
2159
|
if (requestedPlacement === undefined &&
|
|
@@ -2059,6 +2258,11 @@ async function runCreate(ctx, opts) {
|
|
|
2059
2258
|
}
|
|
2060
2259
|
let pinnedAccount;
|
|
2061
2260
|
const accountWasExplicit = Boolean(opts.account && opts.account.trim() !== "");
|
|
2261
|
+
const explicitBalanceAccount = accountWasExplicit
|
|
2262
|
+
? opts.account.trim()
|
|
2263
|
+
: profileAccounts === undefined
|
|
2264
|
+
? profileAccount
|
|
2265
|
+
: undefined;
|
|
2062
2266
|
if (autoPlacementChoice !== undefined) {
|
|
2063
2267
|
pinnedAccount = autoPlacementChoice.account;
|
|
2064
2268
|
}
|
|
@@ -2227,6 +2431,24 @@ async function runCreate(ctx, opts) {
|
|
|
2227
2431
|
}
|
|
2228
2432
|
}
|
|
2229
2433
|
const allowedAccounts = validatedAccounts.size > 0 ? [...validatedAccounts] : profileAccounts;
|
|
2434
|
+
let accountSelection;
|
|
2435
|
+
if (provider === "claude" && ctx.laneClaudeAccountBalanceEnabled()) {
|
|
2436
|
+
const targetHost = placementHostOverride ?? (outpostId !== undefined
|
|
2437
|
+
? { kind: "outpost", id: outpostId }
|
|
2438
|
+
: laneHostFromCtx(ctx));
|
|
2439
|
+
balanceTargetHost = targetHost;
|
|
2440
|
+
balanceHostLoad = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
|
|
2441
|
+
const selected = selectClaudeAccount({
|
|
2442
|
+
hosts: balanceHostLoad.snapshots,
|
|
2443
|
+
targetHost,
|
|
2444
|
+
config,
|
|
2445
|
+
...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
|
|
2446
|
+
...(explicitBalanceAccount !== undefined ? { explicitAccount: explicitBalanceAccount } : {}),
|
|
2447
|
+
unavailableHosts: balanceHostLoad.unavailableHosts,
|
|
2448
|
+
});
|
|
2449
|
+
pinnedAccount = selected.account;
|
|
2450
|
+
accountSelection = { ...selected, selected_at: ctx.now().toISOString() };
|
|
2451
|
+
}
|
|
2230
2452
|
const balancedAdmissionAccounts = !outpostId && pinnedAccount === undefined
|
|
2231
2453
|
? [...new Set(allowedAccounts ??
|
|
2232
2454
|
Object.entries(config.providers[provider].accounts)
|
|
@@ -2239,6 +2461,38 @@ async function runCreate(ctx, opts) {
|
|
|
2239
2461
|
const persist = async () => {
|
|
2240
2462
|
let admission;
|
|
2241
2463
|
let validation;
|
|
2464
|
+
if (!outpostId && provider === "claude" && explicitBalanceAccount === undefined &&
|
|
2465
|
+
balanceHostLoad !== undefined && balanceTargetHost !== undefined) {
|
|
2466
|
+
const localHost = laneHostFromCtx(ctx);
|
|
2467
|
+
if (localHost.kind === balanceTargetHost.kind && localHost.id === balanceTargetHost.id) {
|
|
2468
|
+
const localLanes = loadAllLanes(hqRoot).filter((lane) => lane.host.kind === localHost.kind && lane.host.id === localHost.id);
|
|
2469
|
+
const refreshedLocal = {
|
|
2470
|
+
capacity: buildHostCapacitySnapshot({
|
|
2471
|
+
host: localHost,
|
|
2472
|
+
lanes: localLanes,
|
|
2473
|
+
config,
|
|
2474
|
+
existsSync: ctx.existsSync,
|
|
2475
|
+
probe: ctx.probeProvider,
|
|
2476
|
+
env: ctx.env,
|
|
2477
|
+
now: ctx.now(),
|
|
2478
|
+
}),
|
|
2479
|
+
lanes: localLanes,
|
|
2480
|
+
laneListAvailable: true,
|
|
2481
|
+
};
|
|
2482
|
+
const hosts = balanceHostLoad.snapshots.map((host) => host.capacity.host.kind === localHost.kind && host.capacity.host.id === localHost.id
|
|
2483
|
+
? refreshedLocal
|
|
2484
|
+
: host);
|
|
2485
|
+
const selected = selectClaudeAccount({
|
|
2486
|
+
hosts,
|
|
2487
|
+
targetHost: balanceTargetHost,
|
|
2488
|
+
config,
|
|
2489
|
+
...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
|
|
2490
|
+
unavailableHosts: balanceHostLoad.unavailableHosts,
|
|
2491
|
+
});
|
|
2492
|
+
pinnedAccount = selected.account;
|
|
2493
|
+
accountSelection = { ...selected, selected_at: ctx.now().toISOString() };
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2242
2496
|
if (!outpostId && pinnedAccount === undefined) {
|
|
2243
2497
|
pinnedAccount = resolveAccount(provider, undefined, config, {
|
|
2244
2498
|
hqRoot,
|
|
@@ -2337,6 +2591,7 @@ async function runCreate(ctx, opts) {
|
|
|
2337
2591
|
: {}),
|
|
2338
2592
|
provider,
|
|
2339
2593
|
account: pinnedAccount,
|
|
2594
|
+
...(accountSelection !== undefined ? { account_selection: accountSelection } : {}),
|
|
2340
2595
|
...(defaultPlacement !== undefined
|
|
2341
2596
|
? { placement_default: defaultPlacement }
|
|
2342
2597
|
: {}),
|
|
@@ -3113,6 +3368,8 @@ async function runCapacity(ctx) {
|
|
|
3113
3368
|
lanes: loadAllLanes(hqRoot),
|
|
3114
3369
|
config: loadLanesConfig(hqRoot),
|
|
3115
3370
|
existsSync: ctx.existsSync,
|
|
3371
|
+
probe: ctx.probeProvider,
|
|
3372
|
+
env: ctx.env,
|
|
3116
3373
|
now: ctx.now(),
|
|
3117
3374
|
});
|
|
3118
3375
|
printJson(snapshot);
|
|
@@ -4062,13 +4319,15 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
4062
4319
|
// (`pending_resume` set by this write, cleared by the claim), so two moves
|
|
4063
4320
|
// into one free slot cannot both start.
|
|
4064
4321
|
return withLaneAdmissionLock(hqRoot, lane.provider, async () => {
|
|
4065
|
-
const admission =
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4322
|
+
const admission = input.deferTurnStart
|
|
4323
|
+
? undefined
|
|
4324
|
+
: await assessLaneAdmission(ctx, lane, {
|
|
4325
|
+
account: target,
|
|
4326
|
+
queueAt: at,
|
|
4327
|
+
});
|
|
4328
|
+
if (admission?.action === "refuse" && admission.error)
|
|
4070
4329
|
throw admission.error;
|
|
4071
|
-
const queued = admission
|
|
4330
|
+
const queued = admission?.action === "queue";
|
|
4072
4331
|
const moved = await withResumeHireAdmission(ctx, lane, () => casUpdateLane(hqRoot, laneId, (current) => {
|
|
4073
4332
|
// The copy and every check above were made from `lane`; a record
|
|
4074
4333
|
// that moved on since (another move, a spawn, a cancel) invalidates
|
|
@@ -4080,6 +4339,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
4080
4339
|
const next = {
|
|
4081
4340
|
...current,
|
|
4082
4341
|
account: target,
|
|
4342
|
+
...(input.accountSelection !== undefined ? { account_selection: input.accountSelection } : {}),
|
|
4083
4343
|
admission_accounts: undefined,
|
|
4084
4344
|
provider_session_id: sessionId,
|
|
4085
4345
|
provider_session_id_absent: undefined,
|
|
@@ -4092,11 +4352,16 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
4092
4352
|
at,
|
|
4093
4353
|
reason: SESSION_MOVE_REASON,
|
|
4094
4354
|
}),
|
|
4095
|
-
pending_resume:
|
|
4355
|
+
pending_resume: input.deferTurnStart
|
|
4356
|
+
? undefined
|
|
4357
|
+
: { session_id: sessionId, at, ...input.pending },
|
|
4096
4358
|
};
|
|
4097
|
-
if (
|
|
4359
|
+
if (input.deferTurnStart) {
|
|
4360
|
+
next.state = current.state;
|
|
4361
|
+
}
|
|
4362
|
+
else if (queued) {
|
|
4098
4363
|
next.state = "queued";
|
|
4099
|
-
next.queued_reason = admission
|
|
4364
|
+
next.queued_reason = admission?.reason ?? "throttle";
|
|
4100
4365
|
next.queued_at = at;
|
|
4101
4366
|
}
|
|
4102
4367
|
else {
|
|
@@ -4107,6 +4372,54 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
4107
4372
|
return { lane: moved, from, to: target, copy, queued };
|
|
4108
4373
|
}, ctx.sleep);
|
|
4109
4374
|
}
|
|
4375
|
+
async function selectAutomaticClaudeResumeAccount(ctx, lane) {
|
|
4376
|
+
if (lane.provider !== "claude")
|
|
4377
|
+
return undefined;
|
|
4378
|
+
if (!ctx.laneClaudeAccountBalanceEnabled()) {
|
|
4379
|
+
if (lane.account === undefined)
|
|
4380
|
+
return undefined;
|
|
4381
|
+
const config = loadLanesConfig(ctx.hqRoot());
|
|
4382
|
+
const currentConfig = config.providers.claude.accounts[lane.account];
|
|
4383
|
+
if (currentConfig === undefined || !currentConfig.enabled) {
|
|
4384
|
+
throw new AccountResolutionError(currentConfig === undefined ? "unknown_account" : "account_disabled", currentConfig === undefined
|
|
4385
|
+
? `Lane ${lane.lane_id} records account ${JSON.stringify(lane.account)} which is not configured for claude`
|
|
4386
|
+
: `Account ${JSON.stringify(lane.account)} is unavailable for claude`, { provider: "claude", account: lane.account });
|
|
4387
|
+
}
|
|
4388
|
+
if (accountStatus("claude", lane.account, config, {
|
|
4389
|
+
existsSync: ctx.existsSync,
|
|
4390
|
+
probe: ctx.probeProvider,
|
|
4391
|
+
env: ctx.env,
|
|
4392
|
+
}).logged_in === false) {
|
|
4393
|
+
throw new LanesError("account_not_logged_in", `Account ${lane.account} for claude is not logged in`, { provider: "claude", account: lane.account, lane_id: lane.lane_id });
|
|
4394
|
+
}
|
|
4395
|
+
return undefined;
|
|
4396
|
+
}
|
|
4397
|
+
const hqRoot = ctx.hqRoot();
|
|
4398
|
+
const config = loadLanesConfig(hqRoot);
|
|
4399
|
+
const currentConfig = lane.account === undefined
|
|
4400
|
+
? undefined
|
|
4401
|
+
: config.providers.claude.accounts[lane.account];
|
|
4402
|
+
const currentLogin = lane.account === undefined || currentConfig === undefined
|
|
4403
|
+
? false
|
|
4404
|
+
: accountStatus("claude", lane.account, config, {
|
|
4405
|
+
existsSync: ctx.existsSync,
|
|
4406
|
+
probe: ctx.probeProvider,
|
|
4407
|
+
env: ctx.env,
|
|
4408
|
+
}).logged_in;
|
|
4409
|
+
const accountNeedsRepair = currentConfig?.enabled !== true || currentLogin === false;
|
|
4410
|
+
const mayRebalance = BALANCE_MOVABLE_STATES.includes(lane.state);
|
|
4411
|
+
if (!accountNeedsRepair && !mayRebalance)
|
|
4412
|
+
return undefined;
|
|
4413
|
+
const balanceHosts = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
|
|
4414
|
+
const selected = selectClaudeAccount({
|
|
4415
|
+
hosts: balanceHosts.snapshots,
|
|
4416
|
+
targetHost: lane.host,
|
|
4417
|
+
config,
|
|
4418
|
+
...(lane.admission_accounts !== undefined ? { allowedAccounts: lane.admission_accounts } : {}),
|
|
4419
|
+
unavailableHosts: balanceHosts.unavailableHosts,
|
|
4420
|
+
});
|
|
4421
|
+
return { ...selected, selected_at: ctx.now().toISOString() };
|
|
4422
|
+
}
|
|
4110
4423
|
async function runMessage(ctx, laneId, opts) {
|
|
4111
4424
|
const hqRoot = ctx.hqRoot();
|
|
4112
4425
|
const text = opts.text;
|
|
@@ -4184,6 +4497,31 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
4184
4497
|
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
4185
4498
|
}
|
|
4186
4499
|
}
|
|
4500
|
+
else if (mechanism === "resume") {
|
|
4501
|
+
const selection = await selectAutomaticClaudeResumeAccount(ctx, lane);
|
|
4502
|
+
if (selection !== undefined) {
|
|
4503
|
+
if (selection.account !== lane.account) {
|
|
4504
|
+
move = await prepareAccountMove(ctx, lane, {
|
|
4505
|
+
account: selection.account,
|
|
4506
|
+
cliModel: undefined,
|
|
4507
|
+
...(resumeProfile !== undefined ? { workerProfile: resumeProfile.profile } : {}),
|
|
4508
|
+
pending: {
|
|
4509
|
+
trigger: "message",
|
|
4510
|
+
text,
|
|
4511
|
+
message_id: messageId,
|
|
4512
|
+
compact: opts.compact !== false,
|
|
4513
|
+
},
|
|
4514
|
+
accountSelection: selection,
|
|
4515
|
+
});
|
|
4516
|
+
lane = move.lane;
|
|
4517
|
+
if (move.queued)
|
|
4518
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
4519
|
+
}
|
|
4520
|
+
else {
|
|
4521
|
+
lane = persistClaudeAccountSelection(hqRoot, lane, selection);
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4187
4525
|
if (resumeProfile !== undefined) {
|
|
4188
4526
|
if (opts.account === undefined)
|
|
4189
4527
|
validateResumeProfileModel(ctx, lane, resumeProfile.profile);
|
|
@@ -4357,6 +4695,150 @@ function accountMoveSummary(move) {
|
|
|
4357
4695
|
target_dir: move.copy.target_dir,
|
|
4358
4696
|
};
|
|
4359
4697
|
}
|
|
4698
|
+
function persistClaudeAccountSelection(hqRoot, lane, selection) {
|
|
4699
|
+
return casUpdateLane(hqRoot, lane.lane_id, (current) => {
|
|
4700
|
+
if (current.version !== lane.version || current.state !== lane.state || current.account !== lane.account) {
|
|
4701
|
+
throw new LanesError("claude_account_balance_selection_conflict", `Lane ${lane.lane_id} changed while Claude account balance was being selected`, {
|
|
4702
|
+
lane_id: lane.lane_id,
|
|
4703
|
+
prepared_version: lane.version,
|
|
4704
|
+
version: current.version,
|
|
4705
|
+
state: current.state,
|
|
4706
|
+
account: current.account ?? null,
|
|
4707
|
+
});
|
|
4708
|
+
}
|
|
4709
|
+
return { ...current, account_selection: selection };
|
|
4710
|
+
});
|
|
4711
|
+
}
|
|
4712
|
+
async function applyClaudeBalanceMove(ctx, suggestion) {
|
|
4713
|
+
const currentHost = reconcileHostFromCtx(ctx);
|
|
4714
|
+
const suggestionRunsHere = suggestion.host.kind === "outpost"
|
|
4715
|
+
? currentHost.outpostId === suggestion.host.id
|
|
4716
|
+
: currentHost.outpostId === null && currentHost.hostname === suggestion.host.id;
|
|
4717
|
+
if (suggestion.host.kind === "outpost" && !suggestionRunsHere) {
|
|
4718
|
+
let result;
|
|
4719
|
+
try {
|
|
4720
|
+
result = await ctx.dispatchOutpostExec({
|
|
4721
|
+
outpostId: suggestion.host.id,
|
|
4722
|
+
command: buildOutpostBalanceMoveCommand({
|
|
4723
|
+
laneId: suggestion.lane_id,
|
|
4724
|
+
account: suggestion.to,
|
|
4725
|
+
fromAccount: suggestion.from,
|
|
4726
|
+
expectedState: suggestion.state,
|
|
4727
|
+
}),
|
|
4728
|
+
timeoutSeconds: 120,
|
|
4729
|
+
waitMs: 120_000,
|
|
4730
|
+
});
|
|
4731
|
+
}
|
|
4732
|
+
catch (err) {
|
|
4733
|
+
throw new LanesError("claude_account_balance_move_failed", `Could not move lane ${suggestion.lane_id} on Outpost ${suggestion.host.id}: ${dispatchErrorMessage(err)}`, { lane_id: suggestion.lane_id, host: suggestion.host, account: suggestion.to });
|
|
4734
|
+
}
|
|
4735
|
+
if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
|
|
4736
|
+
throw new LanesError("claude_account_balance_move_failed", `Outpost ${suggestion.host.id} did not confirm the account move for lane ${suggestion.lane_id}`, { lane_id: suggestion.lane_id, host: suggestion.host, account: suggestion.to, status: result.status, exit_code: result.exitCode });
|
|
4737
|
+
}
|
|
4738
|
+
const remote = parseRemoteJson(result.stdout, "Claude account balance move");
|
|
4739
|
+
if (remote?.ok !== true || remote.action !== "balance-move-target" ||
|
|
4740
|
+
remote.lane_id !== suggestion.lane_id || remote.from !== suggestion.from ||
|
|
4741
|
+
remote.account !== suggestion.to ||
|
|
4742
|
+
remote.state !== suggestion.state) {
|
|
4743
|
+
throw new LanesError("claude_account_balance_move_unconfirmed", `Outpost ${suggestion.host.id} returned an invalid account-move result`, {
|
|
4744
|
+
lane_id: suggestion.lane_id,
|
|
4745
|
+
host: suggestion.host,
|
|
4746
|
+
account: suggestion.to,
|
|
4747
|
+
});
|
|
4748
|
+
}
|
|
4749
|
+
return { lane_id: suggestion.lane_id, host: suggestion.host, from: suggestion.from, to: suggestion.to, state: suggestion.state };
|
|
4750
|
+
}
|
|
4751
|
+
const lane = laneWithRecoveredProviderSession(ctx.hqRoot(), readLane(ctx.hqRoot(), suggestion.lane_id));
|
|
4752
|
+
if (lane.state === "running") {
|
|
4753
|
+
throw new LanesError("lane_running", `Lane ${lane.lane_id} is running; balance never moves a running turn`, {
|
|
4754
|
+
lane_id: lane.lane_id,
|
|
4755
|
+
state: lane.state,
|
|
4756
|
+
});
|
|
4757
|
+
}
|
|
4758
|
+
if (lane.host.kind !== suggestion.host.kind || lane.host.id !== suggestion.host.id ||
|
|
4759
|
+
lane.account !== suggestion.from || lane.state !== suggestion.state) {
|
|
4760
|
+
throw new LanesError("claude_account_balance_lane_changed", `Lane ${lane.lane_id} changed after balance was calculated`, {
|
|
4761
|
+
lane_id: lane.lane_id,
|
|
4762
|
+
host: lane.host,
|
|
4763
|
+
account: lane.account ?? null,
|
|
4764
|
+
});
|
|
4765
|
+
}
|
|
4766
|
+
if (!BALANCE_MOVABLE_STATES.includes(lane.state)) {
|
|
4767
|
+
throw new LanesError("claude_account_balance_state_not_movable", `Lane ${lane.lane_id} is no longer between turns`, {
|
|
4768
|
+
lane_id: lane.lane_id,
|
|
4769
|
+
state: lane.state,
|
|
4770
|
+
});
|
|
4771
|
+
}
|
|
4772
|
+
const moved = await prepareAccountMove(ctx, lane, {
|
|
4773
|
+
account: suggestion.to,
|
|
4774
|
+
cliModel: undefined,
|
|
4775
|
+
deferTurnStart: true,
|
|
4776
|
+
});
|
|
4777
|
+
return { lane_id: moved.lane.lane_id, host: moved.lane.host, from: moved.from ?? suggestion.from, to: moved.to, state: moved.lane.state };
|
|
4778
|
+
}
|
|
4779
|
+
async function runClaudeBalanceMoveTarget(ctx, laneId, opts) {
|
|
4780
|
+
assertBalanceApplyEnabled(ctx.laneClaudeAccountBalanceEnabled());
|
|
4781
|
+
const lane = laneWithRecoveredProviderSession(ctx.hqRoot(), readLane(ctx.hqRoot(), laneId));
|
|
4782
|
+
assertLaneRunsHereForOperatorMutation(ctx, lane, "balance-move-target");
|
|
4783
|
+
if (lane.provider !== "claude") {
|
|
4784
|
+
throw new LanesError("claude_account_balance_provider_mismatch", `Lane ${laneId} does not use Claude`, {
|
|
4785
|
+
lane_id: laneId,
|
|
4786
|
+
provider: lane.provider,
|
|
4787
|
+
});
|
|
4788
|
+
}
|
|
4789
|
+
const expectedFrom = opts.fromAccount?.trim();
|
|
4790
|
+
if (!expectedFrom) {
|
|
4791
|
+
throw new LanesError("claude_account_balance_source_invalid", "Balance move requires --from-account <id>", {
|
|
4792
|
+
lane_id: laneId,
|
|
4793
|
+
});
|
|
4794
|
+
}
|
|
4795
|
+
if (lane.account !== expectedFrom) {
|
|
4796
|
+
throw new LanesError("claude_account_balance_source_changed", `Lane ${laneId} no longer uses the planned source account`, {
|
|
4797
|
+
lane_id: laneId,
|
|
4798
|
+
expected_account: expectedFrom,
|
|
4799
|
+
actual_account: lane.account ?? null,
|
|
4800
|
+
});
|
|
4801
|
+
}
|
|
4802
|
+
const expectedState = opts.expectedState?.trim();
|
|
4803
|
+
if (!expectedState) {
|
|
4804
|
+
throw new LanesError("claude_account_balance_state_invalid", "Balance move requires --expected-state <state>", {
|
|
4805
|
+
lane_id: laneId,
|
|
4806
|
+
});
|
|
4807
|
+
}
|
|
4808
|
+
if (lane.state !== expectedState) {
|
|
4809
|
+
throw new LanesError("claude_account_balance_state_changed", `Lane ${laneId} changed state after balance was calculated`, {
|
|
4810
|
+
lane_id: laneId,
|
|
4811
|
+
expected_state: expectedState,
|
|
4812
|
+
actual_state: lane.state,
|
|
4813
|
+
});
|
|
4814
|
+
}
|
|
4815
|
+
if (!BALANCE_MOVABLE_STATES.includes(lane.state) || lane.pending_resume !== undefined) {
|
|
4816
|
+
throw new LanesError("claude_account_balance_state_not_movable", `Lane ${laneId} is not between turns`, {
|
|
4817
|
+
lane_id: laneId,
|
|
4818
|
+
state: lane.state,
|
|
4819
|
+
pending_resume: lane.pending_resume !== undefined,
|
|
4820
|
+
});
|
|
4821
|
+
}
|
|
4822
|
+
const account = opts.account?.trim();
|
|
4823
|
+
if (!account) {
|
|
4824
|
+
throw new LanesError("claude_account_balance_account_invalid", "Balance move requires --account <id>", {
|
|
4825
|
+
lane_id: laneId,
|
|
4826
|
+
});
|
|
4827
|
+
}
|
|
4828
|
+
const moved = await prepareAccountMove(ctx, lane, {
|
|
4829
|
+
account,
|
|
4830
|
+
cliModel: undefined,
|
|
4831
|
+
deferTurnStart: true,
|
|
4832
|
+
});
|
|
4833
|
+
printJson({
|
|
4834
|
+
ok: true,
|
|
4835
|
+
action: "balance-move-target",
|
|
4836
|
+
lane_id: laneId,
|
|
4837
|
+
from: moved.from,
|
|
4838
|
+
state: moved.lane.state,
|
|
4839
|
+
account: moved.lane.account,
|
|
4840
|
+
});
|
|
4841
|
+
}
|
|
4360
4842
|
function laneWithRecoveredProviderSession(hqRoot, lane) {
|
|
4361
4843
|
if (lane.provider === "grok")
|
|
4362
4844
|
return lane;
|
|
@@ -4456,6 +4938,9 @@ async function runResume(ctx, laneId, opts) {
|
|
|
4456
4938
|
if (resumeProfile !== undefined && opts.account === undefined) {
|
|
4457
4939
|
validateResumeProfileModel(ctx, lane, resumeProfile.profile);
|
|
4458
4940
|
}
|
|
4941
|
+
const automaticAccountSelection = opts.account === undefined
|
|
4942
|
+
? await selectAutomaticClaudeResumeAccount(ctx, lane)
|
|
4943
|
+
: undefined;
|
|
4459
4944
|
let move = null;
|
|
4460
4945
|
if (opts.account !== undefined) {
|
|
4461
4946
|
move = await prepareAccountMove(ctx, lane, {
|
|
@@ -4472,6 +4957,27 @@ async function runResume(ctx, laneId, opts) {
|
|
|
4472
4957
|
});
|
|
4473
4958
|
lane = move.lane;
|
|
4474
4959
|
}
|
|
4960
|
+
else if (automaticAccountSelection !== undefined) {
|
|
4961
|
+
if (automaticAccountSelection.account !== lane.account) {
|
|
4962
|
+
move = await prepareAccountMove(ctx, lane, {
|
|
4963
|
+
account: automaticAccountSelection.account,
|
|
4964
|
+
cliModel: undefined,
|
|
4965
|
+
...(resumeProfile !== undefined ? { workerProfile: resumeProfile.profile } : {}),
|
|
4966
|
+
pending: {
|
|
4967
|
+
trigger: "resume",
|
|
4968
|
+
resumed_from: resumedFrom,
|
|
4969
|
+
...(text !== undefined ? { text } : {}),
|
|
4970
|
+
...(messageId !== undefined ? { message_id: messageId } : {}),
|
|
4971
|
+
compact: opts.compact !== false,
|
|
4972
|
+
},
|
|
4973
|
+
accountSelection: automaticAccountSelection,
|
|
4974
|
+
});
|
|
4975
|
+
lane = move.lane;
|
|
4976
|
+
}
|
|
4977
|
+
else {
|
|
4978
|
+
lane = persistClaudeAccountSelection(hqRoot, lane, automaticAccountSelection);
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4475
4981
|
lane = recordResumeSetup(ctx, lane, {
|
|
4476
4982
|
...(resumeProfile !== undefined ? { profile: resumeProfile } : {}),
|
|
4477
4983
|
...(requestedMeshCompany !== undefined
|
|
@@ -5750,6 +6256,105 @@ async function runAccountsList(ctx, opts) {
|
|
|
5750
6256
|
accounts,
|
|
5751
6257
|
});
|
|
5752
6258
|
}
|
|
6259
|
+
async function runAccountsBalance(ctx, opts) {
|
|
6260
|
+
const provider = parseLaneProvider(opts.provider ?? "");
|
|
6261
|
+
if (provider !== "claude") {
|
|
6262
|
+
throw new LanesError("claude_account_balance_provider_unsupported", "Account balance is available only for --provider claude", {
|
|
6263
|
+
provider,
|
|
6264
|
+
accepted: ["claude"],
|
|
6265
|
+
});
|
|
6266
|
+
}
|
|
6267
|
+
if (opts.apply)
|
|
6268
|
+
assertBalanceApplyEnabled(ctx.laneClaudeAccountBalanceEnabled());
|
|
6269
|
+
const hqRoot = ctx.hqRoot();
|
|
6270
|
+
const config = loadLanesConfig(hqRoot);
|
|
6271
|
+
const loaded = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
|
|
6272
|
+
const loads = accountLoadsForReport({ hosts: loaded.snapshots, config });
|
|
6273
|
+
const suggestions = planClaudeBalanceMoves({ hosts: loaded.snapshots, config });
|
|
6274
|
+
const selections = loaded.snapshots.map((host) => {
|
|
6275
|
+
try {
|
|
6276
|
+
const selection = selectClaudeAccount({
|
|
6277
|
+
hosts: loaded.snapshots,
|
|
6278
|
+
targetHost: host.capacity.host,
|
|
6279
|
+
config,
|
|
6280
|
+
unavailableHosts: loaded.unavailableHosts,
|
|
6281
|
+
});
|
|
6282
|
+
return {
|
|
6283
|
+
target_host: host.capacity.host,
|
|
6284
|
+
account: selection.account,
|
|
6285
|
+
signal: selection.signal,
|
|
6286
|
+
skipped_accounts: selection.skipped_accounts,
|
|
6287
|
+
};
|
|
6288
|
+
}
|
|
6289
|
+
catch (err) {
|
|
6290
|
+
return {
|
|
6291
|
+
target_host: host.capacity.host,
|
|
6292
|
+
error: isLanesError(err) ? err.error : "claude_account_balance_selection_failed",
|
|
6293
|
+
message: dispatchErrorMessage(err),
|
|
6294
|
+
};
|
|
6295
|
+
}
|
|
6296
|
+
});
|
|
6297
|
+
const accounts = Object.keys(config.providers.claude.accounts).sort().map((account) => {
|
|
6298
|
+
const load = loads.find((row) => row.account === account);
|
|
6299
|
+
return {
|
|
6300
|
+
account,
|
|
6301
|
+
enabled: config.providers.claude.accounts[account]?.enabled === true,
|
|
6302
|
+
live_lanes: load?.live_lanes ?? 0,
|
|
6303
|
+
counts_complete: load?.counts_complete ?? false,
|
|
6304
|
+
hosts: loaded.snapshots.map((host) => {
|
|
6305
|
+
const accountStatus = host.capacity.providers.claude.accounts[account];
|
|
6306
|
+
const accountLoad = load?.hosts.find((row) => row.host.kind === host.capacity.host.kind && row.host.id === host.capacity.host.id);
|
|
6307
|
+
return {
|
|
6308
|
+
host: host.capacity.host,
|
|
6309
|
+
enabled: accountStatus?.enabled ?? false,
|
|
6310
|
+
logged_in: accountStatus?.logged_in ?? null,
|
|
6311
|
+
live_lanes: accountLoad?.live_lanes ?? null,
|
|
6312
|
+
lane_counts: accountLoad?.lane_counts ?? { running: null, queued: null, awaiting_input: null },
|
|
6313
|
+
count_source: accountLoad?.count_source ?? "unavailable",
|
|
6314
|
+
usage: accountStatus?.usage ?? null,
|
|
6315
|
+
};
|
|
6316
|
+
}),
|
|
6317
|
+
};
|
|
6318
|
+
});
|
|
6319
|
+
const moved = [];
|
|
6320
|
+
const skipped = [];
|
|
6321
|
+
if (opts.apply) {
|
|
6322
|
+
for (const suggestion of suggestions) {
|
|
6323
|
+
if (!suggestion.apply) {
|
|
6324
|
+
skipped.push({
|
|
6325
|
+
lane_id: suggestion.lane_id,
|
|
6326
|
+
host: suggestion.host,
|
|
6327
|
+
from: suggestion.from,
|
|
6328
|
+
to: suggestion.to,
|
|
6329
|
+
reason: suggestion.skip_reason ?? "not_movable",
|
|
6330
|
+
});
|
|
6331
|
+
continue;
|
|
6332
|
+
}
|
|
6333
|
+
try {
|
|
6334
|
+
moved.push(await applyClaudeBalanceMove(ctx, suggestion));
|
|
6335
|
+
}
|
|
6336
|
+
catch (err) {
|
|
6337
|
+
skipped.push({
|
|
6338
|
+
lane_id: suggestion.lane_id,
|
|
6339
|
+
host: suggestion.host,
|
|
6340
|
+
from: suggestion.from,
|
|
6341
|
+
to: suggestion.to,
|
|
6342
|
+
reason: isLanesError(err) ? `${err.error}: ${err.message}` : dispatchErrorMessage(err),
|
|
6343
|
+
});
|
|
6344
|
+
}
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
printJson({
|
|
6348
|
+
ok: true,
|
|
6349
|
+
action: "accounts.balance",
|
|
6350
|
+
provider: "claude",
|
|
6351
|
+
accounts,
|
|
6352
|
+
selections,
|
|
6353
|
+
unavailable_hosts: loaded.unavailableHosts,
|
|
6354
|
+
suggestions,
|
|
6355
|
+
...(opts.apply ? { moved, skipped } : {}),
|
|
6356
|
+
});
|
|
6357
|
+
}
|
|
5753
6358
|
async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
5754
6359
|
const callerCwd = ctx.callerCwd();
|
|
5755
6360
|
const hqRoot = ctx.hqRoot();
|
|
@@ -6123,6 +6728,15 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
6123
6728
|
.action(wrap(async (laneId, opts) => {
|
|
6124
6729
|
await runMoveTarget(ctx, laneId, opts);
|
|
6125
6730
|
}));
|
|
6731
|
+
lanes
|
|
6732
|
+
.command("balance-move-target", { hidden: true })
|
|
6733
|
+
.argument("<lane-id>", "Lane id")
|
|
6734
|
+
.requiredOption("--account <id>", "Target Claude account id")
|
|
6735
|
+
.requiredOption("--from-account <id>", "Expected source Claude account id")
|
|
6736
|
+
.requiredOption("--expected-state <state>", "Expected lane state from the balance plan")
|
|
6737
|
+
.action(wrap(async (laneId, opts) => {
|
|
6738
|
+
await runClaudeBalanceMoveTarget(ctx, laneId, opts);
|
|
6739
|
+
}));
|
|
6126
6740
|
lanes
|
|
6127
6741
|
.command("admit")
|
|
6128
6742
|
.description("Try to admit the oldest eligible queued lane")
|
|
@@ -6321,6 +6935,15 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
6321
6935
|
.action(wrap(async (opts) => {
|
|
6322
6936
|
await runAccountsList(ctx, opts);
|
|
6323
6937
|
}));
|
|
6938
|
+
accounts
|
|
6939
|
+
.command("balance")
|
|
6940
|
+
.description("Report Claude account load and suggest safe between-turn moves")
|
|
6941
|
+
.requiredOption("--provider <name>", "claude")
|
|
6942
|
+
.option("--apply", "Move eligible between-turn sessions (requires the Claude account balance gate)")
|
|
6943
|
+
.option("--json", "JSON on stdout (always)")
|
|
6944
|
+
.action(wrap(async (opts) => {
|
|
6945
|
+
await runAccountsBalance(ctx, opts);
|
|
6946
|
+
}));
|
|
6324
6947
|
accounts
|
|
6325
6948
|
.command("login")
|
|
6326
6949
|
.description("Create a home, run the vendor login, and register the account")
|