@indigoai-us/hq-cli 5.201.3 → 5.201.4

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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.201.4] — 2026-09-25
6
+
7
+ ### Fixed
8
+
9
+ - Claude account balancing on lane resume now respects the attached worker
10
+ profile's account pin, skips accounts pinned to a different model, and treats
11
+ `--account` naming the current account as a keep request for message/resume.
12
+
5
13
  ## [5.201.3] — 2026-09-25
6
14
 
7
15
  ### Fixed
@@ -4905,8 +4905,26 @@ function applyResumeProfileAndEffort(current, next, input) {
4905
4905
  }
4906
4906
  }
4907
4907
  function recordResumeSetup(ctx, lane, input) {
4908
+ if (input.cliModel !== undefined) {
4909
+ const model = input.cliModel.trim();
4910
+ if (!model)
4911
+ throw new LanesError("empty_model", "--model is empty or whitespace-only", { value: input.cliModel });
4912
+ const config = loadLanesConfig(ctx.hqRoot());
4913
+ const account = lane.account === undefined
4914
+ ? undefined
4915
+ : config.providers[lane.provider].accounts[lane.account];
4916
+ if (account === undefined) {
4917
+ 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
+ }
4919
+ validateLaneModel(lane.provider, model, account.home, {
4920
+ readFileSync: ctx.readFileSync,
4921
+ probe: ctx.probeProvider,
4922
+ env: ctx.env,
4923
+ });
4924
+ }
4908
4925
  if (input.profile === undefined &&
4909
4926
  input.effort === undefined &&
4927
+ input.cliModel === undefined &&
4910
4928
  input.meshCompany === undefined)
4911
4929
  return lane;
4912
4930
  return casUpdateLane(ctx.hqRoot(), lane.lane_id, (current) => {
@@ -4915,6 +4933,10 @@ function recordResumeSetup(ctx, lane, input) {
4915
4933
  }
4916
4934
  const next = { ...current };
4917
4935
  applyResumeProfileAndEffort(current, next, input);
4936
+ if (input.cliModel !== undefined) {
4937
+ next.model = input.cliModel.trim();
4938
+ next.model_source = "cli";
4939
+ }
4918
4940
  if (input.meshCompany !== undefined) {
4919
4941
  if (current.company === null)
4920
4942
  next.mesh_company = input.meshCompany;
@@ -5147,7 +5169,7 @@ async function prepareAccountMove(ctx, lane, input) {
5147
5169
  return { lane: moved, from, to: target, copy, queued };
5148
5170
  }, ctx.sleep);
5149
5171
  }
5150
- async function selectAutomaticClaudeResumeAccount(ctx, lane) {
5172
+ async function selectAutomaticClaudeResumeAccount(ctx, lane, workerProfile) {
5151
5173
  if (lane.provider !== "claude")
5152
5174
  return undefined;
5153
5175
  if (!await ctx.laneClaudeAccountBalanceEnabled(lane.company)) {
@@ -5171,6 +5193,16 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane) {
5171
5193
  }
5172
5194
  const hqRoot = ctx.hqRoot();
5173
5195
  const config = loadLanesConfig(hqRoot);
5196
+ const profile = workerProfile?.profile ?? (lane.worker_profile !== undefined
5197
+ ? loadWorkerProfile(hqRoot, lane.worker_profile)
5198
+ : undefined);
5199
+ const profileDefaultsApply = profile !== undefined && profile.meta.provider === lane.provider;
5200
+ const profileAccount = profileDefaultsApply ? profile?.meta.account : undefined;
5201
+ const profileAccounts = profileDefaultsApply ? profile?.meta.accounts : undefined;
5202
+ const profilePin = profileAccounts ?? (profileAccount === undefined ? undefined : [profileAccount]);
5203
+ const allowedAccounts = profilePin !== undefined && lane.admission_accounts !== undefined
5204
+ ? profilePin.filter((account) => lane.admission_accounts.includes(account))
5205
+ : profilePin ?? lane.admission_accounts;
5174
5206
  const currentConfig = lane.account === undefined
5175
5207
  ? undefined
5176
5208
  : config.providers.claude.accounts[lane.account];
@@ -5181,20 +5213,71 @@ async function selectAutomaticClaudeResumeAccount(ctx, lane) {
5181
5213
  probe: ctx.probeProvider,
5182
5214
  env: ctx.env,
5183
5215
  }).logged_in;
5216
+ const pinnedModel = workerProfile !== undefined
5217
+ ? lane.model_source === "cli" ? lane.model : undefined
5218
+ : lane.model_source === undefined || lane.model_source === "cli"
5219
+ ? lane.model
5220
+ : undefined;
5221
+ const model = resolveLaneModel({
5222
+ cliModel: pinnedModel,
5223
+ profileModel: profileDefaultsApply ? profile?.meta.model : undefined,
5224
+ accountModel: lane.account === undefined ? undefined : config.providers.claude.accounts[lane.account]?.model,
5225
+ providerModel: config.providers.claude.model,
5226
+ });
5184
5227
  const accountNeedsRepair = currentConfig?.enabled !== true || currentLogin === false;
5185
5228
  const mayRebalance = BALANCE_MOVABLE_STATES.includes(lane.state);
5186
5229
  if (!accountNeedsRepair && !mayRebalance)
5187
5230
  return undefined;
5188
5231
  const balanceHosts = await loadClaudeBalanceHostSnapshots(ctx, hqRoot, config);
5232
+ const targetHost = balanceHosts.snapshots.find((host) => host.capacity.host.kind === lane.host.kind && host.capacity.host.id === lane.host.id);
5233
+ const currentTargetAccount = lane.account === undefined
5234
+ ? undefined
5235
+ : targetHost?.capacity.providers.claude.accounts[lane.account];
5236
+ if (profilePin !== undefined &&
5237
+ lane.account !== undefined &&
5238
+ allowedAccounts?.includes(lane.account) &&
5239
+ currentConfig?.enabled === true &&
5240
+ currentTargetAccount?.enabled === true &&
5241
+ currentTargetAccount.logged_in === true &&
5242
+ (model === undefined || currentConfig.model === undefined || currentConfig.model === model))
5243
+ return undefined;
5189
5244
  const selected = selectClaudeAccount({
5190
5245
  hosts: balanceHosts.snapshots,
5191
5246
  targetHost: lane.host,
5192
5247
  config,
5193
- ...(lane.admission_accounts !== undefined ? { allowedAccounts: lane.admission_accounts } : {}),
5248
+ ...(allowedAccounts !== undefined ? { allowedAccounts } : {}),
5249
+ ...(model !== undefined ? { model } : {}),
5194
5250
  unavailableHosts: balanceHosts.unavailableHosts,
5195
5251
  });
5196
5252
  return { ...selected, selected_at: ctx.now().toISOString() };
5197
5253
  }
5254
+ function accountRequestKeepsCurrentLane(lane, rawAccount) {
5255
+ const raw = rawAccount.trim();
5256
+ const colon = raw.indexOf(":");
5257
+ const requestedProvider = colon > 0 ? raw.slice(0, colon) : undefined;
5258
+ const requestedAccount = colon > 0 ? raw.slice(colon + 1) : raw;
5259
+ return requestedAccount.length > 0 &&
5260
+ requestedAccount === lane.account &&
5261
+ (requestedProvider === undefined || requestedProvider === lane.provider);
5262
+ }
5263
+ function assertCurrentResumeAccountAvailable(ctx, lane) {
5264
+ const config = loadLanesConfig(ctx.hqRoot());
5265
+ const account = lane.account === undefined
5266
+ ? undefined
5267
+ : config.providers[lane.provider].accounts[lane.account];
5268
+ if (account === undefined || !account.enabled) {
5269
+ throw new AccountResolutionError(account === undefined ? "unknown_account" : "account_disabled", account === undefined
5270
+ ? `Lane ${lane.lane_id} records account ${JSON.stringify(lane.account)} which is not configured for ${lane.provider}`
5271
+ : `Account ${JSON.stringify(lane.account)} is disabled${account.disabled_reason ? `: ${account.disabled_reason}` : ""}`, { provider: lane.provider, account: lane.account ?? "", ...(account?.disabled_reason !== undefined ? { reason: account.disabled_reason } : {}) });
5272
+ }
5273
+ if (accountStatus(lane.provider, lane.account, config, {
5274
+ existsSync: ctx.existsSync,
5275
+ probe: ctx.probeProvider,
5276
+ env: ctx.env,
5277
+ }).logged_in !== true) {
5278
+ throw new LanesError("account_not_logged_in", `Account ${lane.account} for ${lane.provider} is not logged in (no credential file)`, { provider: lane.provider, account: lane.account, lane_id: lane.lane_id });
5279
+ }
5280
+ }
5198
5281
  async function runMessage(ctx, laneId, opts) {
5199
5282
  const hqRoot = ctx.hqRoot();
5200
5283
  const text = opts.text;
@@ -5215,16 +5298,19 @@ async function runMessage(ctx, laneId, opts) {
5215
5298
  // The final state transition repeats this check under the senior lock.
5216
5299
  await withResumeHireAdmission(ctx, lane, () => undefined);
5217
5300
  lane = laneWithRecoveredProviderSession(hqRoot, lane);
5218
- // An account move is refused on its own terms first: a running lane and a
5219
- // provider without session resume get the move's named errors, not the
5220
- // generic delivery refusal below.
5221
- if (opts.account !== undefined && lane.state === "running") {
5301
+ const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
5302
+ if (keepCurrentAccount)
5303
+ assertCurrentResumeAccountAvailable(ctx, lane);
5304
+ // A real account move is refused on its own terms first: a running lane and
5305
+ // a provider without session resume get the move's named errors, not the
5306
+ // generic delivery refusal below. Naming the current account is a keep.
5307
+ if (opts.account !== undefined && !keepCurrentAccount && lane.state === "running") {
5222
5308
  throw new LanesError("lane_running", `Lane ${laneId} is running; stop it or wait for it to exit before moving its session`, {
5223
5309
  lane_id: laneId,
5224
5310
  state: lane.state,
5225
5311
  });
5226
5312
  }
5227
- if (opts.account !== undefined && !providerSupportsSessionResume(lane.provider)) {
5313
+ if (opts.account !== undefined && !keepCurrentAccount && !providerSupportsSessionResume(lane.provider)) {
5228
5314
  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 });
5229
5315
  }
5230
5316
  // One decision point for how a follow-up reaches this lane (delivery.ts).
@@ -5239,6 +5325,9 @@ async function runMessage(ctx, laneId, opts) {
5239
5325
  message,
5240
5326
  });
5241
5327
  }
5328
+ if (keepCurrentAccount && opts.model !== undefined && mechanism !== "resume") {
5329
+ throw new LanesError("model_requires_resume", "--model with --account requires a message that resumes the lane", { lane_id: laneId, state: lane.state });
5330
+ }
5242
5331
  if (opts.worker !== undefined && mechanism === "dropbox") {
5243
5332
  throw new LanesError("worker_profile_requires_resume", `Lane ${laneId} is still running; --worker can only be applied when message starts a resumed provider turn`, { lane_id: laneId, state: lane.state });
5244
5333
  }
@@ -5252,12 +5341,12 @@ async function runMessage(ctx, laneId, opts) {
5252
5341
  throw new LanesError("model_requires_account", "--model on hq lanes message is only accepted together with --account", {});
5253
5342
  }
5254
5343
  const automaticAccountSelection = opts.account === undefined && mechanism === "resume"
5255
- ? await selectAutomaticClaudeResumeAccount(ctx, lane)
5344
+ ? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile)
5256
5345
  : undefined;
5257
5346
  const resumeEffort = resolveResumeEffort(ctx, lane, {
5258
5347
  cliEffort,
5259
5348
  profile: resumeProfile,
5260
- account: opts.account ?? automaticAccountSelection?.account,
5349
+ account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.account,
5261
5350
  model: opts.model,
5262
5351
  });
5263
5352
  const messageId = mintMessageId();
@@ -5280,7 +5369,7 @@ async function runMessage(ctx, laneId, opts) {
5280
5369
  let resumed = null;
5281
5370
  let move = null;
5282
5371
  let queuedOnAdmission = false;
5283
- if (opts.account !== undefined) {
5372
+ if (opts.account !== undefined && !keepCurrentAccount) {
5284
5373
  // A moved message is always a resume on the target account: a running
5285
5374
  // lane (the dropbox mechanism) is refused inside prepareAccountMove.
5286
5375
  move = await prepareAccountMove(ctx, lane, {
@@ -5321,13 +5410,14 @@ async function runMessage(ctx, laneId, opts) {
5321
5410
  }
5322
5411
  }
5323
5412
  }
5324
- if (resumeProfile !== undefined && opts.account === undefined) {
5413
+ if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
5325
5414
  validateResumeProfileModel(ctx, lane, resumeProfile.profile);
5326
5415
  }
5327
- if (move === null && (resumeProfile !== undefined || resumeEffort !== undefined)) {
5416
+ if (move === null && (resumeProfile !== undefined || resumeEffort !== undefined || (keepCurrentAccount && opts.model !== undefined))) {
5328
5417
  lane = recordResumeSetup(ctx, lane, {
5329
5418
  ...(resumeProfile !== undefined ? { profile: resumeProfile } : {}),
5330
5419
  ...(resumeEffort !== undefined ? { effort: resumeEffort } : {}),
5420
+ ...(keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
5331
5421
  });
5332
5422
  }
5333
5423
  if (move?.queued) {
@@ -5751,20 +5841,23 @@ async function runResume(ctx, laneId, opts) {
5751
5841
  const resumeProfile = opts.worker !== undefined
5752
5842
  ? resolveResumeProfile(ctx, lane, opts.worker)
5753
5843
  : undefined;
5754
- if (resumeProfile !== undefined && opts.account === undefined) {
5844
+ const keepCurrentAccount = opts.account !== undefined && accountRequestKeepsCurrentLane(lane, opts.account);
5845
+ if (keepCurrentAccount)
5846
+ assertCurrentResumeAccountAvailable(ctx, lane);
5847
+ if (resumeProfile !== undefined && (opts.account === undefined || keepCurrentAccount)) {
5755
5848
  validateResumeProfileModel(ctx, lane, resumeProfile.profile);
5756
5849
  }
5757
5850
  const automaticAccountSelection = opts.account === undefined
5758
- ? await selectAutomaticClaudeResumeAccount(ctx, lane)
5851
+ ? await selectAutomaticClaudeResumeAccount(ctx, lane, resumeProfile)
5759
5852
  : undefined;
5760
5853
  const resumeEffort = resolveResumeEffort(ctx, lane, {
5761
5854
  cliEffort,
5762
5855
  profile: resumeProfile,
5763
- account: opts.account ?? automaticAccountSelection?.account,
5856
+ account: keepCurrentAccount ? lane.account : opts.account ?? automaticAccountSelection?.account,
5764
5857
  model: opts.model,
5765
5858
  });
5766
5859
  let move = null;
5767
- if (opts.account !== undefined) {
5860
+ if (opts.account !== undefined && !keepCurrentAccount) {
5768
5861
  move = await prepareAccountMove(ctx, lane, {
5769
5862
  account: opts.account,
5770
5863
  cliModel: opts.model?.trim() || undefined,
@@ -5805,6 +5898,7 @@ async function runResume(ctx, laneId, opts) {
5805
5898
  lane = recordResumeSetup(ctx, lane, {
5806
5899
  ...(move === null && resumeProfile !== undefined ? { profile: resumeProfile } : {}),
5807
5900
  ...(move === null && resumeEffort !== undefined ? { effort: resumeEffort } : {}),
5901
+ ...(move === null && keepCurrentAccount && opts.model !== undefined ? { cliModel: opts.model } : {}),
5808
5902
  ...(requestedMeshCompany !== undefined
5809
5903
  ? { meshCompany: requestedMeshCompany, threadId: meshThreadId }
5810
5904
  : {}),
@@ -67,6 +67,7 @@ export declare function selectClaudeAccount(input: {
67
67
  targetHost: LaneHost;
68
68
  config: LanesConfig;
69
69
  allowedAccounts?: readonly string[];
70
+ model?: string;
70
71
  explicitAccount?: string;
71
72
  unavailableHosts?: readonly BalanceHostGap[];
72
73
  }): ClaudeAccountSelection;
@@ -91,7 +91,10 @@ function buildLoads(input) {
91
91
  const loads = Object.keys(configAccounts).sort().map((account) => {
92
92
  const hosts = input.hosts.map((host) => accountHostLoad(host, account));
93
93
  const targetLoad = hosts.find((host) => sameHost(host.host, targetHost));
94
- const ineligible = targetIneligibilityReason(account, configAccounts[account]?.enabled === true, input.allowedAccounts, targetLoad);
94
+ const configuredModel = configAccounts[account]?.model;
95
+ const ineligible = targetIneligibilityReason(account, configAccounts[account]?.enabled === true, input.allowedAccounts, targetLoad) ?? (input.model !== undefined && configuredModel !== undefined && configuredModel !== input.model
96
+ ? "model_mismatch"
97
+ : undefined);
95
98
  if (ineligible !== undefined)
96
99
  skipped.push({ account, host: targetHost, reason: ineligible });
97
100
  const knownCounts = hosts.flatMap((host) => host.live_lanes === null ? [] : [host.live_lanes]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.201.3",
3
+ "version": "5.201.4",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {