@indigoai-us/hq-cli 5.202.1 → 5.204.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/README.md +24 -0
- package/assets/lanes-hooks/hq-lanes-drain.mjs +36 -15
- package/dist/command-catalog.generated.d.ts +195 -0
- package/dist/command-catalog.generated.js +249 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/lanes.d.ts +4 -0
- package/dist/commands/lanes.js +350 -108
- package/dist/commands/monitor.d.ts +58 -0
- package/dist/commands/monitor.js +965 -0
- package/dist/lib/lanes/account-home.d.ts +8 -0
- package/dist/lib/lanes/account-home.js +10 -0
- package/dist/lib/lanes/accounts.d.ts +4 -1
- package/dist/lib/lanes/accounts.js +15 -10
- package/dist/lib/lanes/claude-account-balance.d.ts +3 -0
- package/dist/lib/lanes/claude-account-balance.js +19 -4
- package/dist/lib/lanes/config.d.ts +2 -1
- package/dist/lib/lanes/config.js +55 -13
- package/dist/lib/lanes/delivery-core.d.ts +15 -0
- package/dist/lib/lanes/delivery-core.js +21 -0
- package/dist/lib/lanes/delivery.d.ts +21 -5
- package/dist/lib/lanes/delivery.js +71 -7
- package/dist/lib/lanes/dropbox.d.ts +13 -1
- package/dist/lib/lanes/dropbox.js +61 -5
- package/dist/lib/lanes/host-capacity.d.ts +4 -0
- package/dist/lib/lanes/host-capacity.js +8 -1
- package/dist/lib/lanes/index.d.ts +3 -1
- package/dist/lib/lanes/index.js +2 -1
- package/dist/lib/lanes/liveness.d.ts +1 -0
- package/dist/lib/lanes/liveness.js +3 -1
- package/dist/lib/lanes/move-account.js +1 -1
- package/dist/lib/lanes/reconcile.js +20 -9
- package/dist/lib/lanes/spawn.d.ts +7 -3
- package/dist/lib/lanes/spawn.js +71 -22
- package/dist/lib/lanes/types.d.ts +8 -0
- package/dist/lib/monitor.d.ts +153 -0
- package/dist/lib/monitor.js +519 -0
- package/dist/main.js +40 -28
- package/dist/utils/cli-network-lifecycle.d.ts +4 -0
- package/dist/utils/cli-network-lifecycle.js +11 -0
- package/package.json +1 -1
package/dist/commands/lanes.js
CHANGED
|
@@ -10,6 +10,7 @@ import { randomBytes } from "node:crypto";
|
|
|
10
10
|
import * as fs from "node:fs";
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
|
+
import { readLocalOverride } from "@indigoai-us/hq-flags-client";
|
|
13
14
|
import { Option } from "commander";
|
|
14
15
|
import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
|
|
15
16
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
@@ -32,13 +33,16 @@ import { vaultApiFetch } from "../utils/vault-api.js";
|
|
|
32
33
|
import { openMeshTransport } from "../lib/mesh/client.js";
|
|
33
34
|
import { createLaneMesh, emitLaneDecisionEvents, emitLaneTerminalStateEvent, laneMeshEventIdentity, } from "../lib/lanes/mesh-events.js";
|
|
34
35
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
35
|
-
import {
|
|
36
|
+
import { effectiveAccountHome } from "../lib/lanes/account-home.js";
|
|
37
|
+
import { AccountResolutionError, compareLaneQueueOrder, coveredLaneIds, DEFAULT_ACCOUNT_HOMES, LANE_PROVIDERS, LANE_STATES, PROVIDER_HOME_ENV, QUESTION_STATUSES, SENIOR_KINDS, accountStatus, appendCappedHistory, canonicalizeHome, createLane, createQuestion, decideAdmission, defaultLivenessDeps, defaultSystemdUserDir, defaultThrottleDeps, installDrainHook, installReconcileTimer, providerPathEnv, probeDrainHookRegistration, isLanesError, isLaneVersionConflict, isTerminalLaneState, aliveDirectChildren, assignedChildHireGrants, LANE_HIRE_GRANTS_FLAG, planHireGrantBackfill, withHireGrantAdmission, laneStoryLedgerEnabled, validateProjectStories, buildLaneStatusSnapshot, formatLaneStatusHuman, listAccountStatuses, listLaneIds, listQuestionIds, listSessionLiveness, loadLanesConfig, laneRunsHere, MAX_LANE_DEPTH, MONITOR_ERROR_EXIT_CODE, MONITOR_ENGINES, MONITOR_MAX_TIMEOUT_MS, MONITOR_UNCOVERED_EXIT_CODE, MAX_WATCHED_LANES, monitorReminderText, readLiveMonitorEvidence, resolveMachineIdentity as lookupMachineIdentity, loadLivenessIndex, parseLaneProvider, parseLaneRecord, parseLaneState, parseLivenessState, parseQuestionRecord, parseQuestionStatus, parseSeniorKind, readDaemonPresenceConnected, readLane, retryUpdateLane, syncLaneRecordFromOwner, readQuestion, resolveAccount, resolveLaneWorker, ignoredWorkerProfileFields, loadWorkerProfile, runReconcilePass, wakeAnsweredLaneNow, runLaneWatch, sessionLiveness, tryReadLane, uninstallReconcileTimer, updateLane, updateQuestion, writeLanesConfig, appendDropboxMessage, answerDropboxMessage, deliveryRefusalReason, resumeCommandRefusal, dropboxDir, dropboxInboxPath, laneAcceptsDropboxDelivery, laneAcceptsOperatorMessage, laneDeliveryMechanism, hasActiveAdmissionReservation, hasActivePendingResume, hasRecordedSessionId, hasEarlierEligibleQueuedLane, laneAdmissionCapReason, providerSupportsSessionResume, providerSessionPortable, sessionNotPortableMessage, withLaneAdmissionLock, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
|
|
36
38
|
import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
|
|
37
39
|
import { questionBodyAbsPath } from "../lib/lanes/questions.js";
|
|
38
40
|
import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
|
|
39
|
-
import { providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
41
|
+
import { assertUniqueAccountHomes, providerCompactAfterMs } from "../lib/lanes/config.js";
|
|
40
42
|
import { parseCliEffort, resolveLaneEffort, } from "../lib/lanes/effort.js";
|
|
41
43
|
import { LanesError } from "../lib/lanes/errors.js";
|
|
44
|
+
import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
|
|
45
|
+
import { formatMonitorEvent } from "../lib/lanes/dropbox.js";
|
|
42
46
|
import { accountQuotaView, loadQuotaState, quotaBlockFor } from "../lib/lanes/quota.js";
|
|
43
47
|
import { SESSION_MOVE_REASON } from "../lib/lanes/move-account.js";
|
|
44
48
|
import { ensureSessionHome } from "../lib/lanes/session-relink.js";
|
|
@@ -57,6 +61,7 @@ const VENDOR_LOGIN = {
|
|
|
57
61
|
export { emitLaneDecisionEvents };
|
|
58
62
|
export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
59
63
|
export const LANE_OUTPOST_QUESTION_VISIBILITY_FLAG = "lanes.outpost-question-visibility";
|
|
64
|
+
export const LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG = "lanes.host-scoped-account-homes";
|
|
60
65
|
function defaultLanePlacementEnabled() {
|
|
61
66
|
return resolveProcessFlagGate(LANE_OUTPOST_LOAD_BALANCING_FLAG, { fallback: false }, () => false);
|
|
62
67
|
}
|
|
@@ -80,6 +85,15 @@ function defaultLaneOutpostQuestionVisibilityEnabled(company, env) {
|
|
|
80
85
|
env,
|
|
81
86
|
});
|
|
82
87
|
}
|
|
88
|
+
function defaultLaneHostScopedAccountHomesEnabled(company, env) {
|
|
89
|
+
return readLanesRegistryFlag(LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG, { company, env });
|
|
90
|
+
}
|
|
91
|
+
async function hostScopedAccountHomesEnabled(ctx) {
|
|
92
|
+
const localOverride = readLocalOverride(LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG, ctx.env);
|
|
93
|
+
if (localOverride !== null)
|
|
94
|
+
return localOverride;
|
|
95
|
+
return ctx.laneHostScopedAccountHomesEnabled(await ctx.resolveCompany());
|
|
96
|
+
}
|
|
83
97
|
function withoutProviderHome(value) {
|
|
84
98
|
if (Array.isArray(value))
|
|
85
99
|
return value.map(withoutProviderHome);
|
|
@@ -235,6 +249,7 @@ function resolveDeps(deps = {}) {
|
|
|
235
249
|
throttleDeps: deps.throttleDeps ?? defaultThrottleDeps(env),
|
|
236
250
|
existsSync: deps.existsSync ?? ((p) => fs.existsSync(p)),
|
|
237
251
|
readFileSync: deps.readFileSync ?? ((p, encoding) => fs.readFileSync(p, encoding)),
|
|
252
|
+
readStdin: deps.readStdin ?? (() => fs.readFileSync(0, "utf8")),
|
|
238
253
|
homedir: deps.homedir ?? (() => os.homedir()),
|
|
239
254
|
mesh: deps.mesh ?? createDefaultMesh(),
|
|
240
255
|
sleep: deps.sleep ?? defaultSleep,
|
|
@@ -283,6 +298,8 @@ function resolveDeps(deps = {}) {
|
|
|
283
298
|
((company) => defaultLaneOutpostQuestionVisibilityEnabled(company, env)),
|
|
284
299
|
laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
|
|
285
300
|
laneTrackerMeshEnabled: deps.laneTrackerMeshEnabled ?? defaultLaneTrackerMeshEnabled,
|
|
301
|
+
laneHostScopedAccountHomesEnabled: deps.laneHostScopedAccountHomesEnabled ??
|
|
302
|
+
((company) => defaultLaneHostScopedAccountHomesEnabled(company, env)),
|
|
286
303
|
trackerActor: deps.trackerActor ?? defaultTrackerActor,
|
|
287
304
|
};
|
|
288
305
|
}
|
|
@@ -295,6 +312,7 @@ function livenessDepsFromCtx(ctx) {
|
|
|
295
312
|
env: ctx.env,
|
|
296
313
|
existsSync: ctx.existsSync,
|
|
297
314
|
readFileSync: ctx.readFileSync,
|
|
315
|
+
hostname: ctx.hostname,
|
|
298
316
|
};
|
|
299
317
|
}
|
|
300
318
|
function reconcileHostFromCtx(ctx) {
|
|
@@ -1001,6 +1019,7 @@ async function loadHostCapacitySnapshots(ctx, hqRoot, config = loadLanesConfig(h
|
|
|
1001
1019
|
lanes: loadAllLanes(hqRoot),
|
|
1002
1020
|
config,
|
|
1003
1021
|
existsSync: ctx.existsSync,
|
|
1022
|
+
hostname: ctx.hostname(),
|
|
1004
1023
|
now: ctx.now(),
|
|
1005
1024
|
});
|
|
1006
1025
|
const remote = await Promise.all(remoteIds.map(async (id) => {
|
|
@@ -1115,6 +1134,7 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
|
|
|
1115
1134
|
existsSync: ctx.existsSync,
|
|
1116
1135
|
probe: ctx.probeProvider,
|
|
1117
1136
|
env: ctx.env,
|
|
1137
|
+
hostname: ctx.hostname(),
|
|
1118
1138
|
now: ctx.now(),
|
|
1119
1139
|
});
|
|
1120
1140
|
const snapshots = [{
|
|
@@ -1137,6 +1157,7 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
|
|
|
1137
1157
|
existsSync: ctx.existsSync,
|
|
1138
1158
|
probe: ctx.probeProvider,
|
|
1139
1159
|
env: ctx.env,
|
|
1160
|
+
hostname: ctx.hostname(),
|
|
1140
1161
|
now: ctx.now(),
|
|
1141
1162
|
}),
|
|
1142
1163
|
lanes: actualLocalLanes,
|
|
@@ -2193,6 +2214,7 @@ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
|
|
|
2193
2214
|
try {
|
|
2194
2215
|
const resolved = resolveAccount(lane.provider, lane.account, config, {
|
|
2195
2216
|
hqRoot,
|
|
2217
|
+
hostname: os.hostname(),
|
|
2196
2218
|
accountDeps: {
|
|
2197
2219
|
probe: modelDeps.probe,
|
|
2198
2220
|
env: modelDeps.env,
|
|
@@ -2200,6 +2222,7 @@ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
|
|
|
2200
2222
|
});
|
|
2201
2223
|
const status = accountStatus(lane.provider, resolved.id, config, {
|
|
2202
2224
|
existsSync: (p) => fs.existsSync(p),
|
|
2225
|
+
hostname: os.hostname(),
|
|
2203
2226
|
probe: modelDeps.probe,
|
|
2204
2227
|
env: modelDeps.env,
|
|
2205
2228
|
});
|
|
@@ -2586,6 +2609,7 @@ async function runCreate(ctx, opts) {
|
|
|
2586
2609
|
}
|
|
2587
2610
|
const resolved = resolveAccount(provider, requested, config, {
|
|
2588
2611
|
hqRoot,
|
|
2612
|
+
hostname: ctx.hostname(),
|
|
2589
2613
|
cliModel,
|
|
2590
2614
|
model: cliModel ?? profileModel,
|
|
2591
2615
|
});
|
|
@@ -2594,6 +2618,7 @@ async function runCreate(ctx, opts) {
|
|
|
2594
2618
|
else if (profileAccount !== undefined && profileAccounts === undefined) {
|
|
2595
2619
|
const resolved = resolveAccount(provider, profileAccount, config, {
|
|
2596
2620
|
hqRoot,
|
|
2621
|
+
hostname: ctx.hostname(),
|
|
2597
2622
|
cliModel,
|
|
2598
2623
|
model: cliModel ?? profileModel,
|
|
2599
2624
|
});
|
|
@@ -2647,6 +2672,7 @@ async function runCreate(ctx, opts) {
|
|
|
2647
2672
|
const candidate = pinnedAccount ??
|
|
2648
2673
|
resolveAccount(provider, undefined, config, {
|
|
2649
2674
|
hqRoot,
|
|
2675
|
+
hostname: ctx.hostname(),
|
|
2650
2676
|
existsSync: ctx.existsSync,
|
|
2651
2677
|
accountDeps: { probe: ctx.probeProvider, env: ctx.env },
|
|
2652
2678
|
cliModel,
|
|
@@ -2666,6 +2692,7 @@ async function runCreate(ctx, opts) {
|
|
|
2666
2692
|
(profileAccounts === undefined || profileAccounts.includes(id)) &&
|
|
2667
2693
|
accountStatus(provider, id, config, {
|
|
2668
2694
|
existsSync: ctx.existsSync,
|
|
2695
|
+
hostname: ctx.hostname(),
|
|
2669
2696
|
probe: ctx.probeProvider,
|
|
2670
2697
|
env: ctx.env,
|
|
2671
2698
|
}).logged_in === true)
|
|
@@ -2676,7 +2703,10 @@ async function runCreate(ctx, opts) {
|
|
|
2676
2703
|
const quotaBlocked = [];
|
|
2677
2704
|
const quota = loadQuotaState(hqRoot);
|
|
2678
2705
|
for (const id of toValidate) {
|
|
2679
|
-
const
|
|
2706
|
+
const account = accounts[id];
|
|
2707
|
+
const home = account === undefined
|
|
2708
|
+
? undefined
|
|
2709
|
+
: effectiveAccountHome(account, ctx.hostname()).home;
|
|
2680
2710
|
if (home === undefined)
|
|
2681
2711
|
continue;
|
|
2682
2712
|
checkedAccounts.push(id);
|
|
@@ -2779,6 +2809,7 @@ async function runCreate(ctx, opts) {
|
|
|
2779
2809
|
existsSync: ctx.existsSync,
|
|
2780
2810
|
probe: ctx.probeProvider,
|
|
2781
2811
|
env: ctx.env,
|
|
2812
|
+
hostname: ctx.hostname(),
|
|
2782
2813
|
now: ctx.now(),
|
|
2783
2814
|
}),
|
|
2784
2815
|
lanes: localLanes,
|
|
@@ -2802,6 +2833,7 @@ async function runCreate(ctx, opts) {
|
|
|
2802
2833
|
pinnedAccount = resolveAccount(provider, undefined, config, {
|
|
2803
2834
|
hqRoot,
|
|
2804
2835
|
existsSync: ctx.existsSync,
|
|
2836
|
+
hostname: ctx.hostname(),
|
|
2805
2837
|
accountDeps: { probe: ctx.probeProvider, env: ctx.env },
|
|
2806
2838
|
cliModel,
|
|
2807
2839
|
model: cliModel ?? profileModel,
|
|
@@ -4026,6 +4058,7 @@ async function runCapacity(ctx) {
|
|
|
4026
4058
|
existsSync: ctx.existsSync,
|
|
4027
4059
|
probe: ctx.probeProvider,
|
|
4028
4060
|
env: ctx.env,
|
|
4061
|
+
hostname: ctx.hostname(),
|
|
4029
4062
|
now: ctx.now(),
|
|
4030
4063
|
});
|
|
4031
4064
|
printJson(snapshot);
|
|
@@ -4870,6 +4903,7 @@ function validateResumeProfileModel(ctx, lane, profile) {
|
|
|
4870
4903
|
const account = resolveAccount(lane.provider, lane.account, config, {
|
|
4871
4904
|
hqRoot,
|
|
4872
4905
|
existsSync: ctx.existsSync,
|
|
4906
|
+
hostname: ctx.hostname(),
|
|
4873
4907
|
model: profile.meta.model,
|
|
4874
4908
|
});
|
|
4875
4909
|
validateProfileLaneModel(lane.provider, profile.meta.model, account.home, profile.id, {
|
|
@@ -4916,7 +4950,7 @@ function recordResumeSetup(ctx, lane, input) {
|
|
|
4916
4950
|
if (account === undefined) {
|
|
4917
4951
|
throw new AccountResolutionError("unknown_account", `Lane ${lane.lane_id} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`, { provider: lane.provider, account: lane.account ?? "", enabled: [] });
|
|
4918
4952
|
}
|
|
4919
|
-
validateLaneModel(lane.provider, model, account.home, {
|
|
4953
|
+
validateLaneModel(lane.provider, model, effectiveAccountHome(account, ctx.hostname()).home, {
|
|
4920
4954
|
readFileSync: ctx.readFileSync,
|
|
4921
4955
|
probe: ctx.probeProvider,
|
|
4922
4956
|
env: ctx.env,
|
|
@@ -5021,11 +5055,10 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5021
5055
|
account: target,
|
|
5022
5056
|
});
|
|
5023
5057
|
}
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
throw new LanesError("provider_session_not_portable", `Provider ${lane.provider} has no session resume, so a lane cannot move between its accounts`, { lane_id: laneId, provider: lane.provider });
|
|
5058
|
+
// The grok comparison repeats PROVIDER_SESSION_PORTABLE so TypeScript narrows
|
|
5059
|
+
// the provider for ensureSessionHome below.
|
|
5060
|
+
if (!providerSessionPortable(lane.provider) || lane.provider === "grok") {
|
|
5061
|
+
throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
|
|
5029
5062
|
}
|
|
5030
5063
|
if (!hasRecordedSessionId(lane)) {
|
|
5031
5064
|
throw new LanesError("no_provider_session", `Lane ${laneId} has no recorded provider session id, so there is no session to move`, { lane_id: laneId, provider: lane.provider });
|
|
@@ -5037,11 +5070,13 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5037
5070
|
if (targetCfg === undefined) {
|
|
5038
5071
|
throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(target)} for ${lane.provider}. Configured: ${Object.keys(providerCfg.accounts).join(", ") || "(none)"}`, { provider: lane.provider, account: target, enabled: Object.keys(providerCfg.accounts).filter((id) => providerCfg.accounts[id]?.enabled) });
|
|
5039
5072
|
}
|
|
5073
|
+
const targetHome = effectiveAccountHome(targetCfg, ctx.hostname()).home;
|
|
5040
5074
|
if (!targetCfg.enabled) {
|
|
5041
5075
|
throw new AccountResolutionError("account_disabled", `Account ${JSON.stringify(target)} is disabled${targetCfg.disabled_reason ? `: ${targetCfg.disabled_reason}` : ""}`, { provider: lane.provider, account: target, reason: targetCfg.disabled_reason ?? "" });
|
|
5042
5076
|
}
|
|
5043
5077
|
if (accountStatus(lane.provider, target, config, {
|
|
5044
5078
|
existsSync: ctx.existsSync,
|
|
5079
|
+
hostname: ctx.hostname(),
|
|
5045
5080
|
probe: ctx.probeProvider,
|
|
5046
5081
|
env: ctx.env,
|
|
5047
5082
|
}).logged_in !== true) {
|
|
@@ -5058,7 +5093,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5058
5093
|
: undefined;
|
|
5059
5094
|
const profileModel = profileDefaultsApply ? profile?.meta.model : undefined;
|
|
5060
5095
|
if (input.cliModel === undefined && profileDefaultsApply && profileModel !== undefined && pinnedModel === undefined) {
|
|
5061
|
-
validateProfileLaneModel(lane.provider, profileModel,
|
|
5096
|
+
validateProfileLaneModel(lane.provider, profileModel, targetHome, profile.id, {
|
|
5062
5097
|
readFileSync: ctx.readFileSync,
|
|
5063
5098
|
probe: ctx.probeProvider,
|
|
5064
5099
|
env: ctx.env,
|
|
@@ -5084,7 +5119,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5084
5119
|
throw new LanesError("account_quota_held", `Account ${target} for ${lane.provider} is held${block.quota_reset_at ? ` until ${block.quota_reset_at}` : ""}${block.marker ? ` (${block.marker})` : ""}`, { provider: lane.provider, account: target, quota_reset_at: block.quota_reset_at ?? null, marker: block.marker ?? null, model: model ?? null });
|
|
5085
5120
|
}
|
|
5086
5121
|
if (model !== undefined) {
|
|
5087
|
-
validateLaneModel(lane.provider, model,
|
|
5122
|
+
validateLaneModel(lane.provider, model, targetHome, {
|
|
5088
5123
|
readFileSync: ctx.readFileSync,
|
|
5089
5124
|
probe: ctx.probeProvider,
|
|
5090
5125
|
env: ctx.env,
|
|
@@ -5094,14 +5129,15 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5094
5129
|
if (sourceCfg === undefined) {
|
|
5095
5130
|
throw new AccountResolutionError("unknown_account", `Lane ${laneId} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`, { provider: lane.provider, account: lane.account ?? "", enabled: [] });
|
|
5096
5131
|
}
|
|
5132
|
+
const sourceHome = effectiveAccountHome(sourceCfg, ctx.hostname()).home;
|
|
5097
5133
|
// Checks done. Copy the session files, then write the record.
|
|
5098
5134
|
const copy = ensureSessionHome({
|
|
5099
5135
|
provider: lane.provider,
|
|
5100
5136
|
sessionId,
|
|
5101
5137
|
cwd: hqRoot,
|
|
5102
|
-
targetHome
|
|
5103
|
-
registeredHomes: Object.values(providerCfg.accounts).map((account) => account.home),
|
|
5104
|
-
providerHomeAtSpawn: lane.provider_home_at_spawn ??
|
|
5138
|
+
targetHome,
|
|
5139
|
+
registeredHomes: Object.values(providerCfg.accounts).map((account) => effectiveAccountHome(account, ctx.hostname()).home),
|
|
5140
|
+
providerHomeAtSpawn: lane.provider_home_at_spawn ?? sourceHome,
|
|
5105
5141
|
});
|
|
5106
5142
|
const at = now.toISOString();
|
|
5107
5143
|
const from = lane.account ?? null;
|
|
@@ -5184,6 +5220,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
|
|
|
5184
5220
|
}
|
|
5185
5221
|
if (accountStatus("claude", lane.account, config, {
|
|
5186
5222
|
existsSync: ctx.existsSync,
|
|
5223
|
+
hostname: ctx.hostname(),
|
|
5187
5224
|
probe: ctx.probeProvider,
|
|
5188
5225
|
env: ctx.env,
|
|
5189
5226
|
}).logged_in === false) {
|
|
@@ -5210,6 +5247,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
|
|
|
5210
5247
|
? false
|
|
5211
5248
|
: accountStatus("claude", lane.account, config, {
|
|
5212
5249
|
existsSync: ctx.existsSync,
|
|
5250
|
+
hostname: ctx.hostname(),
|
|
5213
5251
|
probe: ctx.probeProvider,
|
|
5214
5252
|
env: ctx.env,
|
|
5215
5253
|
}).logged_in;
|
|
@@ -5272,6 +5310,7 @@ function assertCurrentResumeAccountAvailable(ctx, lane) {
|
|
|
5272
5310
|
}
|
|
5273
5311
|
if (accountStatus(lane.provider, lane.account, config, {
|
|
5274
5312
|
existsSync: ctx.existsSync,
|
|
5313
|
+
hostname: ctx.hostname(),
|
|
5275
5314
|
probe: ctx.probeProvider,
|
|
5276
5315
|
env: ctx.env,
|
|
5277
5316
|
}).logged_in !== true) {
|
|
@@ -5310,8 +5349,8 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5310
5349
|
state: lane.state,
|
|
5311
5350
|
});
|
|
5312
5351
|
}
|
|
5313
|
-
if (opts.account !== undefined && !keepCurrentAccount && !
|
|
5314
|
-
throw new LanesError("provider_session_not_portable",
|
|
5352
|
+
if (opts.account !== undefined && !keepCurrentAccount && !providerSessionPortable(lane.provider)) {
|
|
5353
|
+
throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
|
|
5315
5354
|
}
|
|
5316
5355
|
// One decision point for how a follow-up reaches this lane (delivery.ts).
|
|
5317
5356
|
// `none` refuses before anything is written: no inbox append, no spawn.
|
|
@@ -5359,12 +5398,27 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5359
5398
|
if ((opts.project !== undefined || opts.story !== undefined) && ctx.storyLedgerEnabled()) {
|
|
5360
5399
|
validateProjectStories(hqRoot, projectId, [storyId], lane.company?.slug);
|
|
5361
5400
|
}
|
|
5401
|
+
const framedMonitorEvent = opts.monitorEvent === undefined
|
|
5402
|
+
? undefined
|
|
5403
|
+
: formatMonitorEvent({
|
|
5404
|
+
monitorId: opts.monitorEvent.monitor_id,
|
|
5405
|
+
description: opts.monitorEvent.description,
|
|
5406
|
+
sequence: opts.monitorEvent.sequence,
|
|
5407
|
+
text,
|
|
5408
|
+
});
|
|
5409
|
+
const deliveryText = framedMonitorEvent ?? text;
|
|
5362
5410
|
const dropboxMessage = {
|
|
5363
5411
|
at,
|
|
5364
5412
|
message_id: messageId,
|
|
5365
5413
|
text,
|
|
5366
5414
|
project_id: projectId,
|
|
5367
5415
|
story_id: storyId,
|
|
5416
|
+
...(opts.monitorEvent === undefined ? {} : {
|
|
5417
|
+
kind: "monitor_event",
|
|
5418
|
+
monitor_id: opts.monitorEvent.monitor_id,
|
|
5419
|
+
description: opts.monitorEvent.description,
|
|
5420
|
+
sequence: opts.monitorEvent.sequence,
|
|
5421
|
+
}),
|
|
5368
5422
|
};
|
|
5369
5423
|
let resumed = null;
|
|
5370
5424
|
let move = null;
|
|
@@ -5377,7 +5431,13 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5377
5431
|
cliModel: opts.model?.trim() || undefined,
|
|
5378
5432
|
...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
|
|
5379
5433
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5380
|
-
pending: {
|
|
5434
|
+
pending: {
|
|
5435
|
+
trigger: "message",
|
|
5436
|
+
text: deliveryText,
|
|
5437
|
+
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5438
|
+
message_id: messageId,
|
|
5439
|
+
compact: opts.compact !== false,
|
|
5440
|
+
},
|
|
5381
5441
|
});
|
|
5382
5442
|
lane = move.lane;
|
|
5383
5443
|
if (move.queued) {
|
|
@@ -5395,7 +5455,8 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5395
5455
|
...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5396
5456
|
pending: {
|
|
5397
5457
|
trigger: "message",
|
|
5398
|
-
text,
|
|
5458
|
+
text: deliveryText,
|
|
5459
|
+
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5399
5460
|
message_id: messageId,
|
|
5400
5461
|
compact: opts.compact !== false,
|
|
5401
5462
|
},
|
|
@@ -5423,81 +5484,94 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5423
5484
|
if (move?.queued) {
|
|
5424
5485
|
// Parked at the target's cap: reconcile resumes the session there.
|
|
5425
5486
|
}
|
|
5426
|
-
else if (mechanism === "dropbox") {
|
|
5427
|
-
// Live (or about-to-be-live) process: the PreToolUse drain delivers it.
|
|
5428
|
-
if (!laneAcceptsOperatorMessage(lane)) {
|
|
5429
|
-
throw new LanesError("lane_not_accepting_messages", operatorMessageRefusalReason(lane.state), { lane_id: laneId, state: lane.state });
|
|
5430
|
-
}
|
|
5431
|
-
appendDropboxMessage(hqRoot, laneId, dropboxMessage);
|
|
5432
|
-
}
|
|
5433
5487
|
else {
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5488
|
+
const dispatched = await dispatchLaneDelivery(lane, {
|
|
5489
|
+
dropbox: () => {
|
|
5490
|
+
// Live (or about-to-be-live) process: the PreToolUse drain delivers it.
|
|
5491
|
+
if (!laneAcceptsOperatorMessage(lane)) {
|
|
5492
|
+
throw new LanesError("lane_not_accepting_messages", operatorMessageRefusalReason(lane.state), { lane_id: laneId, state: lane.state });
|
|
5493
|
+
}
|
|
5494
|
+
appendDropboxMessage(hqRoot, laneId, dropboxMessage);
|
|
5495
|
+
return null;
|
|
5496
|
+
},
|
|
5497
|
+
resume: async () => {
|
|
5498
|
+
// Exited worker with a recorded session: queue the message when capacity
|
|
5499
|
+
// is occupied, otherwise start its next turn under the shared admission lock.
|
|
5500
|
+
assertLaneRunsHereForAdmission(ctx, lane);
|
|
5501
|
+
return withLaneAdmissionLock(hqRoot, lane.provider, async () => {
|
|
5502
|
+
const admission = await assessLaneAdmission(ctx, lane, { queueAt: at });
|
|
5503
|
+
if (admission.action === "refuse" && admission.error)
|
|
5504
|
+
throw admission.error;
|
|
5505
|
+
return withResumeHireAdmission(ctx, lane, () => {
|
|
5506
|
+
if (admission.action === "queue") {
|
|
5507
|
+
const sessionId = lane.provider_session_id;
|
|
5508
|
+
if (!sessionId) {
|
|
5509
|
+
throw new LanesError("no_provider_session", `Lane ${laneId} has no provider session id to queue this message resume`, {
|
|
5510
|
+
lane_id: laneId,
|
|
5511
|
+
provider: lane.provider,
|
|
5512
|
+
});
|
|
5513
|
+
}
|
|
5514
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
5515
|
+
queuedOnAdmission = true;
|
|
5516
|
+
return casUpdateLane(hqRoot, laneId, (current) => {
|
|
5517
|
+
if (current.version !== lane.version || current.state !== lane.state) {
|
|
5518
|
+
throw new LanesError("lane_admission_conflict", `Lane ${laneId} changed while message admission was checked`, {
|
|
5519
|
+
lane_id: laneId,
|
|
5520
|
+
prepared_version: lane.version,
|
|
5521
|
+
version: current.version,
|
|
5522
|
+
state: current.state,
|
|
5523
|
+
});
|
|
5524
|
+
}
|
|
5525
|
+
return {
|
|
5526
|
+
...current,
|
|
5527
|
+
state: "queued",
|
|
5528
|
+
queued_reason: admission.reason ?? "throttle",
|
|
5529
|
+
queued_at: at,
|
|
5530
|
+
pending_resume: {
|
|
5531
|
+
session_id: sessionId,
|
|
5532
|
+
trigger: "message",
|
|
5533
|
+
text: deliveryText,
|
|
5534
|
+
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5535
|
+
message_id: messageId,
|
|
5536
|
+
compact: opts.compact !== false,
|
|
5537
|
+
at,
|
|
5538
|
+
},
|
|
5539
|
+
};
|
|
5459
5540
|
});
|
|
5460
5541
|
}
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5542
|
+
const started = spawnLaneWorker(hqRoot, lane, {
|
|
5543
|
+
env: ctx.env,
|
|
5544
|
+
modelDeps: {
|
|
5545
|
+
readFileSync: ctx.readFileSync,
|
|
5546
|
+
probe: ctx.probeProvider,
|
|
5547
|
+
env: ctx.env,
|
|
5548
|
+
},
|
|
5549
|
+
existsSync: ctx.existsSync,
|
|
5550
|
+
spawnProcess: ctx.spawnProcess,
|
|
5551
|
+
proveDetached: ctx.proveDetached,
|
|
5552
|
+
killProcess: ctx.killProcess,
|
|
5553
|
+
now: ctx.now,
|
|
5554
|
+
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
5555
|
+
runCompaction: ctx.runCompaction,
|
|
5556
|
+
locateTranscript: ctx.locateTranscript,
|
|
5557
|
+
}, {
|
|
5558
|
+
trigger: {
|
|
5559
|
+
kind: "message",
|
|
5560
|
+
text: deliveryText,
|
|
5470
5561
|
message_id: messageId,
|
|
5471
|
-
|
|
5472
|
-
at,
|
|
5562
|
+
...(opts.monitorEvent === undefined ? {} : { monitor_event: true }),
|
|
5473
5563
|
},
|
|
5474
|
-
|
|
5564
|
+
compact: opts.compact !== false,
|
|
5565
|
+
effort: resumeEffort ?? recordedResumeEffort(lane),
|
|
5566
|
+
});
|
|
5567
|
+
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
5568
|
+
return started;
|
|
5475
5569
|
});
|
|
5476
|
-
}
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
probe: ctx.probeProvider,
|
|
5482
|
-
env: ctx.env,
|
|
5483
|
-
},
|
|
5484
|
-
existsSync: ctx.existsSync,
|
|
5485
|
-
spawnProcess: ctx.spawnProcess,
|
|
5486
|
-
proveDetached: ctx.proveDetached,
|
|
5487
|
-
killProcess: ctx.killProcess,
|
|
5488
|
-
now: ctx.now,
|
|
5489
|
-
resolveMachineIdentity: ctx.resolveMachineIdentity,
|
|
5490
|
-
runCompaction: ctx.runCompaction,
|
|
5491
|
-
locateTranscript: ctx.locateTranscript,
|
|
5492
|
-
}, {
|
|
5493
|
-
trigger: { kind: "message", text, message_id: messageId },
|
|
5494
|
-
compact: opts.compact !== false,
|
|
5495
|
-
effort: resumeEffort ?? recordedResumeEffort(lane),
|
|
5496
|
-
});
|
|
5497
|
-
archiveResumeMessage(hqRoot, laneId, dropboxMessage);
|
|
5498
|
-
return started;
|
|
5499
|
-
});
|
|
5500
|
-
}, ctx.sleep);
|
|
5570
|
+
}, ctx.sleep);
|
|
5571
|
+
},
|
|
5572
|
+
}, mechanism);
|
|
5573
|
+
if (dispatched.mechanism === "resume")
|
|
5574
|
+
resumed = dispatched.value;
|
|
5501
5575
|
}
|
|
5502
5576
|
// Every accepted message moves the compaction reference forward.
|
|
5503
5577
|
let next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
@@ -5736,8 +5810,6 @@ async function runClaudeBalanceMoveTarget(ctx, laneId, opts) {
|
|
|
5736
5810
|
});
|
|
5737
5811
|
}
|
|
5738
5812
|
function laneWithRecoveredProviderSession(hqRoot, lane) {
|
|
5739
|
-
if (lane.provider === "grok")
|
|
5740
|
-
return lane;
|
|
5741
5813
|
const manifest = readSpawnManifest(laneRunDir(hqRoot, lane.lane_id));
|
|
5742
5814
|
const sessionId = (typeof lane.provider_session_id === "string" && lane.provider_session_id.trim()
|
|
5743
5815
|
? lane.provider_session_id
|
|
@@ -5795,8 +5867,8 @@ async function runResume(ctx, laneId, opts) {
|
|
|
5795
5867
|
state: lane.state,
|
|
5796
5868
|
});
|
|
5797
5869
|
}
|
|
5798
|
-
if (opts.account !== undefined && !
|
|
5799
|
-
throw new LanesError("provider_session_not_portable",
|
|
5870
|
+
if (opts.account !== undefined && !providerSessionPortable(lane.provider)) {
|
|
5871
|
+
throw new LanesError("provider_session_not_portable", sessionNotPortableMessage(lane.provider), { lane_id: laneId, provider: lane.provider });
|
|
5800
5872
|
}
|
|
5801
5873
|
if (lane.state === "queued") {
|
|
5802
5874
|
throw new LanesError("lane_not_resumable", `Lane ${laneId} is queued; use hq lanes admit ${laneId} to request admission`, { lane_id: laneId, state: lane.state, admission_command: `hq lanes admit ${laneId}` });
|
|
@@ -6384,7 +6456,7 @@ function drainHookStatusFor(ctx, lane) {
|
|
|
6384
6456
|
const account = config.providers[lane.provider].accounts[lane.account];
|
|
6385
6457
|
if (account === undefined)
|
|
6386
6458
|
return null;
|
|
6387
|
-
const home = canonicalizeHome(account.home, {
|
|
6459
|
+
const home = canonicalizeHome(effectiveAccountHome(account, ctx.hostname()).home, {
|
|
6388
6460
|
homedir: ctx.homedir,
|
|
6389
6461
|
realpathSync: (p) => fs.realpathSync(p),
|
|
6390
6462
|
existsSync: (p) => fs.existsSync(p),
|
|
@@ -7636,6 +7708,7 @@ async function runAccountsList(ctx, opts) {
|
|
|
7636
7708
|
const quota = loadQuotaState(hqRoot);
|
|
7637
7709
|
const accounts = providers.flatMap((provider) => listAccountStatuses(provider, config, {
|
|
7638
7710
|
existsSync: ctx.existsSync,
|
|
7711
|
+
hostname: ctx.hostname(),
|
|
7639
7712
|
probe: ctx.probeProvider,
|
|
7640
7713
|
env: ctx.env,
|
|
7641
7714
|
}).map((status) => {
|
|
@@ -7651,6 +7724,7 @@ async function runAccountsList(ctx, opts) {
|
|
|
7651
7724
|
});
|
|
7652
7725
|
return {
|
|
7653
7726
|
...status,
|
|
7727
|
+
effective_home: status.home,
|
|
7654
7728
|
model: accountCfg?.model ?? null,
|
|
7655
7729
|
effort: accountCfg?.effort ?? null,
|
|
7656
7730
|
effective_model: model ?? null,
|
|
@@ -7814,9 +7888,23 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
7814
7888
|
throw new LanesError("device_auth_unsupported", "--device-auth is supported only for codex account login", { provider });
|
|
7815
7889
|
}
|
|
7816
7890
|
const registeredAccount = config.providers[provider].accounts[id];
|
|
7817
|
-
const
|
|
7818
|
-
|
|
7819
|
-
|
|
7891
|
+
const hostname = ctx.hostname().trim();
|
|
7892
|
+
if (!hostname) {
|
|
7893
|
+
throw new LanesError("host_identity_unknown", "Cannot resolve this host's hostname for account login", {});
|
|
7894
|
+
}
|
|
7895
|
+
const requestedHome = opts.home?.trim();
|
|
7896
|
+
if (opts.home !== undefined && !requestedHome) {
|
|
7897
|
+
throw new LanesError("invalid_account_home", "--home must be a non-empty path", { provider, account: id });
|
|
7898
|
+
}
|
|
7899
|
+
const hostScopedWriterEnabled = registeredAccount !== undefined && requestedHome !== undefined
|
|
7900
|
+
? await hostScopedAccountHomesEnabled(ctx)
|
|
7901
|
+
: false;
|
|
7902
|
+
const homeRaw = requestedHome ??
|
|
7903
|
+
(registeredAccount !== undefined
|
|
7904
|
+
? effectiveAccountHome(registeredAccount, hostname).home
|
|
7905
|
+
: id === "a"
|
|
7906
|
+
? DEFAULT_ACCOUNT_HOMES[provider]
|
|
7907
|
+
: `~/.${provider}-${id}`);
|
|
7820
7908
|
const homeResolved = homeRaw.startsWith("~") || path.isAbsolute(homeRaw)
|
|
7821
7909
|
? homeRaw
|
|
7822
7910
|
: resolveCallerPath(callerCwd, homeRaw);
|
|
@@ -7825,20 +7913,43 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
7825
7913
|
realpathSync: (p) => fs.realpathSync(p),
|
|
7826
7914
|
existsSync: (p) => fs.existsSync(p),
|
|
7827
7915
|
});
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
|
|
7831
|
-
|
|
7832
|
-
|
|
7916
|
+
const nextAccount = registeredAccount === undefined
|
|
7917
|
+
? { home, enabled: true }
|
|
7918
|
+
: { ...registeredAccount, enabled: true };
|
|
7919
|
+
if (registeredAccount !== undefined) {
|
|
7920
|
+
if (hostScopedWriterEnabled && requestedHome !== undefined) {
|
|
7921
|
+
if (home === registeredAccount.home) {
|
|
7922
|
+
if (nextAccount.hosts !== undefined) {
|
|
7923
|
+
delete nextAccount.hosts[hostname];
|
|
7924
|
+
if (Object.keys(nextAccount.hosts).length === 0)
|
|
7925
|
+
delete nextAccount.hosts;
|
|
7926
|
+
}
|
|
7927
|
+
}
|
|
7928
|
+
else {
|
|
7929
|
+
nextAccount.hosts = {
|
|
7930
|
+
...(registeredAccount.hosts ?? {}),
|
|
7931
|
+
[hostname]: { home },
|
|
7932
|
+
};
|
|
7933
|
+
}
|
|
7934
|
+
}
|
|
7935
|
+
else if (requestedHome !== undefined) {
|
|
7936
|
+
// With the rollout off, retain the legacy shared-home write shape.
|
|
7937
|
+
nextAccount.home = home;
|
|
7833
7938
|
}
|
|
7834
7939
|
}
|
|
7940
|
+
delete nextAccount.disabled_reason;
|
|
7941
|
+
assertUniqueAccountHomes(provider, {
|
|
7942
|
+
...config.providers[provider].accounts,
|
|
7943
|
+
[id]: nextAccount,
|
|
7944
|
+
});
|
|
7835
7945
|
ensureDir(home);
|
|
7836
7946
|
const defaultId = config.providers[provider].default_account;
|
|
7837
|
-
const
|
|
7838
|
-
? config.providers[provider].accounts[defaultId]
|
|
7947
|
+
const defaultAccount = defaultId !== undefined
|
|
7948
|
+
? config.providers[provider].accounts[defaultId]
|
|
7949
|
+
: Object.values(config.providers[provider].accounts).find((account) => account.enabled);
|
|
7950
|
+
const defaultHome = (defaultAccount !== undefined
|
|
7951
|
+
? effectiveAccountHome(defaultAccount, hostname).home
|
|
7839
7952
|
: undefined) ??
|
|
7840
|
-
Object.values(config.providers[provider].accounts).find((account) => account.enabled)
|
|
7841
|
-
?.home ??
|
|
7842
7953
|
canonicalizeHome(DEFAULT_ACCOUNT_HOMES[provider], {
|
|
7843
7954
|
homedir: ctx.homedir,
|
|
7844
7955
|
realpathSync: (p) => fs.realpathSync(p),
|
|
@@ -7864,12 +7975,10 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
7864
7975
|
if ((grokNeedsRails || copied) && !railsPresent(home)) {
|
|
7865
7976
|
throw new LanesError("rails_missing", `Hook rails are missing under ${home}; account not enabled`, { provider, account: id, home });
|
|
7866
7977
|
}
|
|
7867
|
-
config.providers[provider].accounts[id] =
|
|
7868
|
-
home,
|
|
7869
|
-
enabled: true,
|
|
7870
|
-
};
|
|
7978
|
+
config.providers[provider].accounts[id] = nextAccount;
|
|
7871
7979
|
const status = accountStatus(provider, id, config, {
|
|
7872
7980
|
existsSync: ctx.existsSync,
|
|
7981
|
+
hostname,
|
|
7873
7982
|
probe: ctx.probeProvider,
|
|
7874
7983
|
env: ctx.env,
|
|
7875
7984
|
});
|
|
@@ -7881,6 +7990,93 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
7881
7990
|
writeLanesConfig(hqRoot, config);
|
|
7882
7991
|
printJson({ ok: true, action: "accounts.login", account: status });
|
|
7883
7992
|
}
|
|
7993
|
+
async function runAccountsSetHome(ctx, providerRaw, accountIdRaw, homeRaw, opts) {
|
|
7994
|
+
if (!(await hostScopedAccountHomesEnabled(ctx))) {
|
|
7995
|
+
throw new LanesError("host_scoped_account_homes_disabled", "Setting a host-specific account home requires the lanes.host-scoped-account-homes flag", { flag: LANE_HOST_SCOPED_ACCOUNT_HOMES_FLAG });
|
|
7996
|
+
}
|
|
7997
|
+
const callerCwd = ctx.callerCwd();
|
|
7998
|
+
const hqRoot = ctx.hqRoot();
|
|
7999
|
+
const provider = parseLaneProvider(providerRaw);
|
|
8000
|
+
const id = accountIdRaw.trim();
|
|
8001
|
+
if (!id) {
|
|
8002
|
+
throw new AccountResolutionError("unknown_account", "account-id must be non-empty", {
|
|
8003
|
+
provider,
|
|
8004
|
+
account: accountIdRaw,
|
|
8005
|
+
enabled: [],
|
|
8006
|
+
});
|
|
8007
|
+
}
|
|
8008
|
+
const hostname = opts.host === undefined ? ctx.hostname().trim() : opts.host.trim();
|
|
8009
|
+
if (!hostname || (opts.host !== undefined && hostname !== opts.host)) {
|
|
8010
|
+
throw new LanesError("invalid_host", "--host must be a non-empty hostname without surrounding whitespace", {
|
|
8011
|
+
host: opts.host ?? "",
|
|
8012
|
+
});
|
|
8013
|
+
}
|
|
8014
|
+
if (!homeRaw.trim()) {
|
|
8015
|
+
throw new LanesError("invalid_account_home", "home path must be a non-empty path", {
|
|
8016
|
+
provider,
|
|
8017
|
+
account: id,
|
|
8018
|
+
});
|
|
8019
|
+
}
|
|
8020
|
+
const config = loadLanesConfig(hqRoot);
|
|
8021
|
+
const account = config.providers[provider].accounts[id];
|
|
8022
|
+
if (account === undefined) {
|
|
8023
|
+
throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(id)} for ${provider}`, {
|
|
8024
|
+
provider,
|
|
8025
|
+
account: id,
|
|
8026
|
+
enabled: Object.keys(config.providers[provider].accounts).filter((key) => config.providers[provider].accounts[key]?.enabled),
|
|
8027
|
+
});
|
|
8028
|
+
}
|
|
8029
|
+
const homePath = homeRaw.trim();
|
|
8030
|
+
const isCurrentHost = hostname === ctx.hostname().trim();
|
|
8031
|
+
let home;
|
|
8032
|
+
if (isCurrentHost) {
|
|
8033
|
+
const homeResolved = homePath.startsWith("~") || path.isAbsolute(homePath)
|
|
8034
|
+
? homePath
|
|
8035
|
+
: resolveCallerPath(callerCwd, homePath);
|
|
8036
|
+
home = canonicalizeHome(homeResolved, {
|
|
8037
|
+
homedir: ctx.homedir,
|
|
8038
|
+
realpathSync: (p) => fs.realpathSync(p),
|
|
8039
|
+
existsSync: (p) => fs.existsSync(p),
|
|
8040
|
+
});
|
|
8041
|
+
}
|
|
8042
|
+
else if (homePath === "~" || homePath.startsWith("~/")) {
|
|
8043
|
+
home = homePath;
|
|
8044
|
+
}
|
|
8045
|
+
else if (path.isAbsolute(homePath)) {
|
|
8046
|
+
home = path.normalize(homePath);
|
|
8047
|
+
}
|
|
8048
|
+
else {
|
|
8049
|
+
throw new LanesError("invalid_remote_account_home", "A home for another host must be an absolute path or start with ~/", { provider, account: id, hostname, home: homePath });
|
|
8050
|
+
}
|
|
8051
|
+
for (const [otherId, other] of Object.entries(config.providers[provider].accounts)) {
|
|
8052
|
+
if (otherId !== id && effectiveAccountHome(other, hostname).home === home) {
|
|
8053
|
+
throw new LanesError("duplicate_account_home", `Home ${home} is already registered as ${provider} account ${otherId} on host ${hostname}`, { provider, accounts: [otherId, id], home, hostname });
|
|
8054
|
+
}
|
|
8055
|
+
}
|
|
8056
|
+
if (home === account.home) {
|
|
8057
|
+
if (account.hosts !== undefined) {
|
|
8058
|
+
delete account.hosts[hostname];
|
|
8059
|
+
if (Object.keys(account.hosts).length === 0)
|
|
8060
|
+
delete account.hosts;
|
|
8061
|
+
}
|
|
8062
|
+
}
|
|
8063
|
+
else {
|
|
8064
|
+
const hosts = Object.assign(Object.create(null), account.hosts ?? {});
|
|
8065
|
+
hosts[hostname] = { home };
|
|
8066
|
+
account.hosts = hosts;
|
|
8067
|
+
}
|
|
8068
|
+
assertUniqueAccountHomes(provider, config.providers[provider].accounts);
|
|
8069
|
+
writeLanesConfig(hqRoot, config);
|
|
8070
|
+
printJson({
|
|
8071
|
+
ok: true,
|
|
8072
|
+
action: "accounts.set-home",
|
|
8073
|
+
provider,
|
|
8074
|
+
account: id,
|
|
8075
|
+
hostname,
|
|
8076
|
+
home,
|
|
8077
|
+
home_source: home === account.home ? "default" : "host",
|
|
8078
|
+
});
|
|
8079
|
+
}
|
|
7884
8080
|
async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
7885
8081
|
const hqRoot = ctx.hqRoot();
|
|
7886
8082
|
const provider = parseLaneProvider(providerRaw);
|
|
@@ -7897,6 +8093,7 @@ async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
|
7897
8093
|
}
|
|
7898
8094
|
const status = accountStatus(provider, accountId, config, {
|
|
7899
8095
|
existsSync: ctx.existsSync,
|
|
8096
|
+
hostname: ctx.hostname(),
|
|
7900
8097
|
probe: ctx.probeProvider,
|
|
7901
8098
|
env: ctx.env,
|
|
7902
8099
|
});
|
|
@@ -7913,6 +8110,7 @@ async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
|
7913
8110
|
action: "accounts.enable",
|
|
7914
8111
|
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
|
|
7915
8112
|
existsSync: ctx.existsSync,
|
|
8113
|
+
hostname: ctx.hostname(),
|
|
7916
8114
|
probe: ctx.probeProvider,
|
|
7917
8115
|
env: ctx.env,
|
|
7918
8116
|
}),
|
|
@@ -7941,6 +8139,7 @@ async function runAccountsDisable(ctx, providerRaw, accountId, opts) {
|
|
|
7941
8139
|
action: "accounts.disable",
|
|
7942
8140
|
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
|
|
7943
8141
|
existsSync: ctx.existsSync,
|
|
8142
|
+
hostname: ctx.hostname(),
|
|
7944
8143
|
probe: ctx.probeProvider,
|
|
7945
8144
|
env: ctx.env,
|
|
7946
8145
|
}),
|
|
@@ -8304,6 +8503,38 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8304
8503
|
.action(wrap(async (laneId, opts) => {
|
|
8305
8504
|
await runMessage(ctx, laneId, opts);
|
|
8306
8505
|
}));
|
|
8506
|
+
lanes
|
|
8507
|
+
.command("_monitor-message", { hidden: true })
|
|
8508
|
+
.argument("<lane-id>", "Lane id")
|
|
8509
|
+
.action(wrap(async (laneId) => {
|
|
8510
|
+
const input = ctx.readStdin();
|
|
8511
|
+
let payload;
|
|
8512
|
+
try {
|
|
8513
|
+
payload = JSON.parse(input);
|
|
8514
|
+
}
|
|
8515
|
+
catch (err) {
|
|
8516
|
+
throw new LanesError("monitor_lane_payload_invalid", `Monitor lane delivery payload is invalid JSON: ${err instanceof Error ? err.message : String(err)}.`, {});
|
|
8517
|
+
}
|
|
8518
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
8519
|
+
throw new LanesError("monitor_lane_payload_invalid", "Monitor lane delivery payload must be an object.", {});
|
|
8520
|
+
}
|
|
8521
|
+
const value = payload;
|
|
8522
|
+
if (typeof value.text !== "string" ||
|
|
8523
|
+
typeof value.monitor_id !== "string" ||
|
|
8524
|
+
typeof value.description !== "string" ||
|
|
8525
|
+
!Number.isInteger(value.sequence) ||
|
|
8526
|
+
Number(value.sequence) < 1) {
|
|
8527
|
+
throw new LanesError("monitor_lane_payload_invalid", "Monitor lane delivery payload is missing text, monitor metadata, or sequence.", {});
|
|
8528
|
+
}
|
|
8529
|
+
await runMessage(ctx, laneId, {
|
|
8530
|
+
text: value.text,
|
|
8531
|
+
monitorEvent: {
|
|
8532
|
+
monitor_id: value.monitor_id,
|
|
8533
|
+
description: value.description,
|
|
8534
|
+
sequence: Number(value.sequence),
|
|
8535
|
+
},
|
|
8536
|
+
});
|
|
8537
|
+
}));
|
|
8307
8538
|
lanes
|
|
8308
8539
|
.command("resume")
|
|
8309
8540
|
.description("Resume an interrupted, done or blocked lane on its recorded provider session")
|
|
@@ -8423,6 +8654,17 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8423
8654
|
.action(wrap(async (opts) => {
|
|
8424
8655
|
await runAccountsBalance(ctx, opts);
|
|
8425
8656
|
}));
|
|
8657
|
+
accounts
|
|
8658
|
+
.command("set-home")
|
|
8659
|
+
.description("Set the host-specific home for an existing provider account")
|
|
8660
|
+
.argument("<provider>", "claude | codex | grok")
|
|
8661
|
+
.argument("<account-id>", "Existing account id")
|
|
8662
|
+
.argument("<path>", "Home directory for this host")
|
|
8663
|
+
.option("--host <hostname>", "Host key (defaults to this host's hostname)")
|
|
8664
|
+
.option("--json", "JSON on stdout (always)")
|
|
8665
|
+
.action(wrap(async (provider, accountId, home, opts) => {
|
|
8666
|
+
await runAccountsSetHome(ctx, provider, accountId, home, opts);
|
|
8667
|
+
}));
|
|
8426
8668
|
accounts
|
|
8427
8669
|
.command("login")
|
|
8428
8670
|
.description("Create a home, run the vendor login, and register the account")
|