@indigoai-us/hq-cli 5.222.0 → 5.224.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.
@@ -15,6 +15,7 @@ import { Option } from "commander";
15
15
  import { registerOutpostPrunerCommand } from "./outpost-pruner.js";
16
16
  import { registerLaneChecksCommand } from "./lanes-checks.js";
17
17
  import { registerLaneReportCommands } from "./lanes-report.js";
18
+ import { registerLaneTrackerImportCommand } from "./lanes-import.js";
18
19
  import { ensureCognitoToken } from "../utils/cognito-session.js";
19
20
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
20
21
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
@@ -42,7 +43,7 @@ import { effectiveAccountHome } from "../lib/lanes/account-home.js";
42
43
  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, hireGrantCapacityError, 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, withAllLaneAdmissionLocks, withLaneHireLocks, laneIdForAnswerDelivery, operatorMessageRefusalReason, raiseLaneQuestion, sha256Text, writeQuestionBody, } from "../lib/lanes/index.js";
43
44
  import { activeLaneStories, laneStories, laneStory, storyWarning, } from "../lib/lanes/stories.js";
44
45
  import { resolveLaneKeepAlive } from "../lib/lanes/profile.js";
45
- import { keepAliveExclusion, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
46
+ import { keepAliveExclusion, LEAD_LANE_KEEP_ALIVE_FLAG, loadRetiredLaneIds, } from "../lib/lanes/keep-alive.js";
46
47
  import { questionBodyAbsPath, resolveQuestionCompanyWithFallback, resolveProjectQuestionCompany, } from "../lib/lanes/questions.js";
47
48
  import { assertQuestionBodySecretSafe } from "../lib/lanes/questions.js";
48
49
  import { compactionAfterUsage, } from "../lib/lanes/compaction.js";
@@ -1262,10 +1263,75 @@ function resolveMonitorEngine(requested, env) {
1262
1263
  }
1263
1264
  async function runWatch(ctx, laneIds, opts) {
1264
1265
  const rawInterval = opts.intervalMs === undefined ? undefined : Number(opts.intervalMs);
1265
- await runLaneWatch(ctx.hqRoot(), laneIds, {
1266
+ const hqRoot = ctx.hqRoot();
1267
+ await runLaneWatch(hqRoot, laneIds, {
1266
1268
  intervalMs: rawInterval,
1267
1269
  sleep: ctx.sleep,
1270
+ keepAliveEnabled: (lane) => readLanesRegistryFlag(LEAD_LANE_KEEP_ALIVE_FLAG, {
1271
+ company: lane.company,
1272
+ env: ctx.env,
1273
+ }),
1274
+ readAccountStatuses: async (lanes) => {
1275
+ if (lanes.length === 0)
1276
+ return [];
1277
+ const config = loadLanesConfig(hqRoot);
1278
+ const currentHost = laneHostFromCtx(ctx);
1279
+ const currentHostname = ctx.hostname().trim();
1280
+ const isCurrentHost = (host) => (host.kind === currentHost.kind && host.id === currentHost.id) ||
1281
+ (host.kind === "local" && host.id === currentHostname);
1282
+ const remoteOutpostIds = [...new Set(lanes
1283
+ .filter((lane) => lane.host.kind === "outpost" && !isCurrentHost(lane.host))
1284
+ .map((lane) => lane.host.id))];
1285
+ const remoteOutposts = await Promise.all(remoteOutpostIds.map(async (id) => [id, await readOutpostCapacity(ctx, id)]));
1286
+ const outpostSnapshots = new Map(remoteOutposts);
1287
+ const remoteLocalLanes = lanes.filter((lane) => lane.host.kind === "local" && !isCurrentHost(lane.host));
1288
+ const remoteLocalHosts = remoteLocalLanes.length > 0
1289
+ ? await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config)
1290
+ : undefined;
1291
+ const localStatusCache = new Map();
1292
+ return lanes.map((lane) => {
1293
+ if (config.providers[lane.provider].accounts[lane.account] === undefined) {
1294
+ throw new LanesError("watch_account_unconfigured", `Lane ${lane.lane_id} records unconfigured ${lane.provider} account ${lane.account}`, { lane_id: lane.lane_id, provider: lane.provider, account: lane.account, host: lane.host });
1295
+ }
1296
+ let loggedIn;
1297
+ if (isCurrentHost(lane.host)) {
1298
+ const key = JSON.stringify([lane.provider, lane.account, currentHost.kind, currentHost.id]);
1299
+ if (!localStatusCache.has(key)) {
1300
+ localStatusCache.set(key, accountStatus(lane.provider, lane.account, config, {
1301
+ existsSync: ctx.existsSync,
1302
+ probe: ctx.probeProvider,
1303
+ env: ctx.env,
1304
+ hostname: currentHostname,
1305
+ }).logged_in);
1306
+ }
1307
+ loggedIn = localStatusCache.get(key);
1308
+ }
1309
+ else if (lane.host.kind === "outpost") {
1310
+ loggedIn = outpostSnapshots.get(lane.host.id)?.providers[lane.provider].accounts[lane.account]?.logged_in;
1311
+ }
1312
+ else {
1313
+ const snapshot = remoteLocalHosts?.snapshots.find((candidate) => candidate.capacity.host.kind === lane.host.kind &&
1314
+ candidate.capacity.host.id === lane.host.id);
1315
+ if (!snapshot) {
1316
+ const gap = remoteLocalHosts?.unavailableHosts.find((candidate) => candidate.host.kind === lane.host.kind && candidate.host.id === lane.host.id);
1317
+ throw new LanesError("watch_account_host_unavailable", `Cannot check ${lane.provider} account ${lane.account} on ${lane.host.kind}:${lane.host.id}${gap ? `: ${gap.reason}` : ""}`, { lane_id: lane.lane_id, provider: lane.provider, account: lane.account, host: lane.host });
1318
+ }
1319
+ loggedIn = snapshot.capacity.providers[lane.provider].accounts[lane.account]?.logged_in;
1320
+ }
1321
+ if (loggedIn === undefined) {
1322
+ throw new LanesError("watch_account_unconfigured", `Lane ${lane.lane_id} records unconfigured ${lane.provider} account ${lane.account} on ${lane.host.kind}:${lane.host.id}`, { lane_id: lane.lane_id, provider: lane.provider, account: lane.account, host: lane.host });
1323
+ }
1324
+ return {
1325
+ lane_id: lane.lane_id,
1326
+ provider: lane.provider,
1327
+ account: lane.account,
1328
+ host: lane.host,
1329
+ logged_in: loggedIn,
1330
+ };
1331
+ });
1332
+ },
1268
1333
  writeLine: (line) => console.log(opts.json ? JSON.stringify({ event: line }) : line),
1334
+ writeAccountEvent: (event) => console.log(opts.json ? JSON.stringify(event) : `account_logged_out provider=${event.provider} account=${event.account} host=${event.host.kind}:${event.host.id} lane_ids=${event.lane_ids.join(",")}`),
1269
1335
  writeError: (line) => process.stderr.write(`${line}\n`),
1270
1336
  });
1271
1337
  return 0;
@@ -5002,7 +5068,8 @@ function resolveResumeProfile(ctx, lane, workerId) {
5002
5068
  function validateResumeProfileModel(ctx, lane, profile) {
5003
5069
  if (profile.meta.provider !== lane.provider ||
5004
5070
  profile.meta.model === undefined ||
5005
- lane.model_source === "cli") {
5071
+ lane.model_source === "cli" ||
5072
+ lane.model_source === "preserved") {
5006
5073
  return;
5007
5074
  }
5008
5075
  const hqRoot = ctx.hqRoot();
@@ -5036,7 +5103,7 @@ function applyResumeProfileAndEffort(current, next, input) {
5036
5103
  : undefined;
5037
5104
  next.keep_alive_blocked_reason = undefined;
5038
5105
  }
5039
- if (next.model_source !== "cli") {
5106
+ if (next.model_source !== "cli" && next.model_source !== "preserved") {
5040
5107
  if (profile.meta.provider === current.provider && profile.meta.model !== undefined) {
5041
5108
  next.model = profile.meta.model;
5042
5109
  next.model_source = "profile";
@@ -5130,7 +5197,7 @@ function resolveResumeEffort(ctx, lane, input) {
5130
5197
  ? undefined
5131
5198
  : providerConfig.accounts[accountId];
5132
5199
  const profileDefaultsApply = input.profile?.profile.meta.provider === lane.provider;
5133
- const pinnedModel = lane.model_source === undefined || lane.model_source === "cli"
5200
+ const pinnedModel = lane.model_source === undefined || lane.model_source === "cli" || lane.model_source === "preserved"
5134
5201
  ? lane.model
5135
5202
  : undefined;
5136
5203
  const model = resolveLaneModel({
@@ -5225,11 +5292,11 @@ async function prepareAccountMove(ctx, lane, input) {
5225
5292
  ? loadWorkerProfile(hqRoot, lane.worker_profile)
5226
5293
  : undefined);
5227
5294
  const profileDefaultsApply = profile !== undefined && profile.meta.provider === lane.provider;
5228
- const pinnedModel = input.workerProfile !== undefined
5229
- ? lane.model_source === "cli" ? lane.model : undefined
5230
- : lane.model_source === undefined || lane.model_source === "cli"
5295
+ const pinnedModel = input.preservedModel ?? (input.workerProfile !== undefined
5296
+ ? lane.model_source === "cli" || lane.model_source === "preserved" ? lane.model : undefined
5297
+ : lane.model_source === undefined || lane.model_source === "cli" || lane.model_source === "preserved"
5231
5298
  ? lane.model
5232
- : undefined;
5299
+ : undefined);
5233
5300
  const profileModel = profileDefaultsApply ? profile?.meta.model : undefined;
5234
5301
  if (input.cliModel === undefined && profileDefaultsApply && profileModel !== undefined && pinnedModel === undefined) {
5235
5302
  validateProfileLaneModel(lane.provider, profileModel, targetHome, profile.id, {
@@ -5313,12 +5380,14 @@ async function prepareAccountMove(ctx, lane, input) {
5313
5380
  provider_session_id_absent: undefined,
5314
5381
  ...(input.cliModel !== undefined
5315
5382
  ? { model: input.cliModel, model_source: "cli" }
5316
- : {}),
5383
+ : input.preservedModel !== undefined
5384
+ ? { model: input.preservedModel, model_source: "preserved" }
5385
+ : {}),
5317
5386
  account_history: appendCappedHistory(current.account_history ?? [], {
5318
5387
  from,
5319
5388
  to: target,
5320
5389
  at,
5321
- reason: SESSION_MOVE_REASON,
5390
+ reason: input.accountHistoryReason ?? SESSION_MOVE_REASON,
5322
5391
  }),
5323
5392
  pending_resume: input.deferTurnStart
5324
5393
  ? undefined
@@ -5345,31 +5414,39 @@ async function prepareAccountMove(ctx, lane, input) {
5345
5414
  return { lane: moved, from, to: target, copy, queued };
5346
5415
  }, ctx.sleep);
5347
5416
  }
5348
- async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
5417
+ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile, options = {}) {
5349
5418
  if (lane.provider !== "claude")
5350
5419
  return undefined;
5351
- if (!await ctx.laneClaudeAccountBalanceEnabled(lane.company)) {
5420
+ const balanceEnabled = await ctx.laneClaudeAccountBalanceEnabled(lane.company);
5421
+ const hqRoot = ctx.hqRoot();
5422
+ const config = loadLanesConfig(hqRoot);
5423
+ const currentConfig = lane.account === undefined
5424
+ ? undefined
5425
+ : config.providers.claude.accounts[lane.account];
5426
+ const currentLogin = lane.account === undefined || currentConfig === undefined
5427
+ ? false
5428
+ : accountStatus("claude", lane.account, config, {
5429
+ existsSync: ctx.existsSync,
5430
+ hostname: ctx.hostname(),
5431
+ probe: ctx.probeProvider,
5432
+ env: ctx.env,
5433
+ }).logged_in;
5434
+ const accountNeedsRepair = lane.account !== undefined &&
5435
+ (currentConfig?.enabled !== true || currentLogin === false);
5436
+ const repairUnavailableAccount = options.repairUnavailableAccount === true && accountNeedsRepair;
5437
+ if (!balanceEnabled && !(options.repairUnavailableAccount && accountNeedsRepair)) {
5352
5438
  if (lane.account === undefined)
5353
5439
  return undefined;
5354
- const config = loadLanesConfig(ctx.hqRoot());
5355
- const currentConfig = config.providers.claude.accounts[lane.account];
5356
5440
  if (currentConfig === undefined || !currentConfig.enabled) {
5357
5441
  throw new AccountResolutionError(currentConfig === undefined ? "unknown_account" : "account_disabled", currentConfig === undefined
5358
5442
  ? `Lane ${lane.lane_id} records account ${JSON.stringify(lane.account)} which is not configured for claude`
5359
5443
  : `Account ${JSON.stringify(lane.account)} is unavailable for claude`, { provider: "claude", account: lane.account });
5360
5444
  }
5361
- if (accountStatus("claude", lane.account, config, {
5362
- existsSync: ctx.existsSync,
5363
- hostname: ctx.hostname(),
5364
- probe: ctx.probeProvider,
5365
- env: ctx.env,
5366
- }).logged_in === false) {
5445
+ if (currentLogin === false) {
5367
5446
  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 });
5368
5447
  }
5369
5448
  return undefined;
5370
5449
  }
5371
- const hqRoot = ctx.hqRoot();
5372
- const config = loadLanesConfig(hqRoot);
5373
5450
  const profile = workerProfile?.profile ?? (lane.worker_profile !== undefined
5374
5451
  ? loadWorkerProfile(hqRoot, lane.worker_profile)
5375
5452
  : undefined);
@@ -5380,33 +5457,45 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
5380
5457
  const allowedAccounts = profilePin !== undefined && lane.admission_accounts !== undefined
5381
5458
  ? profilePin.filter((account) => lane.admission_accounts.includes(account))
5382
5459
  : profilePin ?? lane.admission_accounts;
5383
- const currentConfig = lane.account === undefined
5384
- ? undefined
5385
- : config.providers.claude.accounts[lane.account];
5386
- const currentLogin = lane.account === undefined || currentConfig === undefined
5387
- ? false
5388
- : accountStatus("claude", lane.account, config, {
5389
- existsSync: ctx.existsSync,
5390
- hostname: ctx.hostname(),
5391
- probe: ctx.probeProvider,
5392
- env: ctx.env,
5393
- }).logged_in;
5394
5460
  const pinnedModel = workerProfile !== undefined
5395
- ? lane.model_source === "cli" ? lane.model : undefined
5396
- : lane.model_source === undefined || lane.model_source === "cli"
5461
+ ? lane.model_source === "cli" || lane.model_source === "preserved" ? lane.model : undefined
5462
+ : lane.model_source === undefined || lane.model_source === "cli" || lane.model_source === "preserved"
5397
5463
  ? lane.model
5398
5464
  : undefined;
5465
+ const preservedModel = repairUnavailableAccount && lane.model_source === "account"
5466
+ ? lane.model ?? currentConfig?.model
5467
+ : undefined;
5399
5468
  const model = resolveLaneModel({
5400
- cliModel: pinnedModel,
5469
+ cliModel: preservedModel ?? pinnedModel,
5401
5470
  profileModel: profileDefaultsApply ? profile?.meta.model : undefined,
5402
5471
  accountModel: lane.account === undefined ? undefined : config.providers.claude.accounts[lane.account]?.model,
5403
5472
  providerModel: config.providers.claude.model,
5404
5473
  });
5405
- const accountNeedsRepair = currentConfig?.enabled !== true || currentLogin === false;
5406
5474
  const mayRebalance = BALANCE_MOVABLE_STATES.includes(lane.state);
5407
5475
  if (!accountNeedsRepair && !mayRebalance)
5408
5476
  return undefined;
5409
- const balanceHosts = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
5477
+ const balanceHosts = balanceEnabled
5478
+ ? await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config)
5479
+ : (() => {
5480
+ const lanes = loadAllLanes(hqRoot).filter((candidate) => candidate.host.kind === lane.host.kind && candidate.host.id === lane.host.id);
5481
+ return {
5482
+ snapshots: [{
5483
+ capacity: buildHostCapacitySnapshot({
5484
+ host: lane.host,
5485
+ lanes,
5486
+ config,
5487
+ existsSync: ctx.existsSync,
5488
+ probe: ctx.probeProvider,
5489
+ env: ctx.env,
5490
+ hostname: ctx.hostname(),
5491
+ now: ctx.now(),
5492
+ }),
5493
+ lanes,
5494
+ laneListAvailable: true,
5495
+ }],
5496
+ unavailableHosts: [],
5497
+ };
5498
+ })();
5410
5499
  const targetHost = balanceHosts.snapshots.find((host) => host.capacity.host.kind === lane.host.kind && host.capacity.host.id === lane.host.id);
5411
5500
  const currentTargetAccount = lane.account === undefined
5412
5501
  ? undefined
@@ -5423,11 +5512,15 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
5423
5512
  hosts: balanceHosts.snapshots,
5424
5513
  targetHost: lane.host,
5425
5514
  config,
5426
- ...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
5515
+ ...(!repairUnavailableAccount && allowedAccounts !== undefined ? { allowedAccounts } : {}),
5427
5516
  ...(model !== undefined ? { model } : {}),
5428
5517
  unavailableHosts: balanceHosts.unavailableHosts,
5429
5518
  });
5430
- return { ...selected, selected_at: ctx.now().toISOString() };
5519
+ return {
5520
+ selection: { ...selected, selected_at: ctx.now().toISOString() },
5521
+ ...(repairUnavailableAccount ? { historyReason: "recorded_account_logged_out" } : {}),
5522
+ ...(preservedModel !== undefined ? { preservedModel } : {}),
5523
+ };
5431
5524
  }
5432
5525
  function accountRequestKeepsCurrentLane(lane, rawAccount) {
5433
5526
  const raw = rawAccount.trim();
@@ -5525,7 +5618,7 @@ async function runMessage(ctx, laneId, opts) {
5525
5618
  const resumeEffort = resolveResumeEffort(ctx, lane, {
5526
5619
  cliEffort,
5527
5620
  profile: resumeProfile,
5528
- account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.account,
5621
+ account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.selection.account,
5529
5622
  model: opts.model,
5530
5623
  });
5531
5624
  const messageId = mintMessageId();
@@ -5585,12 +5678,14 @@ async function runMessage(ctx, laneId, opts) {
5585
5678
  }
5586
5679
  }
5587
5680
  else if (mechanism === "resume") {
5588
- const selection = automaticAccountSelection;
5589
- if (selection !== undefined) {
5681
+ const automatic = automaticAccountSelection;
5682
+ if (automatic !== undefined) {
5683
+ const selection = automatic.selection;
5590
5684
  if (selection.account !== lane.account) {
5591
5685
  move = await prepareAccountMove(ctx, lane, {
5592
5686
  account: selection.account,
5593
5687
  cliModel: undefined,
5688
+ ...(automatic.historyReason !== undefined ? { accountHistoryReason: automatic.historyReason } : {}),
5594
5689
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
5595
5690
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
5596
5691
  pending: {
@@ -6063,12 +6158,12 @@ async function runResume(ctx, laneId, opts) {
6063
6158
  validateResumeProfileModel(ctx, lane, resumeProfile.profile);
6064
6159
  }
6065
6160
  const automaticAccountSelection = opts.account === undefined
6066
- ? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile)
6161
+ ? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile, { repairUnavailableAccount: true })
6067
6162
  : undefined;
6068
6163
  const resumeEffort = resolveResumeEffort(ctx, lane, {
6069
6164
  cliEffort,
6070
6165
  profile: resumeProfile,
6071
- account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.account,
6166
+ account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.selection.account,
6072
6167
  model: opts.model,
6073
6168
  });
6074
6169
  let move = null;
@@ -6089,10 +6184,17 @@ async function runResume(ctx, laneId, opts) {
6089
6184
  lane = move.lane;
6090
6185
  }
6091
6186
  else if (automaticAccountSelection !== undefined) {
6092
- if (automaticAccountSelection.account !== lane.account) {
6187
+ const selection = automaticAccountSelection.selection;
6188
+ if (selection.account !== lane.account) {
6093
6189
  move = await prepareAccountMove(ctx, lane, {
6094
- account: automaticAccountSelection.account,
6190
+ account: selection.account,
6095
6191
  cliModel: undefined,
6192
+ ...(automaticAccountSelection.historyReason !== undefined
6193
+ ? { accountHistoryReason: automaticAccountSelection.historyReason }
6194
+ : {}),
6195
+ ...(automaticAccountSelection.preservedModel !== undefined
6196
+ ? { preservedModel: automaticAccountSelection.preservedModel }
6197
+ : {}),
6096
6198
  ...(resumeProfile !== undefined ? { workerProfile: resumeProfile } : {}),
6097
6199
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
6098
6200
  pending: {
@@ -6102,12 +6204,12 @@ async function runResume(ctx, laneId, opts) {
6102
6204
  ...(messageId !== undefined ? { message_id: messageId } : {}),
6103
6205
  compact: opts.compact !== false,
6104
6206
  },
6105
- accountSelection: automaticAccountSelection,
6207
+ accountSelection: selection,
6106
6208
  });
6107
6209
  lane = move.lane;
6108
6210
  }
6109
6211
  else {
6110
- lane = persistClaudeAccountSelection(hqRoot, lane, automaticAccountSelection);
6212
+ lane = persistClaudeAccountSelection(hqRoot, lane, selection);
6111
6213
  }
6112
6214
  }
6113
6215
  lane = recordResumeSetup(ctx, lane, {
@@ -8412,6 +8514,11 @@ async function runReconcile(ctx, opts) {
8412
8514
  dryRun: Boolean(opts.dryRun),
8413
8515
  spawnLaneDeps: {
8414
8516
  env: ctx.env,
8517
+ modelDeps: {
8518
+ readFileSync: ctx.readFileSync,
8519
+ probe: ctx.probeProvider,
8520
+ env: ctx.env,
8521
+ },
8415
8522
  existsSync: ctx.existsSync,
8416
8523
  spawnProcess: ctx.spawnProcess,
8417
8524
  proveDetached: ctx.proveDetached,
@@ -8505,6 +8612,13 @@ export function registerLanesCommand(parent, deps = {}) {
8505
8612
  now: ctx.now,
8506
8613
  actor: ctx.trackerActor,
8507
8614
  }, wrap);
8615
+ registerLaneTrackerImportCommand(lanes, {
8616
+ hqRoot: ctx.hqRoot,
8617
+ resolveCompany: ctx.resolveCompany,
8618
+ trackerEnabled: ctx.laneTrackerEnabled,
8619
+ trackerMeshEnabled: ctx.laneTrackerMeshEnabled,
8620
+ now: ctx.now,
8621
+ }, wrap);
8508
8622
  registerLaneChecksCommand(lanes, {
8509
8623
  hqRoot: ctx.hqRoot,
8510
8624
  resolveCompany: ctx.resolveCompany,
@@ -14,6 +14,8 @@ export interface HqCloudCopy {
14
14
  }
15
15
  export interface PickedHqCloud extends HqCloudCopy {
16
16
  source: "bundled" | "managed";
17
+ /** Set when a version is pinned but that version is not the installed managed copy. */
18
+ pinnedVersionMissing?: string;
17
19
  }
18
20
  export declare function managedHqCloudDir(daemonRoot: string): string;
19
21
  /** The package folder inside the updater's install prefix. */
@@ -44,8 +44,14 @@ export function pickHqCloud(bundled, managed, usable = () => true, pinnedVersion
44
44
  if (managed && managed.version === pinnedVersion && b && m && m[0] === b[0] && usable(managed)) {
45
45
  return { ...managed, source: "managed" };
46
46
  }
47
- return { ...bundled, source: "bundled" };
47
+ // The pinned version is not installed yet (the updater installs it). Keep
48
+ // running the newest usable copy meanwhile instead of dropping to an older
49
+ // bundled one, and report the missing pin.
50
+ return { ...pickNewest(bundled, managed, b, m, usable), pinnedVersionMissing: pinnedVersion };
48
51
  }
52
+ return pickNewest(bundled, managed, b, m, usable);
53
+ }
54
+ function pickNewest(bundled, managed, b, m, usable) {
49
55
  if (managed && b && m && m[0] === b[0] && newer(m, b) && usable(managed))
50
56
  return { ...managed, source: "managed" };
51
57
  return { ...bundled, source: "bundled" };
@@ -243,6 +243,7 @@ export async function runDaemon(opts) {
243
243
  }
244
244
  return legacyCache.units;
245
245
  };
246
+ let warnedMissingPin = null;
246
247
  const plan = () => {
247
248
  const config = readDaemonConfig(paths.configPath);
248
249
  const legacy = legacyUnits();
@@ -250,6 +251,13 @@ export async function runDaemon(opts) {
250
251
  const hqRoot = configured && fs.existsSync(configured) ? configured : undefined;
251
252
  // Re-picked every reconcile, so a newer hq-cloud from the updater is used as soon as it lands.
252
253
  const hqCloud = currentHqCloud(paths.root);
254
+ const missingPin = hqCloud?.pinnedVersionMissing ?? null;
255
+ if (missingPin !== warnedMissingPin) {
256
+ warnedMissingPin = missingPin;
257
+ if (missingPin && hqCloud) {
258
+ log("warn", `hq-cloud is pinned to ${missingPin}, which is not installed; running ${hqCloud.version} (${hqCloud.source}) until the updater installs it`);
259
+ }
260
+ }
253
261
  const runners = hqCloud ? hqCloudRunners(hqCloud.root) : { syncRunner: null, heartbeat: null, sessionHost: null };
254
262
  return planUnits(enabledServices(machineType, config), {
255
263
  machineType,
@@ -329,7 +329,7 @@ function availableBalancedAccount(hqRoot, lane, occupying, config, host, profile
329
329
  if (laneAdmissionCapReason(occupying, lane, config, host, profileMaxConcurrent, nowMs) !== "account_cap") {
330
330
  return undefined;
331
331
  }
332
- const pinnedModel = lane.model_source === undefined || lane.model_source === "cli"
332
+ const pinnedModel = lane.model_source === undefined || lane.model_source === "cli" || lane.model_source === "preserved"
333
333
  ? lane.model
334
334
  : lane.model_source === "profile"
335
335
  ? lane.model
@@ -14,6 +14,8 @@ import * as path from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import { PROVIDER_HOME_ENV, accountStatus, isLoggedOut, resolveAccount, selectBalancedAccount, } from "./accounts.js";
16
16
  import { effectiveAccountHome } from "./account-home.js";
17
+ import { buildHostCapacitySnapshot } from "./host-capacity.js";
18
+ import { selectClaudeAccount } from "./claude-account-balance.js";
17
19
  import { COMPACTION_PROMPT, compactionAfterUsage, compactionDue, compactLaneSession, } from "./compaction.js";
18
20
  import { installDrainHook } from "./hooks-install.js";
19
21
  import { loadLanesConfig, providerAutoCompactTokens, providerCompactAfterMs, } from "./config.js";
@@ -159,7 +161,7 @@ function spawnModelFailure(provider, accountId, config, pinnedModel, profileMode
159
161
  }
160
162
  }
161
163
  function selectSpawnAccount(input) {
162
- const { hqRoot, lane, config, existsSync, accountDeps, modelDeps, pinnedModel, profileModel, currentModelValidated, hostname } = input;
164
+ const { hqRoot, lane, config, existsSync, accountDeps, modelDeps, pinnedModel, profileModel, currentModelValidated, hostname, repairRecordedAccount, recordedModel, now } = input;
163
165
  const context = {
164
166
  hqRoot,
165
167
  existsSync,
@@ -187,7 +189,7 @@ function selectSpawnAccount(input) {
187
189
  const operatorMovePending = lane.pending_resume !== undefined &&
188
190
  lane.account_history?.at(-1)?.reason === "operator_move";
189
191
  if (current === undefined || !current.enabled) {
190
- reason = "account_unavailable";
192
+ reason = repairRecordedAccount ? "recorded_account_logged_out" : "account_unavailable";
191
193
  refusal = new AccountResolutionError(current === undefined ? "unknown_account" : "account_disabled", `Account ${JSON.stringify(lane.account)} is unavailable for ${lane.provider}`, { provider: lane.provider, account: lane.account });
192
194
  }
193
195
  else {
@@ -198,7 +200,7 @@ function selectSpawnAccount(input) {
198
200
  ...(accountDeps?.env ? { env: accountDeps.env } : {}),
199
201
  });
200
202
  if (isLoggedOut(status)) {
201
- reason = "account_unavailable";
203
+ reason = repairRecordedAccount ? "recorded_account_logged_out" : "account_unavailable";
202
204
  refusal = new LanesError("account_not_logged_in", `Account ${lane.account} for ${lane.provider} is not logged in`, { provider: lane.provider, account: lane.account });
203
205
  }
204
206
  else {
@@ -242,6 +244,33 @@ function selectSpawnAccount(input) {
242
244
  account: lane.account,
243
245
  });
244
246
  }
247
+ if (reason === "recorded_account_logged_out" && lane.provider === "claude") {
248
+ const lanes = listLaneIds(hqRoot)
249
+ .map((laneId) => readLane(hqRoot, laneId))
250
+ .filter((record) => record.host.kind === lane.host.kind && record.host.id === lane.host.id);
251
+ const capacity = buildHostCapacitySnapshot({
252
+ host: lane.host,
253
+ lanes,
254
+ config,
255
+ existsSync,
256
+ probe: accountDeps?.probe,
257
+ env: accountDeps?.env,
258
+ hostname,
259
+ now,
260
+ });
261
+ const selection = selectClaudeAccount({
262
+ hosts: [{ capacity, lanes, laneListAvailable: true }],
263
+ targetHost: lane.host,
264
+ config,
265
+ ...((recordedModel ?? pinnedModel ?? profileModel) !== undefined
266
+ ? { model: recordedModel ?? pinnedModel ?? profileModel }
267
+ : {}),
268
+ });
269
+ return {
270
+ account: resolveAccount(lane.provider, selection.account, config, context),
271
+ changeReason: "recorded_account_logged_out",
272
+ };
273
+ }
245
274
  const allowedAccounts = Object.entries(config.providers[lane.provider].accounts)
246
275
  .filter(([id, account]) => id !== lane.account && account.enabled)
247
276
  .filter(([id]) => accountStatus(lane.provider, id, config, {
@@ -254,12 +283,15 @@ function selectSpawnAccount(input) {
254
283
  .map(([id]) => id);
255
284
  try {
256
285
  return {
257
- account: selectBalancedAccount(lane.provider, config, { ...context, allowedAccounts }),
286
+ account: selectBalancedAccount(lane.provider, config, {
287
+ ...context,
288
+ allowedAccounts,
289
+ }),
258
290
  changeReason: reason,
259
291
  };
260
292
  }
261
293
  catch (err) {
262
- if (reason === "account_unavailable") {
294
+ if (reason === "account_unavailable" || reason === "recorded_account_logged_out") {
263
295
  if (refusal instanceof Error)
264
296
  throw refusal;
265
297
  throw new LanesError("account_not_logged_in", `Account ${lane.account} for ${lane.provider} is not logged in or available, and no other logged-in account can serve this lane`, { provider: lane.provider, account: lane.account });
@@ -274,7 +306,8 @@ function previouslyUnavailableAccounts(lane) {
274
306
  for (const entry of [...(lane.account_history ?? [])].reverse()) {
275
307
  if (entry.reason === "first_assignment" || entry.reason === "operator_move")
276
308
  break;
277
- if (entry.reason === "account_unavailable" && entry.from !== null)
309
+ if ((entry.reason === "account_unavailable" || entry.reason === "recorded_account_logged_out") &&
310
+ entry.from !== null)
278
311
  excluded.add(entry.from);
279
312
  }
280
313
  return excluded;
@@ -364,7 +397,9 @@ export async function retryLaneOnAlternateAccount(hqRoot, lane, reason, writer =
364
397
  };
365
398
  const profile = lane.worker_profile ? loadWorkerProfile(hqRoot, lane.worker_profile) : undefined;
366
399
  const profileModel = profile?.meta.provider === lane.provider ? profile.meta.model : undefined;
367
- const pinnedModel = lane.model_source === undefined || lane.model_source === "cli" ? lane.model : undefined;
400
+ const pinnedModel = lane.model_source === undefined || lane.model_source === "cli" || lane.model_source === "preserved"
401
+ ? lane.model
402
+ : undefined;
368
403
  const unavailableAccounts = previouslyUnavailableAccounts(lane);
369
404
  const allowedAccounts = Object.entries(config.providers[lane.provider].accounts)
370
405
  .filter(([id, account]) => id !== lane.account && !unavailableAccounts.has(id) && account.enabled)
@@ -1649,7 +1684,12 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1649
1684
  : undefined;
1650
1685
  const profileChanged = profile !== undefined && existing.worker_profile_hash !== profile.contentHash;
1651
1686
  const config = loadLanesConfig(hqRoot);
1652
- const pinnedModel = existing.model_source === undefined || existing.model_source === "cli"
1687
+ const repairRecordedAccount = plan.mode === "resume" &&
1688
+ (trigger.kind === "resume" || pendingResume?.trigger === "resume");
1689
+ const recordedModel = repairRecordedAccount && existing.model_source === "account"
1690
+ ? existing.model
1691
+ : undefined;
1692
+ let pinnedModel = existing.model_source === undefined || existing.model_source === "cli" || existing.model_source === "preserved"
1653
1693
  ? existing.model
1654
1694
  : undefined;
1655
1695
  const accountDeps = {
@@ -1669,6 +1709,9 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1669
1709
  profileModel,
1670
1710
  currentModelValidated: deps.currentModelValidated === true,
1671
1711
  hostname,
1712
+ repairRecordedAccount,
1713
+ ...(recordedModel !== undefined ? { recordedModel } : {}),
1714
+ now: (deps.now ?? (() => new Date()))(),
1672
1715
  });
1673
1716
  }
1674
1717
  catch (err) {
@@ -1683,6 +1726,9 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1683
1726
  process.stderr.write(`hq lanes: account bind failed for ${lane.lane_id}: ${err instanceof Error ? err.message : String(err)}\n`);
1684
1727
  throw err;
1685
1728
  }
1729
+ const preservedAccountModel = selection.changeReason === "recorded_account_logged_out" ? recordedModel : undefined;
1730
+ if (preservedAccountModel !== undefined)
1731
+ pinnedModel = preservedAccountModel;
1686
1732
  const resolved = selection.account;
1687
1733
  const status = accountStatus(lane.provider, resolved.id, config, { existsSync, hostname });
1688
1734
  if (isLoggedOut(status)) {
@@ -1738,6 +1784,9 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1738
1784
  return {
1739
1785
  ...current,
1740
1786
  account: resolved.id,
1787
+ ...(preservedAccountModel !== undefined
1788
+ ? { model: preservedAccountModel, model_source: "preserved" }
1789
+ : {}),
1741
1790
  account_history: appendCappedHistory(current.account_history ?? [], entry),
1742
1791
  };
1743
1792
  });
@@ -1934,7 +1983,9 @@ export function spawnLaneWorker(hqRoot, lane, deps = {}, launch = {}) {
1934
1983
  ...(model !== undefined
1935
1984
  ? {
1936
1985
  model_source: pinnedModel !== undefined
1937
- ? "cli"
1986
+ ? preservedAccountModel !== undefined || existing.model_source === "preserved"
1987
+ ? "preserved"
1988
+ : "cli"
1938
1989
  : profileModel !== undefined
1939
1990
  ? "profile"
1940
1991
  : accountCfg?.model !== undefined
@@ -2195,7 +2246,7 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
2195
2246
  ignoredFields: ignoredProfileFields,
2196
2247
  }
2197
2248
  : undefined;
2198
- const pinnedModel = lane.model_source === undefined || lane.model_source === "cli"
2249
+ const pinnedModel = lane.model_source === undefined || lane.model_source === "cli" || lane.model_source === "preserved"
2199
2250
  ? lane.model
2200
2251
  : undefined;
2201
2252
  const resolved = resolveAccount(lane.provider, lane.account, config, {
@@ -2307,7 +2358,7 @@ export function spawnEnvelopeRepair(hqRoot, lane, input, deps = {}) {
2307
2358
  if (model !== undefined) {
2308
2359
  next.model = model;
2309
2360
  next.model_source = pinnedModel !== undefined
2310
- ? "cli"
2361
+ ? lane.model_source === "preserved" ? "preserved" : "cli"
2311
2362
  : profileModel !== undefined
2312
2363
  ? "profile"
2313
2364
  : accountCfg?.model !== undefined
@@ -412,8 +412,8 @@ export interface LaneRecord {
412
412
  last_envelope?: LastEnvelopeRef;
413
413
  /** Effective model id recorded at create/spawn. Omitted when the provider chooses. */
414
414
  model?: string;
415
- /** Where the effective model came from; profile values are re-read on spawn. */
416
- model_source?: "cli" | "profile" | "account" | "provider" | "none";
415
+ /** Where the effective model came from; repaired models stay pinned across the account move. */
416
+ model_source?: "cli" | "profile" | "account" | "provider" | "preserved" | "none";
417
417
  /** Effective effort level passed to the provider at the last spawn (effort.ts). */
418
418
  effort?: string;
419
419
  /** Where `effort` came from. `cli` pins it across later spawns. */
@@ -523,7 +523,7 @@ export interface AccountHistoryEntry {
523
523
  from: string | null;
524
524
  to: string;
525
525
  at: string;
526
- reason: "first_assignment" | "model_change" | "usage_limit" | "account_unavailable" | "operator_move";
526
+ reason: "first_assignment" | "model_change" | "usage_limit" | "account_unavailable" | "recorded_account_logged_out" | "operator_move";
527
527
  }
528
528
  export interface PendingResume {
529
529
  /** Fresh answered-question launches need no previous session id. */