@indigoai-us/hq-cli 5.203.0 → 5.205.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 +27 -0
- package/dist/command-catalog.generated.d.ts +33 -0
- package/dist/command-catalog.generated.js +43 -0
- package/dist/commands/lanes.d.ts +3 -0
- package/dist/commands/lanes.js +253 -50
- 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/admission.js +6 -0
- 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/hire-grants.d.ts +4 -0
- package/dist/lib/lanes/hire-grants.js +62 -0
- 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/keep-alive.d.ts +8 -0
- package/dist/lib/lanes/keep-alive.js +14 -0
- package/dist/lib/lanes/liveness.d.ts +1 -0
- package/dist/lib/lanes/liveness.js +3 -1
- package/dist/lib/lanes/outpost-dispatch.d.ts +1 -0
- package/dist/lib/lanes/outpost-dispatch.js +2 -0
- package/dist/lib/lanes/profile.d.ts +1 -0
- package/dist/lib/lanes/profile.js +8 -0
- package/dist/lib/lanes/provider-rejection.d.ts +1 -1
- package/dist/lib/lanes/provider-rejection.js +15 -5
- package/dist/lib/lanes/reconcile.d.ts +3 -0
- package/dist/lib/lanes/reconcile.js +293 -13
- package/dist/lib/lanes/spawn.d.ts +1 -0
- package/dist/lib/lanes/spawn.js +30 -14
- package/dist/lib/lanes/store.js +25 -0
- package/dist/lib/lanes/types.d.ts +15 -0
- package/dist/lib/lanes/types.js +1 -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,11 +33,12 @@ 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, terminalChildResumeHireGrantError, 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";
|
|
42
44
|
import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
|
|
@@ -59,6 +61,7 @@ const VENDOR_LOGIN = {
|
|
|
59
61
|
export { emitLaneDecisionEvents };
|
|
60
62
|
export const LANE_OUTPOST_LOAD_BALANCING_FLAG = "lanes.outpost-load-balancing";
|
|
61
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";
|
|
62
65
|
function defaultLanePlacementEnabled() {
|
|
63
66
|
return resolveProcessFlagGate(LANE_OUTPOST_LOAD_BALANCING_FLAG, { fallback: false }, () => false);
|
|
64
67
|
}
|
|
@@ -82,6 +85,15 @@ function defaultLaneOutpostQuestionVisibilityEnabled(company, env) {
|
|
|
82
85
|
env,
|
|
83
86
|
});
|
|
84
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
|
+
}
|
|
85
97
|
function withoutProviderHome(value) {
|
|
86
98
|
if (Array.isArray(value))
|
|
87
99
|
return value.map(withoutProviderHome);
|
|
@@ -286,6 +298,8 @@ function resolveDeps(deps = {}) {
|
|
|
286
298
|
((company) => defaultLaneOutpostQuestionVisibilityEnabled(company, env)),
|
|
287
299
|
laneTrackerEnabled: deps.laneTrackerEnabled ?? defaultLaneTrackerEnabled,
|
|
288
300
|
laneTrackerMeshEnabled: deps.laneTrackerMeshEnabled ?? defaultLaneTrackerMeshEnabled,
|
|
301
|
+
laneHostScopedAccountHomesEnabled: deps.laneHostScopedAccountHomesEnabled ??
|
|
302
|
+
((company) => defaultLaneHostScopedAccountHomesEnabled(company, env)),
|
|
289
303
|
trackerActor: deps.trackerActor ?? defaultTrackerActor,
|
|
290
304
|
};
|
|
291
305
|
}
|
|
@@ -298,6 +312,7 @@ function livenessDepsFromCtx(ctx) {
|
|
|
298
312
|
env: ctx.env,
|
|
299
313
|
existsSync: ctx.existsSync,
|
|
300
314
|
readFileSync: ctx.readFileSync,
|
|
315
|
+
hostname: ctx.hostname,
|
|
301
316
|
};
|
|
302
317
|
}
|
|
303
318
|
function reconcileHostFromCtx(ctx) {
|
|
@@ -849,33 +864,15 @@ async function withResumeHireAdmission(ctx, lane, fn) {
|
|
|
849
864
|
}
|
|
850
865
|
if (!await ctx.laneHireGrantsEnabled(lane.company))
|
|
851
866
|
return fn();
|
|
852
|
-
return withHireGrantAdmission(hqRoot, lane.senior.id, (
|
|
867
|
+
return withHireGrantAdmission(hqRoot, lane.senior.id, (snapshot) => {
|
|
853
868
|
const current = readLane(hqRoot, lane.lane_id);
|
|
854
869
|
// A different resume already reactivated this lane; it occupies the slot
|
|
855
870
|
// in the shared snapshot and is not a second hire.
|
|
856
871
|
if (!isTerminalLaneState(current.state))
|
|
857
872
|
return fn();
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
if (children.length >= (parent.hire_grant ?? 0)) {
|
|
862
|
-
throw hireGrantCapacityError(parent, children);
|
|
863
|
-
}
|
|
864
|
-
const requestedGrant = current.hire_grant ?? 0;
|
|
865
|
-
if (requestedGrant > availableChildGrant) {
|
|
866
|
-
throw new LanesError("hire_grant_carve_exceeded", `Senior lane ${parent.lane_id} has ${availableChildGrant} unassigned hire grant remaining, but terminal child ${current.lane_id} needs ${requestedGrant} to resume. Ask its senior for more: write an ask envelope with the desired total grant and why; the senior runs hq lanes grant ${parent.lane_id} --hire <n>.`, {
|
|
867
|
-
senior_lane_id: parent.lane_id,
|
|
868
|
-
parent_grant: parentGrant,
|
|
869
|
-
alive_children: children.map((child) => child.lane_id),
|
|
870
|
-
assigned_child_grants: assignedChildHireGrants(children),
|
|
871
|
-
requested_hire_grant: requestedGrant,
|
|
872
|
-
available_hire_grant: availableChildGrant,
|
|
873
|
-
ask_guidance: {
|
|
874
|
-
envelope: "decision=ask; include the desired total grant and the reason",
|
|
875
|
-
senior_command: `hq lanes grant ${parent.lane_id} --hire <n>`,
|
|
876
|
-
},
|
|
877
|
-
});
|
|
878
|
-
}
|
|
873
|
+
const refusal = terminalChildResumeHireGrantError(current, snapshot);
|
|
874
|
+
if (refusal !== null)
|
|
875
|
+
throw refusal;
|
|
879
876
|
return fn();
|
|
880
877
|
});
|
|
881
878
|
}
|
|
@@ -1004,6 +1001,7 @@ async function loadHostCapacitySnapshots(ctx, hqRoot, config = loadLanesConfig(h
|
|
|
1004
1001
|
lanes: loadAllLanes(hqRoot),
|
|
1005
1002
|
config,
|
|
1006
1003
|
existsSync: ctx.existsSync,
|
|
1004
|
+
hostname: ctx.hostname(),
|
|
1007
1005
|
now: ctx.now(),
|
|
1008
1006
|
});
|
|
1009
1007
|
const remote = await Promise.all(remoteIds.map(async (id) => {
|
|
@@ -1118,6 +1116,7 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
|
|
|
1118
1116
|
existsSync: ctx.existsSync,
|
|
1119
1117
|
probe: ctx.probeProvider,
|
|
1120
1118
|
env: ctx.env,
|
|
1119
|
+
hostname: ctx.hostname(),
|
|
1121
1120
|
now: ctx.now(),
|
|
1122
1121
|
});
|
|
1123
1122
|
const snapshots = [{
|
|
@@ -1140,6 +1139,7 @@ async function loadClaudeBalanceHostSnapshots(ctx, hqRoot, config) {
|
|
|
1140
1139
|
existsSync: ctx.existsSync,
|
|
1141
1140
|
probe: ctx.probeProvider,
|
|
1142
1141
|
env: ctx.env,
|
|
1142
|
+
hostname: ctx.hostname(),
|
|
1143
1143
|
now: ctx.now(),
|
|
1144
1144
|
}),
|
|
1145
1145
|
lanes: actualLocalLanes,
|
|
@@ -2196,6 +2196,7 @@ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
|
|
|
2196
2196
|
try {
|
|
2197
2197
|
const resolved = resolveAccount(lane.provider, lane.account, config, {
|
|
2198
2198
|
hqRoot,
|
|
2199
|
+
hostname: os.hostname(),
|
|
2199
2200
|
accountDeps: {
|
|
2200
2201
|
probe: modelDeps.probe,
|
|
2201
2202
|
env: modelDeps.env,
|
|
@@ -2203,6 +2204,7 @@ export function applyOutpostAccountPickup(hqRoot, laneId, modelDeps = {}) {
|
|
|
2203
2204
|
});
|
|
2204
2205
|
const status = accountStatus(lane.provider, resolved.id, config, {
|
|
2205
2206
|
existsSync: (p) => fs.existsSync(p),
|
|
2207
|
+
hostname: os.hostname(),
|
|
2206
2208
|
probe: modelDeps.probe,
|
|
2207
2209
|
env: modelDeps.env,
|
|
2208
2210
|
});
|
|
@@ -2449,6 +2451,11 @@ async function runCreate(ctx, opts) {
|
|
|
2449
2451
|
const profileEffort = profileDefaultsApply ? profile?.meta.effort : undefined;
|
|
2450
2452
|
const profileAccount = profileDefaultsApply ? profile?.meta.account : undefined;
|
|
2451
2453
|
const profileAccounts = profileDefaultsApply ? profile?.meta.accounts : undefined;
|
|
2454
|
+
const keepAlive = opts.keepAlive === true ||
|
|
2455
|
+
(profileDefaultsApply && profile?.meta.keep_alive === true);
|
|
2456
|
+
if (keepAlive && !providerSupportsSessionResume(provider)) {
|
|
2457
|
+
throw new LanesError("keep_alive_provider_unsupported", `Provider ${provider} cannot resume recorded sessions, so keep-alive is unavailable`, { provider });
|
|
2458
|
+
}
|
|
2452
2459
|
const briefAbs = resolveCallerPath(ctx.callerCwd(), briefFile);
|
|
2453
2460
|
if (!ctx.existsSync(briefAbs)) {
|
|
2454
2461
|
throw new LanesError("brief_not_found", `Brief file not found: ${briefAbs}`, {
|
|
@@ -2589,6 +2596,7 @@ async function runCreate(ctx, opts) {
|
|
|
2589
2596
|
}
|
|
2590
2597
|
const resolved = resolveAccount(provider, requested, config, {
|
|
2591
2598
|
hqRoot,
|
|
2599
|
+
hostname: ctx.hostname(),
|
|
2592
2600
|
cliModel,
|
|
2593
2601
|
model: cliModel ?? profileModel,
|
|
2594
2602
|
});
|
|
@@ -2597,6 +2605,7 @@ async function runCreate(ctx, opts) {
|
|
|
2597
2605
|
else if (profileAccount !== undefined && profileAccounts === undefined) {
|
|
2598
2606
|
const resolved = resolveAccount(provider, profileAccount, config, {
|
|
2599
2607
|
hqRoot,
|
|
2608
|
+
hostname: ctx.hostname(),
|
|
2600
2609
|
cliModel,
|
|
2601
2610
|
model: cliModel ?? profileModel,
|
|
2602
2611
|
});
|
|
@@ -2650,6 +2659,7 @@ async function runCreate(ctx, opts) {
|
|
|
2650
2659
|
const candidate = pinnedAccount ??
|
|
2651
2660
|
resolveAccount(provider, undefined, config, {
|
|
2652
2661
|
hqRoot,
|
|
2662
|
+
hostname: ctx.hostname(),
|
|
2653
2663
|
existsSync: ctx.existsSync,
|
|
2654
2664
|
accountDeps: { probe: ctx.probeProvider, env: ctx.env },
|
|
2655
2665
|
cliModel,
|
|
@@ -2669,6 +2679,7 @@ async function runCreate(ctx, opts) {
|
|
|
2669
2679
|
(profileAccounts === undefined || profileAccounts.includes(id)) &&
|
|
2670
2680
|
accountStatus(provider, id, config, {
|
|
2671
2681
|
existsSync: ctx.existsSync,
|
|
2682
|
+
hostname: ctx.hostname(),
|
|
2672
2683
|
probe: ctx.probeProvider,
|
|
2673
2684
|
env: ctx.env,
|
|
2674
2685
|
}).logged_in === true)
|
|
@@ -2679,7 +2690,10 @@ async function runCreate(ctx, opts) {
|
|
|
2679
2690
|
const quotaBlocked = [];
|
|
2680
2691
|
const quota = loadQuotaState(hqRoot);
|
|
2681
2692
|
for (const id of toValidate) {
|
|
2682
|
-
const
|
|
2693
|
+
const account = accounts[id];
|
|
2694
|
+
const home = account === undefined
|
|
2695
|
+
? undefined
|
|
2696
|
+
: effectiveAccountHome(account, ctx.hostname()).home;
|
|
2683
2697
|
if (home === undefined)
|
|
2684
2698
|
continue;
|
|
2685
2699
|
checkedAccounts.push(id);
|
|
@@ -2782,6 +2796,7 @@ async function runCreate(ctx, opts) {
|
|
|
2782
2796
|
existsSync: ctx.existsSync,
|
|
2783
2797
|
probe: ctx.probeProvider,
|
|
2784
2798
|
env: ctx.env,
|
|
2799
|
+
hostname: ctx.hostname(),
|
|
2785
2800
|
now: ctx.now(),
|
|
2786
2801
|
}),
|
|
2787
2802
|
lanes: localLanes,
|
|
@@ -2805,6 +2820,7 @@ async function runCreate(ctx, opts) {
|
|
|
2805
2820
|
pinnedAccount = resolveAccount(provider, undefined, config, {
|
|
2806
2821
|
hqRoot,
|
|
2807
2822
|
existsSync: ctx.existsSync,
|
|
2823
|
+
hostname: ctx.hostname(),
|
|
2808
2824
|
accountDeps: { probe: ctx.probeProvider, env: ctx.env },
|
|
2809
2825
|
cliModel,
|
|
2810
2826
|
model: cliModel ?? profileModel,
|
|
@@ -2898,6 +2914,7 @@ async function runCreate(ctx, opts) {
|
|
|
2898
2914
|
worker_profile_ignored: ignoredWorkerProfileFields(provider),
|
|
2899
2915
|
}
|
|
2900
2916
|
: {}),
|
|
2917
|
+
keep_alive: keepAlive,
|
|
2901
2918
|
provider,
|
|
2902
2919
|
account: pinnedAccount,
|
|
2903
2920
|
...(accountSelection !== undefined ? { account_selection: accountSelection } : {}),
|
|
@@ -3353,6 +3370,7 @@ async function dispatchOutpostLaneCommand(ctx, lane, action, options) {
|
|
|
3353
3370
|
model: options.model,
|
|
3354
3371
|
effort: options.effort,
|
|
3355
3372
|
worker: options.worker,
|
|
3373
|
+
keepAlive: options.keepAlive,
|
|
3356
3374
|
hire: options.hire,
|
|
3357
3375
|
grantContext,
|
|
3358
3376
|
compact: options.compact,
|
|
@@ -4029,6 +4047,7 @@ async function runCapacity(ctx) {
|
|
|
4029
4047
|
existsSync: ctx.existsSync,
|
|
4030
4048
|
probe: ctx.probeProvider,
|
|
4031
4049
|
env: ctx.env,
|
|
4050
|
+
hostname: ctx.hostname(),
|
|
4032
4051
|
now: ctx.now(),
|
|
4033
4052
|
});
|
|
4034
4053
|
printJson(snapshot);
|
|
@@ -4478,6 +4497,13 @@ async function runShow(ctx, laneId) {
|
|
|
4478
4497
|
effort_source: lane.effort_source ?? null,
|
|
4479
4498
|
terminal_reason: lane.terminal_reason ?? null,
|
|
4480
4499
|
terminal_details: lane.terminal_details ?? null,
|
|
4500
|
+
keep_alive_state: {
|
|
4501
|
+
enabled: lane.keep_alive === true,
|
|
4502
|
+
consecutive_auto_resumes: lane.keep_alive_resume_count ?? 0,
|
|
4503
|
+
last_auto_resume_at: lane.keep_alive_last_resume_at ?? null,
|
|
4504
|
+
next_eligible_at: lane.keep_alive_next_eligible_at ?? null,
|
|
4505
|
+
exhausted_at: lane.keep_alive_exhausted_at ?? null,
|
|
4506
|
+
},
|
|
4481
4507
|
last_message_at: lane.last_message_at ?? null,
|
|
4482
4508
|
last_compaction: lane.last_compaction
|
|
4483
4509
|
? { ...lane.last_compaction, after: compactionAfterUsage(storedLane) }
|
|
@@ -4873,6 +4899,7 @@ function validateResumeProfileModel(ctx, lane, profile) {
|
|
|
4873
4899
|
const account = resolveAccount(lane.provider, lane.account, config, {
|
|
4874
4900
|
hqRoot,
|
|
4875
4901
|
existsSync: ctx.existsSync,
|
|
4902
|
+
hostname: ctx.hostname(),
|
|
4876
4903
|
model: profile.meta.model,
|
|
4877
4904
|
});
|
|
4878
4905
|
validateProfileLaneModel(lane.provider, profile.meta.model, account.home, profile.id, {
|
|
@@ -4891,6 +4918,9 @@ function applyResumeProfileAndEffort(current, next, input) {
|
|
|
4891
4918
|
next.worker_profile_hash = profile.contentHash;
|
|
4892
4919
|
next.worker_profile_ignored = ignoredWorkerProfileFields(current.provider);
|
|
4893
4920
|
next.worker_policy_delivery = undefined;
|
|
4921
|
+
if (profile.meta.keep_alive !== undefined) {
|
|
4922
|
+
next.keep_alive = profile.meta.keep_alive;
|
|
4923
|
+
}
|
|
4894
4924
|
if (next.model_source !== "cli") {
|
|
4895
4925
|
if (profile.meta.provider === current.provider && profile.meta.model !== undefined) {
|
|
4896
4926
|
next.model = profile.meta.model;
|
|
@@ -4906,6 +4936,8 @@ function applyResumeProfileAndEffort(current, next, input) {
|
|
|
4906
4936
|
next.effort = input.effort.effort;
|
|
4907
4937
|
next.effort_source = input.effort.source;
|
|
4908
4938
|
}
|
|
4939
|
+
if (input.keepAlive !== undefined)
|
|
4940
|
+
next.keep_alive = input.keepAlive;
|
|
4909
4941
|
}
|
|
4910
4942
|
function recordResumeSetup(ctx, lane, input) {
|
|
4911
4943
|
if (input.cliModel !== undefined) {
|
|
@@ -4919,7 +4951,7 @@ function recordResumeSetup(ctx, lane, input) {
|
|
|
4919
4951
|
if (account === undefined) {
|
|
4920
4952
|
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: [] });
|
|
4921
4953
|
}
|
|
4922
|
-
validateLaneModel(lane.provider, model, account.home, {
|
|
4954
|
+
validateLaneModel(lane.provider, model, effectiveAccountHome(account, ctx.hostname()).home, {
|
|
4923
4955
|
readFileSync: ctx.readFileSync,
|
|
4924
4956
|
probe: ctx.probeProvider,
|
|
4925
4957
|
env: ctx.env,
|
|
@@ -4927,6 +4959,7 @@ function recordResumeSetup(ctx, lane, input) {
|
|
|
4927
4959
|
}
|
|
4928
4960
|
if (input.profile === undefined &&
|
|
4929
4961
|
input.effort === undefined &&
|
|
4962
|
+
input.keepAlive === undefined &&
|
|
4930
4963
|
input.cliModel === undefined &&
|
|
4931
4964
|
input.meshCompany === undefined)
|
|
4932
4965
|
return lane;
|
|
@@ -4949,6 +4982,23 @@ function recordResumeSetup(ctx, lane, input) {
|
|
|
4949
4982
|
return next;
|
|
4950
4983
|
});
|
|
4951
4984
|
}
|
|
4985
|
+
function hasKeepAliveProgress(lane) {
|
|
4986
|
+
return (lane.keep_alive_resume_count ?? 0) !== 0 ||
|
|
4987
|
+
lane.keep_alive_last_resume_at !== undefined ||
|
|
4988
|
+
lane.keep_alive_next_eligible_at !== undefined ||
|
|
4989
|
+
lane.keep_alive_exhausted_at !== undefined;
|
|
4990
|
+
}
|
|
4991
|
+
function resetKeepAliveProgress(lane) {
|
|
4992
|
+
if (!hasKeepAliveProgress(lane))
|
|
4993
|
+
return lane;
|
|
4994
|
+
return {
|
|
4995
|
+
...lane,
|
|
4996
|
+
keep_alive_resume_count: 0,
|
|
4997
|
+
keep_alive_last_resume_at: undefined,
|
|
4998
|
+
keep_alive_next_eligible_at: undefined,
|
|
4999
|
+
keep_alive_exhausted_at: undefined,
|
|
5000
|
+
};
|
|
5001
|
+
}
|
|
4952
5002
|
/** Resolve an explicit resume override or newly attached profile like create does. */
|
|
4953
5003
|
function resolveResumeEffort(ctx, lane, input) {
|
|
4954
5004
|
if (input.cliEffort === undefined && input.profile === undefined)
|
|
@@ -5039,11 +5089,13 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5039
5089
|
if (targetCfg === undefined) {
|
|
5040
5090
|
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) });
|
|
5041
5091
|
}
|
|
5092
|
+
const targetHome = effectiveAccountHome(targetCfg, ctx.hostname()).home;
|
|
5042
5093
|
if (!targetCfg.enabled) {
|
|
5043
5094
|
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 ?? "" });
|
|
5044
5095
|
}
|
|
5045
5096
|
if (accountStatus(lane.provider, target, config, {
|
|
5046
5097
|
existsSync: ctx.existsSync,
|
|
5098
|
+
hostname: ctx.hostname(),
|
|
5047
5099
|
probe: ctx.probeProvider,
|
|
5048
5100
|
env: ctx.env,
|
|
5049
5101
|
}).logged_in !== true) {
|
|
@@ -5060,7 +5112,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5060
5112
|
: undefined;
|
|
5061
5113
|
const profileModel = profileDefaultsApply ? profile?.meta.model : undefined;
|
|
5062
5114
|
if (input.cliModel === undefined && profileDefaultsApply && profileModel !== undefined && pinnedModel === undefined) {
|
|
5063
|
-
validateProfileLaneModel(lane.provider, profileModel,
|
|
5115
|
+
validateProfileLaneModel(lane.provider, profileModel, targetHome, profile.id, {
|
|
5064
5116
|
readFileSync: ctx.readFileSync,
|
|
5065
5117
|
probe: ctx.probeProvider,
|
|
5066
5118
|
env: ctx.env,
|
|
@@ -5086,7 +5138,7 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5086
5138
|
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 });
|
|
5087
5139
|
}
|
|
5088
5140
|
if (model !== undefined) {
|
|
5089
|
-
validateLaneModel(lane.provider, model,
|
|
5141
|
+
validateLaneModel(lane.provider, model, targetHome, {
|
|
5090
5142
|
readFileSync: ctx.readFileSync,
|
|
5091
5143
|
probe: ctx.probeProvider,
|
|
5092
5144
|
env: ctx.env,
|
|
@@ -5096,14 +5148,15 @@ async function prepareAccountMove(ctx, lane, input) {
|
|
|
5096
5148
|
if (sourceCfg === undefined) {
|
|
5097
5149
|
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: [] });
|
|
5098
5150
|
}
|
|
5151
|
+
const sourceHome = effectiveAccountHome(sourceCfg, ctx.hostname()).home;
|
|
5099
5152
|
// Checks done. Copy the session files, then write the record.
|
|
5100
5153
|
const copy = ensureSessionHome({
|
|
5101
5154
|
provider: lane.provider,
|
|
5102
5155
|
sessionId,
|
|
5103
5156
|
cwd: hqRoot,
|
|
5104
|
-
targetHome
|
|
5105
|
-
registeredHomes: Object.values(providerCfg.accounts).map((account) => account.home),
|
|
5106
|
-
providerHomeAtSpawn: lane.provider_home_at_spawn ??
|
|
5157
|
+
targetHome,
|
|
5158
|
+
registeredHomes: Object.values(providerCfg.accounts).map((account) => effectiveAccountHome(account, ctx.hostname()).home),
|
|
5159
|
+
providerHomeAtSpawn: lane.provider_home_at_spawn ?? sourceHome,
|
|
5107
5160
|
});
|
|
5108
5161
|
const at = now.toISOString();
|
|
5109
5162
|
const from = lane.account ?? null;
|
|
@@ -5186,6 +5239,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
|
|
|
5186
5239
|
}
|
|
5187
5240
|
if (accountStatus("claude", lane.account, config, {
|
|
5188
5241
|
existsSync: ctx.existsSync,
|
|
5242
|
+
hostname: ctx.hostname(),
|
|
5189
5243
|
probe: ctx.probeProvider,
|
|
5190
5244
|
env: ctx.env,
|
|
5191
5245
|
}).logged_in === false) {
|
|
@@ -5212,6 +5266,7 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
|
|
|
5212
5266
|
? false
|
|
5213
5267
|
: accountStatus("claude", lane.account, config, {
|
|
5214
5268
|
existsSync: ctx.existsSync,
|
|
5269
|
+
hostname: ctx.hostname(),
|
|
5215
5270
|
probe: ctx.probeProvider,
|
|
5216
5271
|
env: ctx.env,
|
|
5217
5272
|
}).logged_in;
|
|
@@ -5274,6 +5329,7 @@ function assertCurrentResumeAccountAvailable(ctx, lane) {
|
|
|
5274
5329
|
}
|
|
5275
5330
|
if (accountStatus(lane.provider, lane.account, config, {
|
|
5276
5331
|
existsSync: ctx.existsSync,
|
|
5332
|
+
hostname: ctx.hostname(),
|
|
5277
5333
|
probe: ctx.probeProvider,
|
|
5278
5334
|
env: ctx.env,
|
|
5279
5335
|
}).logged_in !== true) {
|
|
@@ -5538,7 +5594,7 @@ async function runMessage(ctx, laneId, opts) {
|
|
|
5538
5594
|
}
|
|
5539
5595
|
// Every accepted message moves the compaction reference forward.
|
|
5540
5596
|
let next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
5541
|
-
...current,
|
|
5597
|
+
...resetKeepAliveProgress(current),
|
|
5542
5598
|
last_message_at: at,
|
|
5543
5599
|
}));
|
|
5544
5600
|
if (projectId !== lane.project_id || storyId !== lane.story_id) {
|
|
@@ -5876,6 +5932,9 @@ async function runResume(ctx, laneId, opts) {
|
|
|
5876
5932
|
const resumeProfile = opts.worker !== undefined
|
|
5877
5933
|
? resolveResumeProfile(ctx, lane, opts.worker)
|
|
5878
5934
|
: undefined;
|
|
5935
|
+
const keepAlive = opts.keepAlive === true
|
|
5936
|
+
? true
|
|
5937
|
+
: resumeProfile?.profile.meta.keep_alive;
|
|
5879
5938
|
const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
|
|
5880
5939
|
if (keepCurrentAccount)
|
|
5881
5940
|
assertCurrentResumeAccountAvailable(ctx, lane);
|
|
@@ -5933,6 +5992,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
5933
5992
|
lane = recordResumeSetup(ctx, lane, {
|
|
5934
5993
|
...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
|
|
5935
5994
|
...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
|
|
5995
|
+
...(keepAlive !== undefined ? { keepAlive } : {}),
|
|
5936
5996
|
...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
|
|
5937
5997
|
...(requestedMeshCompany !== undefined
|
|
5938
5998
|
? { meshCompany: requestedMeshCompany, threadId: meshThreadId }
|
|
@@ -6028,7 +6088,7 @@ async function runResume(ctx, laneId, opts) {
|
|
|
6028
6088
|
if (text !== undefined) {
|
|
6029
6089
|
// An operator text moves the compaction reference forward, as a message does.
|
|
6030
6090
|
next = casUpdateLane(hqRoot, laneId, (current) => ({
|
|
6031
|
-
...current,
|
|
6091
|
+
...resetKeepAliveProgress(current),
|
|
6032
6092
|
last_message_at: at,
|
|
6033
6093
|
}));
|
|
6034
6094
|
}
|
|
@@ -6063,6 +6123,9 @@ async function runResume(ctx, laneId, opts) {
|
|
|
6063
6123
|
};
|
|
6064
6124
|
});
|
|
6065
6125
|
}
|
|
6126
|
+
if (text === undefined && hasKeepAliveProgress(next)) {
|
|
6127
|
+
next = casUpdateLane(hqRoot, laneId, (current) => resetKeepAliveProgress(current));
|
|
6128
|
+
}
|
|
6066
6129
|
printJson({
|
|
6067
6130
|
ok: true,
|
|
6068
6131
|
action: "resume",
|
|
@@ -6419,7 +6482,7 @@ function drainHookStatusFor(ctx, lane) {
|
|
|
6419
6482
|
const account = config.providers[lane.provider].accounts[lane.account];
|
|
6420
6483
|
if (account === undefined)
|
|
6421
6484
|
return null;
|
|
6422
|
-
const home = canonicalizeHome(account.home, {
|
|
6485
|
+
const home = canonicalizeHome(effectiveAccountHome(account, ctx.hostname()).home, {
|
|
6423
6486
|
homedir: ctx.homedir,
|
|
6424
6487
|
realpathSync: (p) => fs.realpathSync(p),
|
|
6425
6488
|
existsSync: (p) => fs.existsSync(p),
|
|
@@ -7671,6 +7734,7 @@ async function runAccountsList(ctx, opts) {
|
|
|
7671
7734
|
const quota = loadQuotaState(hqRoot);
|
|
7672
7735
|
const accounts = providers.flatMap((provider) => listAccountStatuses(provider, config, {
|
|
7673
7736
|
existsSync: ctx.existsSync,
|
|
7737
|
+
hostname: ctx.hostname(),
|
|
7674
7738
|
probe: ctx.probeProvider,
|
|
7675
7739
|
env: ctx.env,
|
|
7676
7740
|
}).map((status) => {
|
|
@@ -7686,6 +7750,7 @@ async function runAccountsList(ctx, opts) {
|
|
|
7686
7750
|
});
|
|
7687
7751
|
return {
|
|
7688
7752
|
...status,
|
|
7753
|
+
effective_home: status.home,
|
|
7689
7754
|
model: accountCfg?.model ?? null,
|
|
7690
7755
|
effort: accountCfg?.effort ?? null,
|
|
7691
7756
|
effective_model: model ?? null,
|
|
@@ -7849,9 +7914,23 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
7849
7914
|
throw new LanesError("device_auth_unsupported", "--device-auth is supported only for codex account login", { provider });
|
|
7850
7915
|
}
|
|
7851
7916
|
const registeredAccount = config.providers[provider].accounts[id];
|
|
7852
|
-
const
|
|
7853
|
-
|
|
7854
|
-
|
|
7917
|
+
const hostname = ctx.hostname().trim();
|
|
7918
|
+
if (!hostname) {
|
|
7919
|
+
throw new LanesError("host_identity_unknown", "Cannot resolve this host's hostname for account login", {});
|
|
7920
|
+
}
|
|
7921
|
+
const requestedHome = opts.home?.trim();
|
|
7922
|
+
if (opts.home !== undefined && !requestedHome) {
|
|
7923
|
+
throw new LanesError("invalid_account_home", "--home must be a non-empty path", { provider, account: id });
|
|
7924
|
+
}
|
|
7925
|
+
const hostScopedWriterEnabled = registeredAccount !== undefined && requestedHome !== undefined
|
|
7926
|
+
? await hostScopedAccountHomesEnabled(ctx)
|
|
7927
|
+
: false;
|
|
7928
|
+
const homeRaw = requestedHome ??
|
|
7929
|
+
(registeredAccount !== undefined
|
|
7930
|
+
? effectiveAccountHome(registeredAccount, hostname).home
|
|
7931
|
+
: id === "a"
|
|
7932
|
+
? DEFAULT_ACCOUNT_HOMES[provider]
|
|
7933
|
+
: `~/.${provider}-${id}`);
|
|
7855
7934
|
const homeResolved = homeRaw.startsWith("~") || path.isAbsolute(homeRaw)
|
|
7856
7935
|
? homeRaw
|
|
7857
7936
|
: resolveCallerPath(callerCwd, homeRaw);
|
|
@@ -7860,20 +7939,43 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
7860
7939
|
realpathSync: (p) => fs.realpathSync(p),
|
|
7861
7940
|
existsSync: (p) => fs.existsSync(p),
|
|
7862
7941
|
});
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7942
|
+
const nextAccount = registeredAccount === undefined
|
|
7943
|
+
? { home, enabled: true }
|
|
7944
|
+
: { ...registeredAccount, enabled: true };
|
|
7945
|
+
if (registeredAccount !== undefined) {
|
|
7946
|
+
if (hostScopedWriterEnabled && requestedHome !== undefined) {
|
|
7947
|
+
if (home === registeredAccount.home) {
|
|
7948
|
+
if (nextAccount.hosts !== undefined) {
|
|
7949
|
+
delete nextAccount.hosts[hostname];
|
|
7950
|
+
if (Object.keys(nextAccount.hosts).length === 0)
|
|
7951
|
+
delete nextAccount.hosts;
|
|
7952
|
+
}
|
|
7953
|
+
}
|
|
7954
|
+
else {
|
|
7955
|
+
nextAccount.hosts = {
|
|
7956
|
+
...(registeredAccount.hosts ?? {}),
|
|
7957
|
+
[hostname]: { home },
|
|
7958
|
+
};
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
else if (requestedHome !== undefined) {
|
|
7962
|
+
// With the rollout off, retain the legacy shared-home write shape.
|
|
7963
|
+
nextAccount.home = home;
|
|
7868
7964
|
}
|
|
7869
7965
|
}
|
|
7966
|
+
delete nextAccount.disabled_reason;
|
|
7967
|
+
assertUniqueAccountHomes(provider, {
|
|
7968
|
+
...config.providers[provider].accounts,
|
|
7969
|
+
[id]: nextAccount,
|
|
7970
|
+
});
|
|
7870
7971
|
ensureDir(home);
|
|
7871
7972
|
const defaultId = config.providers[provider].default_account;
|
|
7872
|
-
const
|
|
7873
|
-
? config.providers[provider].accounts[defaultId]
|
|
7973
|
+
const defaultAccount = defaultId !== undefined
|
|
7974
|
+
? config.providers[provider].accounts[defaultId]
|
|
7975
|
+
: Object.values(config.providers[provider].accounts).find((account) => account.enabled);
|
|
7976
|
+
const defaultHome = (defaultAccount !== undefined
|
|
7977
|
+
? effectiveAccountHome(defaultAccount, hostname).home
|
|
7874
7978
|
: undefined) ??
|
|
7875
|
-
Object.values(config.providers[provider].accounts).find((account) => account.enabled)
|
|
7876
|
-
?.home ??
|
|
7877
7979
|
canonicalizeHome(DEFAULT_ACCOUNT_HOMES[provider], {
|
|
7878
7980
|
homedir: ctx.homedir,
|
|
7879
7981
|
realpathSync: (p) => fs.realpathSync(p),
|
|
@@ -7899,12 +8001,10 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
7899
8001
|
if ((grokNeedsRails || copied) && !railsPresent(home)) {
|
|
7900
8002
|
throw new LanesError("rails_missing", `Hook rails are missing under ${home}; account not enabled`, { provider, account: id, home });
|
|
7901
8003
|
}
|
|
7902
|
-
config.providers[provider].accounts[id] =
|
|
7903
|
-
home,
|
|
7904
|
-
enabled: true,
|
|
7905
|
-
};
|
|
8004
|
+
config.providers[provider].accounts[id] = nextAccount;
|
|
7906
8005
|
const status = accountStatus(provider, id, config, {
|
|
7907
8006
|
existsSync: ctx.existsSync,
|
|
8007
|
+
hostname,
|
|
7908
8008
|
probe: ctx.probeProvider,
|
|
7909
8009
|
env: ctx.env,
|
|
7910
8010
|
});
|
|
@@ -7916,6 +8016,93 @@ async function runAccountsLogin(ctx, providerRaw, accountId, opts) {
|
|
|
7916
8016
|
writeLanesConfig(hqRoot, config);
|
|
7917
8017
|
printJson({ ok: true, action: "accounts.login", account: status });
|
|
7918
8018
|
}
|
|
8019
|
+
async function runAccountsSetHome(ctx, providerRaw, accountIdRaw, homeRaw, opts) {
|
|
8020
|
+
if (!(await hostScopedAccountHomesEnabled(ctx))) {
|
|
8021
|
+
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 });
|
|
8022
|
+
}
|
|
8023
|
+
const callerCwd = ctx.callerCwd();
|
|
8024
|
+
const hqRoot = ctx.hqRoot();
|
|
8025
|
+
const provider = parseLaneProvider(providerRaw);
|
|
8026
|
+
const id = accountIdRaw.trim();
|
|
8027
|
+
if (!id) {
|
|
8028
|
+
throw new AccountResolutionError("unknown_account", "account-id must be non-empty", {
|
|
8029
|
+
provider,
|
|
8030
|
+
account: accountIdRaw,
|
|
8031
|
+
enabled: [],
|
|
8032
|
+
});
|
|
8033
|
+
}
|
|
8034
|
+
const hostname = opts.host === undefined ? ctx.hostname().trim() : opts.host.trim();
|
|
8035
|
+
if (!hostname || (opts.host !== undefined && hostname !== opts.host)) {
|
|
8036
|
+
throw new LanesError("invalid_host", "--host must be a non-empty hostname without surrounding whitespace", {
|
|
8037
|
+
host: opts.host ?? "",
|
|
8038
|
+
});
|
|
8039
|
+
}
|
|
8040
|
+
if (!homeRaw.trim()) {
|
|
8041
|
+
throw new LanesError("invalid_account_home", "home path must be a non-empty path", {
|
|
8042
|
+
provider,
|
|
8043
|
+
account: id,
|
|
8044
|
+
});
|
|
8045
|
+
}
|
|
8046
|
+
const config = loadLanesConfig(hqRoot);
|
|
8047
|
+
const account = config.providers[provider].accounts[id];
|
|
8048
|
+
if (account === undefined) {
|
|
8049
|
+
throw new AccountResolutionError("unknown_account", `Unknown account ${JSON.stringify(id)} for ${provider}`, {
|
|
8050
|
+
provider,
|
|
8051
|
+
account: id,
|
|
8052
|
+
enabled: Object.keys(config.providers[provider].accounts).filter((key) => config.providers[provider].accounts[key]?.enabled),
|
|
8053
|
+
});
|
|
8054
|
+
}
|
|
8055
|
+
const homePath = homeRaw.trim();
|
|
8056
|
+
const isCurrentHost = hostname === ctx.hostname().trim();
|
|
8057
|
+
let home;
|
|
8058
|
+
if (isCurrentHost) {
|
|
8059
|
+
const homeResolved = homePath.startsWith("~") || path.isAbsolute(homePath)
|
|
8060
|
+
? homePath
|
|
8061
|
+
: resolveCallerPath(callerCwd, homePath);
|
|
8062
|
+
home = canonicalizeHome(homeResolved, {
|
|
8063
|
+
homedir: ctx.homedir,
|
|
8064
|
+
realpathSync: (p) => fs.realpathSync(p),
|
|
8065
|
+
existsSync: (p) => fs.existsSync(p),
|
|
8066
|
+
});
|
|
8067
|
+
}
|
|
8068
|
+
else if (homePath === "~" || homePath.startsWith("~/")) {
|
|
8069
|
+
home = homePath;
|
|
8070
|
+
}
|
|
8071
|
+
else if (path.isAbsolute(homePath)) {
|
|
8072
|
+
home = path.normalize(homePath);
|
|
8073
|
+
}
|
|
8074
|
+
else {
|
|
8075
|
+
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 });
|
|
8076
|
+
}
|
|
8077
|
+
for (const [otherId, other] of Object.entries(config.providers[provider].accounts)) {
|
|
8078
|
+
if (otherId !== id && effectiveAccountHome(other, hostname).home === home) {
|
|
8079
|
+
throw new LanesError("duplicate_account_home", `Home ${home} is already registered as ${provider} account ${otherId} on host ${hostname}`, { provider, accounts: [otherId, id], home, hostname });
|
|
8080
|
+
}
|
|
8081
|
+
}
|
|
8082
|
+
if (home === account.home) {
|
|
8083
|
+
if (account.hosts !== undefined) {
|
|
8084
|
+
delete account.hosts[hostname];
|
|
8085
|
+
if (Object.keys(account.hosts).length === 0)
|
|
8086
|
+
delete account.hosts;
|
|
8087
|
+
}
|
|
8088
|
+
}
|
|
8089
|
+
else {
|
|
8090
|
+
const hosts = Object.assign(Object.create(null), account.hosts ?? {});
|
|
8091
|
+
hosts[hostname] = { home };
|
|
8092
|
+
account.hosts = hosts;
|
|
8093
|
+
}
|
|
8094
|
+
assertUniqueAccountHomes(provider, config.providers[provider].accounts);
|
|
8095
|
+
writeLanesConfig(hqRoot, config);
|
|
8096
|
+
printJson({
|
|
8097
|
+
ok: true,
|
|
8098
|
+
action: "accounts.set-home",
|
|
8099
|
+
provider,
|
|
8100
|
+
account: id,
|
|
8101
|
+
hostname,
|
|
8102
|
+
home,
|
|
8103
|
+
home_source: home === account.home ? "default" : "host",
|
|
8104
|
+
});
|
|
8105
|
+
}
|
|
7919
8106
|
async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
7920
8107
|
const hqRoot = ctx.hqRoot();
|
|
7921
8108
|
const provider = parseLaneProvider(providerRaw);
|
|
@@ -7932,6 +8119,7 @@ async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
|
7932
8119
|
}
|
|
7933
8120
|
const status = accountStatus(provider, accountId, config, {
|
|
7934
8121
|
existsSync: ctx.existsSync,
|
|
8122
|
+
hostname: ctx.hostname(),
|
|
7935
8123
|
probe: ctx.probeProvider,
|
|
7936
8124
|
env: ctx.env,
|
|
7937
8125
|
});
|
|
@@ -7948,6 +8136,7 @@ async function runAccountsEnable(ctx, providerRaw, accountId) {
|
|
|
7948
8136
|
action: "accounts.enable",
|
|
7949
8137
|
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
|
|
7950
8138
|
existsSync: ctx.existsSync,
|
|
8139
|
+
hostname: ctx.hostname(),
|
|
7951
8140
|
probe: ctx.probeProvider,
|
|
7952
8141
|
env: ctx.env,
|
|
7953
8142
|
}),
|
|
@@ -7976,6 +8165,7 @@ async function runAccountsDisable(ctx, providerRaw, accountId, opts) {
|
|
|
7976
8165
|
action: "accounts.disable",
|
|
7977
8166
|
account: accountStatus(provider, accountId, loadLanesConfig(hqRoot), {
|
|
7978
8167
|
existsSync: ctx.existsSync,
|
|
8168
|
+
hostname: ctx.hostname(),
|
|
7979
8169
|
probe: ctx.probeProvider,
|
|
7980
8170
|
env: ctx.env,
|
|
7981
8171
|
}),
|
|
@@ -8145,6 +8335,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8145
8335
|
.option("--effort <level>", "Effort level; claude: low|medium|high|xhigh|max, codex: minimal|low|medium|high|xhigh|max|ultra, grok: low|medium|high|xhigh")
|
|
8146
8336
|
.option("--senior <ref>", "session:<id> | lane:<id> | user")
|
|
8147
8337
|
.option("--hire-grant <n>", "Maximum alive direct child lanes assigned to this lane; set by its senior")
|
|
8338
|
+
.option("--keep-alive", "Automatically resume done lead lanes while their story remains open")
|
|
8148
8339
|
.option("--timeout <secs>", "Deadline in seconds (warns, does not kill)")
|
|
8149
8340
|
.option("--priority <n>", "Queue priority; higher runs first", "1")
|
|
8150
8341
|
.option("--outpost <id>", "Run on this Outpost instead of the local host")
|
|
@@ -8382,6 +8573,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8382
8573
|
.option("--model <id>", "Model for the moved lane (with --account only)")
|
|
8383
8574
|
.option("--effort <level>", "Reasoning effort for the resumed provider turn")
|
|
8384
8575
|
.option("--worker <profile>", "Attach a personal worker profile for the resumed turn")
|
|
8576
|
+
.option("--keep-alive", "Automatically resume done lead lanes while their story remains open")
|
|
8385
8577
|
.option("--mesh-company <slug>", "Attach Work Mesh attribution after verifying company membership")
|
|
8386
8578
|
.option("--no-compact", "Skip the resume-time compaction check for this resume")
|
|
8387
8579
|
.action(wrap(async (laneId, opts) => {
|
|
@@ -8490,6 +8682,17 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
8490
8682
|
.action(wrap(async (opts) => {
|
|
8491
8683
|
await runAccountsBalance(ctx, opts);
|
|
8492
8684
|
}));
|
|
8685
|
+
accounts
|
|
8686
|
+
.command("set-home")
|
|
8687
|
+
.description("Set the host-specific home for an existing provider account")
|
|
8688
|
+
.argument("<provider>", "claude | codex | grok")
|
|
8689
|
+
.argument("<account-id>", "Existing account id")
|
|
8690
|
+
.argument("<path>", "Home directory for this host")
|
|
8691
|
+
.option("--host <hostname>", "Host key (defaults to this host's hostname)")
|
|
8692
|
+
.option("--json", "JSON on stdout (always)")
|
|
8693
|
+
.action(wrap(async (provider, accountId, home, opts) => {
|
|
8694
|
+
await runAccountsSetHome(ctx, provider, accountId, home, opts);
|
|
8695
|
+
}));
|
|
8493
8696
|
accounts
|
|
8494
8697
|
.command("login")
|
|
8495
8698
|
.description("Create a home, run the vendor login, and register the account")
|