@indigoai-us/hq-cli 5.203.0 → 5.204.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,7 +19,7 @@ export declare const CLAUDE_CREDENTIALS_NAME = ".credentials.json";
19
19
  * file here at all.
20
20
  */
21
21
  export declare const PROVIDER_CREDENTIAL_FILES: Record<LaneProvider, readonly string[]>;
22
- export type AuthState = "file" | "status" | "absent" | "unknown";
22
+ export type AuthState = "file" | "status" | "absent" | "unknown" | "home_missing";
23
23
  export interface AccountStatusProbeResult {
24
24
  status: number | null;
25
25
  stdout: string;
@@ -27,12 +27,14 @@ export interface AccountStatusProbeResult {
27
27
  }
28
28
  export interface AccountDeps {
29
29
  existsSync: (p: string) => boolean;
30
+ hostname?: string;
30
31
  /** Provider status output is parsed for a boolean only; it is never printed. */
31
32
  probe?: (command: string, args: string[], env: NodeJS.ProcessEnv) => AccountStatusProbeResult;
32
33
  env?: NodeJS.ProcessEnv;
33
34
  }
34
35
  export interface AccountResolveContext {
35
36
  hqRoot?: string;
37
+ hostname?: string;
36
38
  existsSync?: (p: string) => boolean;
37
39
  accountDeps?: Omit<AccountDeps, "existsSync">;
38
40
  loadLanes?: (hqRoot: string) => LaneRecord[];
@@ -61,6 +63,7 @@ export interface AccountStatus {
61
63
  provider: LaneProvider;
62
64
  id: string;
63
65
  home: string;
66
+ home_source?: "host" | "default";
64
67
  enabled: boolean;
65
68
  disabled_reason?: string;
66
69
  /** Present in personal/lanes/config.json. */
@@ -8,6 +8,7 @@
8
8
  * module never reads, prints, copies, or hashes credential files.
9
9
  */
10
10
  import * as fs from "node:fs";
11
+ import { effectiveAccountHome } from "./account-home.js";
11
12
  import * as path from "node:path";
12
13
  import { AccountResolutionError } from "./errors.js";
13
14
  import { isTerminalLaneState } from "./types.js";
@@ -179,11 +180,11 @@ function throwIfQuotaBlocked(block, provider, accountId) {
179
180
  quota_reset_at: block.quota_reset_at ?? null,
180
181
  });
181
182
  }
182
- function boundAccount(provider, id, account) {
183
+ function boundAccount(provider, id, account, hostname) {
183
184
  return {
184
185
  provider,
185
186
  id,
186
- home: account.home,
187
+ home: effectiveAccountHome(account, hostname).home,
187
188
  enabled: true,
188
189
  envVar: PROVIDER_HOME_ENV[provider],
189
190
  };
@@ -209,6 +210,7 @@ export function selectBalancedAccount(provider, config, ctx = {}) {
209
210
  existsSync,
210
211
  ...(ctx.accountDeps?.probe ? { probe: ctx.accountDeps.probe } : {}),
211
212
  ...(ctx.accountDeps?.env ? { env: ctx.accountDeps.env } : {}),
213
+ ...(ctx.hostname !== undefined ? { hostname: ctx.hostname } : {}),
212
214
  });
213
215
  if (status.logged_in === true)
214
216
  proven.push(id);
@@ -251,7 +253,7 @@ export function selectBalancedAccount(provider, config, ctx = {}) {
251
253
  }))
252
254
  .sort(compareEligible);
253
255
  const chosen = preferred ?? ranked[0].id;
254
- return boundAccount(provider, chosen, providerCfg.accounts[chosen]);
256
+ return boundAccount(provider, chosen, providerCfg.accounts[chosen], ctx.hostname);
255
257
  }
256
258
  export function resolveAccount(provider, requestedId, config, ctx = {}) {
257
259
  const providerCfg = config.providers[provider];
@@ -276,7 +278,7 @@ export function resolveAccount(provider, requestedId, config, ctx = {}) {
276
278
  const block = quotaBlockFor(loadQuotaState(ctx.hqRoot), provider, requestedId, modelForAccount(provider, requestedId, config, ctx), now);
277
279
  throwIfQuotaBlocked(block, provider, requestedId);
278
280
  }
279
- return boundAccount(provider, requestedId, account);
281
+ return boundAccount(provider, requestedId, account, ctx.hostname);
280
282
  }
281
283
  return selectBalancedAccount(provider, config, ctx);
282
284
  }
@@ -299,12 +301,14 @@ export function accountStatus(provider, accountId, config, deps = defaultAccount
299
301
  envVar,
300
302
  };
301
303
  }
302
- const present = hasCredentialFile(provider, account.home, deps.existsSync);
303
- let auth_state = authStateFor(provider, present);
304
- let logged_in = loggedInFromAuthState(auth_state);
305
- if (!present && provider === "claude" && deps.probe !== undefined) {
304
+ const effective = effectiveAccountHome(account, deps.hostname);
305
+ const homeExists = deps.existsSync(effective.home);
306
+ const present = homeExists && hasCredentialFile(provider, effective.home, deps.existsSync);
307
+ let auth_state = homeExists ? authStateFor(provider, present) : "home_missing";
308
+ let logged_in = homeExists ? loggedInFromAuthState(auth_state) : false;
309
+ if (homeExists && !present && provider === "claude" && deps.probe !== undefined) {
306
310
  try {
307
- const result = deps.probe("claude", ["auth", "status"], { ...(deps.env ?? process.env), CLAUDE_CONFIG_DIR: account.home });
311
+ const result = deps.probe("claude", ["auth", "status"], { ...(deps.env ?? process.env), CLAUDE_CONFIG_DIR: effective.home });
308
312
  const providerStatus = claudeLoginStatus(result);
309
313
  if (providerStatus !== null) {
310
314
  auth_state = "status";
@@ -318,7 +322,8 @@ export function accountStatus(provider, accountId, config, deps = defaultAccount
318
322
  const status = {
319
323
  provider,
320
324
  id: accountId,
321
- home: account.home,
325
+ home: effective.home,
326
+ home_source: effective.source,
322
327
  enabled: account.enabled,
323
328
  configured: true,
324
329
  logged_in,
@@ -1,3 +1,4 @@
1
+ import type { AuthState } from "./accounts.js";
1
2
  import type { HostCapacityAccount, HostCapacitySnapshot } from "./host-capacity.js";
2
3
  import type { LaneHost, LaneRecord, LanesConfig } from "./types.js";
3
4
  export declare const CLAUDE_ACCOUNT_BALANCE_FLAG = "lanes.claude-account-balance";
@@ -20,6 +21,8 @@ export interface AccountHostLoad {
20
21
  host: LaneHost;
21
22
  enabled: boolean;
22
23
  logged_in: boolean | null;
24
+ account_in_snapshot: boolean;
25
+ auth_state?: AuthState;
23
26
  live_lanes: number | null;
24
27
  lane_counts: {
25
28
  running: number | null;
@@ -21,7 +21,8 @@ function accountHostLoad(host, accountId) {
21
21
  return {
22
22
  host: host.capacity.host,
23
23
  enabled: false,
24
- logged_in: false,
24
+ logged_in: null,
25
+ account_in_snapshot: false,
25
26
  live_lanes: host.laneListAvailable ? countLaneStates(host.lanes, accountId).live : null,
26
27
  lane_counts: host.laneListAvailable
27
28
  ? countLaneStates(host.lanes, accountId)
@@ -35,6 +36,8 @@ function accountHostLoad(host, accountId) {
35
36
  host: host.capacity.host,
36
37
  enabled: hostCapacity.enabled,
37
38
  logged_in: hostCapacity.logged_in,
39
+ account_in_snapshot: true,
40
+ ...(hostCapacity.auth_state !== undefined ? { auth_state: hostCapacity.auth_state } : {}),
38
41
  live_lanes: counts.live,
39
42
  lane_counts: { running: counts.running, queued: counts.queued, awaiting_input: counts.awaiting_input },
40
43
  count_source: "lane_list",
@@ -45,6 +48,8 @@ function accountHostLoad(host, accountId) {
45
48
  host: host.capacity.host,
46
49
  enabled: hostCapacity.enabled,
47
50
  logged_in: hostCapacity.logged_in,
51
+ account_in_snapshot: true,
52
+ ...(hostCapacity.auth_state !== undefined ? { auth_state: hostCapacity.auth_state } : {}),
48
53
  live_lanes: hostCapacity.lane_counts.live,
49
54
  lane_counts: {
50
55
  running: hostCapacity.lane_counts.running,
@@ -58,6 +63,8 @@ function accountHostLoad(host, accountId) {
58
63
  host: host.capacity.host,
59
64
  enabled: hostCapacity.enabled,
60
65
  logged_in: hostCapacity.logged_in,
66
+ account_in_snapshot: true,
67
+ ...(hostCapacity.auth_state !== undefined ? { auth_state: hostCapacity.auth_state } : {}),
61
68
  live_lanes: hostCapacity.running + hostCapacity.queued,
62
69
  lane_counts: { running: hostCapacity.running, queued: hostCapacity.queued, awaiting_input: null },
63
70
  count_source: "capacity_partial",
@@ -74,10 +81,16 @@ function selectionSignal(loads) {
74
81
  : "lane_count";
75
82
  }
76
83
  function targetIneligibilityReason(accountId, globalEnabled, allowedAccounts, target) {
77
- if (!globalEnabled || !target.enabled)
84
+ if (!globalEnabled)
78
85
  return "disabled";
79
86
  if (allowedAccounts !== undefined && !allowedAccounts.includes(accountId))
80
87
  return "not_allowed_by_lane";
88
+ if (!target.account_in_snapshot)
89
+ return "not_in_snapshot";
90
+ if (!target.enabled)
91
+ return "disabled";
92
+ if (target.auth_state === "home_missing")
93
+ return "home_missing";
81
94
  if (target.logged_in === false)
82
95
  return "not_logged_in";
83
96
  if (target.logged_in === null)
@@ -203,6 +216,9 @@ export function planClaudeBalanceMoves(input) {
203
216
  const hostSnapshot = input.hosts.find((row) => sameHost(row.capacity.host, host));
204
217
  if (!hostSnapshot)
205
218
  continue;
219
+ const sourceStatus = hostSnapshot.capacity.providers.claude.accounts[source];
220
+ if (sourceStatus === undefined)
221
+ continue;
206
222
  const candidates = Object.keys(input.config.providers.claude.accounts)
207
223
  .filter((account) => account !== source && input.config.providers.claude.accounts[account]?.enabled === true)
208
224
  .map((account) => ({
@@ -212,8 +228,7 @@ export function planClaudeBalanceMoves(input) {
212
228
  }))
213
229
  .filter((candidate) => candidate.targetStatus?.enabled === true && candidate.targetStatus.logged_in === true)
214
230
  .sort((a, b) => a.load - b.load || a.account.localeCompare(b.account));
215
- const sourceStatus = hostSnapshot.capacity.providers.claude.accounts[source];
216
- const sourceUnavailable = sourceStatus?.enabled !== true || sourceStatus.logged_in !== true;
231
+ const sourceUnavailable = sourceStatus.enabled !== true || sourceStatus.logged_in !== true;
217
232
  const target = candidates[0];
218
233
  const laneIsLive = isBalanceLive(lane);
219
234
  if (!target || (!sourceUnavailable && (!laneIsLive || target.load >= sourceLoad)))
@@ -7,7 +7,7 @@
7
7
  * that resolve to the same home are refused (identity-keyed-cap rule).
8
8
  */
9
9
  import * as fs from "node:fs";
10
- import { type LaneProvider, type LanesConfig, type LivenessWindows, type ProviderConfig } from "./types.js";
10
+ import { type AccountConfig, type LaneProvider, type LanesConfig, type LivenessWindows, type ProviderConfig } from "./types.js";
11
11
  export declare const LANES_CONFIG_REL: string;
12
12
  export declare const DEFAULT_MAX_QUEUED = 3;
13
13
  export declare const DEFAULT_CPU_BUSY_PAUSE = 0.85;
@@ -55,6 +55,7 @@ export declare function expandHome(home: string, homedir: string): string;
55
55
  * not-yet-created spellings of the same directory still collide.
56
56
  */
57
57
  export declare function canonicalizeHome(home: string, deps?: Pick<ConfigLoadDeps, "homedir" | "realpathSync" | "existsSync">): string;
58
+ export declare function assertUniqueAccountHomes(provider: LaneProvider, accounts: Record<string, AccountConfig>): void;
58
59
  export declare function documentedDefaultConfig(deps?: ConfigLoadDeps): LanesConfig;
59
60
  export declare function parseLanesConfig(raw: unknown, deps?: ConfigLoadDeps): LanesConfig;
60
61
  export declare function lanesConfigPath(hqRoot: string): string;
@@ -110,6 +110,17 @@ export function canonicalizeHome(home, deps = defaultDeps()) {
110
110
  }
111
111
  return expanded;
112
112
  }
113
+ function canonicalizeHostHome(hostname, home, deps) {
114
+ if (hostname === os.hostname())
115
+ return canonicalizeHome(home, deps);
116
+ if (home === "~" || home.startsWith("~/")) {
117
+ return home === "~" ? home : `~/${path.posix.normalize(home.slice(2))}`;
118
+ }
119
+ if (!path.isAbsolute(home)) {
120
+ throw new LanesConfigError("invalid_config", `Host-scoped home for remote host ${JSON.stringify(hostname)} must be absolute or start with ~/`, { hostname, home });
121
+ }
122
+ return path.normalize(home);
123
+ }
113
124
  function isPlainObject(value) {
114
125
  return value !== null && typeof value === "object" && !Array.isArray(value);
115
126
  }
@@ -143,6 +154,27 @@ function parseAccount(provider, id, raw, deps) {
143
154
  home: canonicalizeHome(raw.home, deps),
144
155
  enabled: raw.enabled,
145
156
  };
157
+ if (raw.hosts !== undefined) {
158
+ const field = `providers.${provider}.accounts.${id}.hosts`;
159
+ if (!isPlainObject(raw.hosts)) {
160
+ throw new LanesConfigError("invalid_config", `${field} must be an object keyed by non-empty hostnames`, { provider, account: id, field });
161
+ }
162
+ const hosts = Object.create(null);
163
+ for (const [hostname, value] of Object.entries(raw.hosts)) {
164
+ const hostField = `${field}.${hostname}`;
165
+ if (hostname.trim().length === 0 || hostname.trim() !== hostname) {
166
+ throw new LanesConfigError("invalid_config", `${field} hostnames must be non-empty and have no surrounding whitespace`, { provider, account: id, field, hostname });
167
+ }
168
+ if (!isPlainObject(value) || Object.keys(value).some((key) => key !== "home")) {
169
+ throw new LanesConfigError("invalid_config", `${hostField} must be an object containing only a home field`, { provider, account: id, field: hostField });
170
+ }
171
+ if (typeof value.home !== "string" || value.home.trim().length === 0) {
172
+ throw new LanesConfigError("invalid_config", `${hostField}.home must be a non-empty string`, { provider, account: id, field: `${hostField}.home` });
173
+ }
174
+ hosts[hostname] = { home: canonicalizeHostHome(hostname, value.home, deps) };
175
+ }
176
+ account.hosts = hosts;
177
+ }
146
178
  if (raw.disabled_reason !== undefined) {
147
179
  if (typeof raw.disabled_reason !== "string") {
148
180
  throw new LanesConfigError("invalid_config", `providers.${provider}.accounts.${id}.disabled_reason must be a string`, { provider, account: id });
@@ -198,19 +230,29 @@ function parsePreferredAccounts(provider, value, accounts) {
198
230
  }
199
231
  return [...preferred];
200
232
  }
201
- function rejectDuplicateHomes(provider, accounts) {
202
- const seen = new Map();
203
- for (const [id, account] of Object.entries(accounts)) {
204
- const prior = seen.get(account.home);
205
- if (prior !== undefined) {
206
- throw new LanesConfigError("duplicate_account_home", `providers.${provider} accounts ${JSON.stringify(prior)} and ${JSON.stringify(id)} resolve to the same home ${account.home}`, {
207
- provider,
208
- accounts: [prior, id],
209
- home: account.home,
210
- });
233
+ export function assertUniqueAccountHomes(provider, accounts) {
234
+ const hostnames = new Set(Object.values(accounts).flatMap((account) => Object.keys(account.hosts ?? {})));
235
+ const check = (hostname) => {
236
+ const seen = new Map();
237
+ for (const [id, account] of Object.entries(accounts)) {
238
+ const home = hostname === undefined
239
+ ? account.home
240
+ : account.hosts?.[hostname]?.home ?? account.home;
241
+ const prior = seen.get(home);
242
+ if (prior !== undefined) {
243
+ throw new LanesConfigError("duplicate_account_home", `providers.${provider} accounts ${JSON.stringify(prior)} and ${JSON.stringify(id)} resolve to the same home ${home}${hostname === undefined ? "" : ` on host ${JSON.stringify(hostname)}`}`, {
244
+ provider,
245
+ accounts: [prior, id],
246
+ home,
247
+ ...(hostname !== undefined ? { hostname } : {}),
248
+ });
249
+ }
250
+ seen.set(home, id);
211
251
  }
212
- seen.set(account.home, id);
213
- }
252
+ };
253
+ check();
254
+ for (const hostname of hostnames)
255
+ check(hostname);
214
256
  }
215
257
  function defaultAccountMap(provider, deps) {
216
258
  return {
@@ -240,7 +282,7 @@ function parseProvider(provider, raw, deps) {
240
282
  accounts[id] = parseAccount(provider, id, value, deps);
241
283
  }
242
284
  }
243
- rejectDuplicateHomes(provider, accounts);
285
+ assertUniqueAccountHomes(provider, accounts);
244
286
  // default_account is accepted so existing files parse, and ignored for
245
287
  // selection. An omitted field is not an error.
246
288
  let defaultAccount;
@@ -1,7 +1,10 @@
1
+ import { type AuthState } from "./accounts.js";
1
2
  import type { LaneHost, LaneProvider, LaneRecord, LanesConfig } from "./types.js";
2
3
  export interface HostCapacityAccount {
3
4
  enabled: boolean;
4
5
  logged_in: boolean | null;
6
+ /** Absent on snapshots from older CLIs. */
7
+ auth_state?: AuthState;
5
8
  max_running: number;
6
9
  running: number;
7
10
  queued: number;
@@ -49,6 +52,7 @@ export declare function buildHostCapacitySnapshot(input: {
49
52
  stderr: string;
50
53
  };
51
54
  env?: NodeJS.ProcessEnv;
55
+ hostname?: string;
52
56
  now: Date;
53
57
  }): HostCapacitySnapshot;
54
58
  /** Reject malformed remote snapshots before they can influence host selection. */
@@ -23,12 +23,14 @@ export function buildHostCapacitySnapshot(input) {
23
23
  existsSync: input.existsSync,
24
24
  ...(input.probe !== undefined ? { probe: input.probe } : {}),
25
25
  ...(input.env !== undefined ? { env: input.env } : {}),
26
+ ...(input.hostname !== undefined ? { hostname: input.hostname } : {}),
26
27
  });
27
28
  const accountLanes = providerLanes.filter((lane) => lane.account === id);
28
29
  const liveLanes = accountLanes.filter((lane) => lane.state === "running" || lane.state === "queued" || lane.state === "awaiting_input");
29
30
  accounts[id] = {
30
31
  enabled: account.enabled,
31
32
  logged_in: status.logged_in,
33
+ auth_state: status.auth_state,
32
34
  max_running: accountMaxRunning(providerConfig, id),
33
35
  running: running.filter((lane) => lane.account === id).length,
34
36
  queued: queued.filter((lane) => lane.account === id).length,
@@ -99,7 +101,9 @@ export function parseHostCapacitySnapshot(raw) {
99
101
  const account = objectValue(accountValue);
100
102
  if (id.trim() === "" || account === undefined ||
101
103
  typeof account.enabled !== "boolean" ||
102
- !(account.logged_in === null || typeof account.logged_in === "boolean")) {
104
+ !(account.logged_in === null || typeof account.logged_in === "boolean") ||
105
+ (account.auth_state !== undefined &&
106
+ !["file", "status", "absent", "unknown", "home_missing"].includes(account.auth_state))) {
103
107
  throw new LanesError("host_capacity_invalid", `Malformed ${provider} account capacity`, {
104
108
  provider,
105
109
  account: id,
@@ -108,6 +112,9 @@ export function parseHostCapacitySnapshot(raw) {
108
112
  accounts[id] = {
109
113
  enabled: account.enabled,
110
114
  logged_in: account.logged_in,
115
+ ...(account.auth_state !== undefined
116
+ ? { auth_state: account.auth_state }
117
+ : {}),
111
118
  max_running: requireCount(account.max_running, `${provider}.${id}.max_running`, true),
112
119
  running: requireCount(account.running, `${provider}.${id}.running`),
113
120
  queued: requireCount(account.queued, `${provider}.${id}.queued`),
@@ -38,6 +38,8 @@ export { LANE_MESH_EVENTS, WIRE_EVENT_KINDS, createLaneMesh, emitLaneDecisionEve
38
38
  export type { CreateLaneMeshOptions, LaneMesh, LaneMeshEvent, WireEventKind, WireThreadEvent, } from "./mesh-events.js";
39
39
  export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO, STARVATION_MAX_DEFERRALS, STARVATION_MAX_MS, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, parseMemAvailableKb, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
40
40
  export type { CpuTimes, ThrottleAdmitReason, ThrottleDecision, ThrottleDeferReason, ThrottleDeps, ThrottleState, } from "./throttle.js";
41
+ export { effectiveAccountHome, } from "./account-home.js";
42
+ export type { EffectiveAccountHome, } from "./account-home.js";
41
43
  export { AUTH_JSON_NAME, CLAUDE_CREDENTIALS_NAME, PROVIDER_CREDENTIAL_FILES, PROVIDER_HOME_ENV, accountEnv, accountStatus, authJsonPath, authStateFor, credentialPaths, hasCredentialFile, isCredentialProven, isLoggedOut, isOccupyingLaneState, lastStartedAt, listAccountStatuses, occupyingLaneCount, resolveAccount, runningLaneCount, selectBalancedAccount, } from "./accounts.js";
42
44
  export type { AccountResolveContext, AccountStatus, AuthState, ResolvedAccount, } from "./accounts.js";
43
45
  export { LIVE_WITHIN_MS, LIVENESS_EVENT_KINDS, SILENT_LOST_MS, TURN_START_LOST_MS, buildLivenessIndexFromSpool, classifySessionLiveness, defaultLivenessDeps, defaultSpoolPath, ingestSpoolLine, listSessionLiveness, livenessIndexPath, loadLivenessIndex, readDaemonPresenceConnected, readLivenessIndex, rebuildLivenessIndex, sessionLiveness, } from "./liveness.js";
@@ -21,6 +21,7 @@ export { SILENCE_MS, WATCHER_DECISION_EVENTS, WATCHER_OUT_MAX_BYTES, WATCHER_POL
21
21
  export { ENVELOPE_INVALID_REASON, decideEnvelopeRepair, emitMeshSoft, envelopeQuestionIds, readLatestEnvelopeFile, readLatestEnvelopeRecord, } from "./watcher.js";
22
22
  export { LANE_MESH_EVENTS, WIRE_EVENT_KINDS, createLaneMesh, emitLaneDecisionEvents, emitLaneProgressEvent, laneCanEmit, parseLaneMeshEvent, progressEventDue, toWireThreadEvent, } from "./mesh-events.js";
23
23
  export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO, STARVATION_MAX_DEFERRALS, STARVATION_MAX_MS, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, parseMemAvailableKb, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
24
+ export { effectiveAccountHome, } from "./account-home.js";
24
25
  export { AUTH_JSON_NAME, CLAUDE_CREDENTIALS_NAME, PROVIDER_CREDENTIAL_FILES, PROVIDER_HOME_ENV, accountEnv, accountStatus, authJsonPath, authStateFor, credentialPaths, hasCredentialFile, isCredentialProven, isLoggedOut, isOccupyingLaneState, lastStartedAt, listAccountStatuses, occupyingLaneCount, resolveAccount, runningLaneCount, selectBalancedAccount, } from "./accounts.js";
25
26
  export { LIVE_WITHIN_MS, LIVENESS_EVENT_KINDS, SILENT_LOST_MS, TURN_START_LOST_MS, buildLivenessIndexFromSpool, classifySessionLiveness, defaultLivenessDeps, defaultSpoolPath, ingestSpoolLine, listSessionLiveness, livenessIndexPath, loadLivenessIndex, readDaemonPresenceConnected, readLivenessIndex, rebuildLivenessIndex, sessionLiveness, } from "./liveness.js";
26
27
  export { DEAD_HOST_MS, MAX_AGED_ADVANCES, QUESTION_AGE_MS, RECONCILE_TIMER_NAME, RECONCILE_UNIT_NAME, wakeAnsweredLaneNow, agedAdvanceCount, currentHopStartedAtMs, isDeadlinePassed, laneRunsHere, readTimeoutDeadline, reconcileHeartbeatPath, resolveQuestionAudience, runReconcilePass, shouldAdvanceAudienceEnded, shouldAgeQuestion, timeoutPath, } from "./reconcile.js";
@@ -44,6 +44,7 @@ export interface LivenessDeps {
44
44
  readdirSync: (dir: string, options: {
45
45
  withFileTypes: true;
46
46
  }) => fs.Dirent[];
47
+ hostname?: () => string;
47
48
  }
48
49
  export declare function defaultLivenessDeps(): LivenessDeps;
49
50
  export declare function defaultSpoolPath(deps?: LivenessDeps): string;
@@ -14,6 +14,7 @@ import * as path from "node:path";
14
14
  import { workMeshSpoolPath, workMeshRoot } from "../mesh/live/paths.js";
15
15
  import { daemonDir, daemonStatePath } from "../mesh/live/daemon/paths.js";
16
16
  import { LanesError } from "./errors.js";
17
+ import { effectiveAccountHome } from "./account-home.js";
17
18
  import { DEFAULT_LIVE_WITHIN_MS, DEFAULT_SILENT_LOST_MS, DEFAULT_TURN_START_LOST_MS, loadLanesConfig, } from "./config.js";
18
19
  import { lanesStoreRoot } from "./store.js";
19
20
  export const LIVE_WITHIN_MS = DEFAULT_LIVE_WITHIN_MS;
@@ -39,6 +40,7 @@ export function defaultLivenessDeps() {
39
40
  statSync: (file) => fs.statSync(file),
40
41
  lstatSync: (file) => fs.lstatSync(file),
41
42
  readdirSync: fs.readdirSync.bind(fs),
43
+ hostname: () => os.hostname(),
42
44
  };
43
45
  }
44
46
  export function defaultSpoolPath(deps = defaultLivenessDeps()) {
@@ -373,7 +375,7 @@ export function sessionLiveness(hqRoot, sessionId, opts = {}) {
373
375
  const config = loadLanesConfig(hqRoot);
374
376
  const windows = opts.windows ?? config.liveness;
375
377
  if (!entry) {
376
- return unindexedClaudeSessionLiveness(sessionId, opts.nowMs ?? deps.now(), windows, deps, Object.values(config.providers.claude.accounts).map((account) => account.home));
378
+ return unindexedClaudeSessionLiveness(sessionId, opts.nowMs ?? deps.now(), windows, deps, Object.values(config.providers.claude.accounts).map((account) => effectiveAccountHome(account, deps.hostname?.()).home));
377
379
  }
378
380
  const presenceConnected = opts.presenceConnected ?? readDaemonPresenceConnected(deps);
379
381
  return classifySessionLiveness(entry, opts.nowMs ?? deps.now(), presenceConnected, windows);
@@ -10,6 +10,7 @@ import * as path from "node:path";
10
10
  import { parseUserYaml } from "../../utils/user-yaml-error.js";
11
11
  import { compareLaneQueueOrder, hasActiveAdmissionReservation, hasActivePendingResume, laneAdmissionCapReason, laneRunsOnHost, withAllLaneAdmissionLocks, } from "./admission.js";
12
12
  import { accountStatus, isCredentialProven, resolveAccount } from "./accounts.js";
13
+ import { effectiveAccountHome } from "./account-home.js";
13
14
  import { loadLanesConfig, providerQuotaDefaultHoldMs } from "./config.js";
14
15
  import { MAX_LANE_DEPTH } from "./constants.js";
15
16
  import { AccountResolutionError, LanesError, isLaneVersionConflict } from "./errors.js";
@@ -38,6 +39,13 @@ export const PICKUP_VALIDATION_MAX_AGE_MS = 15 * 60 * 1000;
38
39
  export const PICKUP_VALIDATION_TIMEOUT_REASON = "pickup_validation_timeout";
39
40
  export const RECONCILE_UNIT_NAME = "hq-lanes-reconcile.service";
40
41
  export const RECONCILE_TIMER_NAME = "hq-lanes-reconcile.timer";
42
+ function spawnDepsForHost(deps) {
43
+ return {
44
+ ...(deps.spawnLaneDeps ?? {}),
45
+ hostname: deps.host.hostname,
46
+ admissionHost: deps.host,
47
+ };
48
+ }
41
49
  function logSkip(skip) {
42
50
  process.stderr.write(`hq lanes reconcile: skipped ${skip.kind}${skip.id ? ` ${skip.id}` : ""}: ${skip.why}\n`);
43
51
  }
@@ -326,12 +334,16 @@ function availableBalancedAccount(hqRoot, lane, occupying, config, host, profile
326
334
  continue;
327
335
  if (config.providers[lane.provider].accounts[account]?.enabled !== true)
328
336
  continue;
329
- const status = accountStatus(lane.provider, account, config);
337
+ const status = accountStatus(lane.provider, account, config, {
338
+ hostname: host.hostname,
339
+ existsSync: (file) => fs.existsSync(file),
340
+ });
330
341
  if (!isCredentialProven(status))
331
342
  continue;
332
343
  try {
333
344
  resolveAccount(lane.provider, account, config, {
334
345
  hqRoot,
346
+ hostname: host.hostname,
335
347
  ...(pinnedModel !== undefined ? { model: pinnedModel } : {}),
336
348
  });
337
349
  }
@@ -655,7 +667,7 @@ export async function runPassBody(hqRoot, deps) {
655
667
  };
656
668
  const readSessionSenior = deps.readSessionSenior ??
657
669
  ((sessionId) => defaultReadSessionSenior(hqRoot, sessionId));
658
- const configuredClaudeHomes = Object.values(config.providers.claude.accounts).map((account) => account.home);
670
+ const configuredClaudeHomes = Object.values(config.providers.claude.accounts).map((account) => effectiveAccountHome(account, deps.host.hostname).home);
659
671
  const sessionStateCache = new Map();
660
672
  const sessionState = (sessionId) => {
661
673
  if (sessionStateCache.has(sessionId)) {
@@ -887,7 +899,7 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
887
899
  }
888
900
  if (!watcherAlive && !deps.dryRun) {
889
901
  try {
890
- armLaneWatcher(hqRoot, lane.lane_id, deps.spawnLaneDeps ?? {});
902
+ armLaneWatcher(hqRoot, lane.lane_id, spawnDepsForHost(deps));
891
903
  summary.heartbeats.push(`${lane.lane_id}:watcher-rearmed`);
892
904
  }
893
905
  catch (err) {
@@ -941,10 +953,10 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
941
953
  repairCandidate.envelope === null &&
942
954
  (!repairCandidate.missing || envelopeRepairMode(classified.provider) === "resume")) {
943
955
  const spawnRepair = deps.spawnRepair ??
944
- ((root, repairLane, input) => spawnEnvelopeRepair(root, repairLane, input, deps.spawnLaneDeps));
956
+ ((root, repairLane, input) => spawnEnvelopeRepair(root, repairLane, input, spawnDepsForHost(deps)));
945
957
  const repair = decideEnvelopeRepair(hqRoot, lane.lane_id, repairCandidate, nowMs, spawnRepair, () => undefined);
946
958
  if (repair.action === "spawned") {
947
- armLaneWatcher(hqRoot, lane.lane_id, deps.spawnLaneDeps);
959
+ armLaneWatcher(hqRoot, lane.lane_id, spawnDepsForHost(deps));
948
960
  summary.heartbeats.push(`${lane.lane_id}:envelope-repair`);
949
961
  continue;
950
962
  }
@@ -969,8 +981,7 @@ async function heartbeatAndDeadHosts(hqRoot, lanes, summary, deps, now) {
969
981
  if (!deps.dryRun && accountRetryReason && classified.account) {
970
982
  try {
971
983
  const recovered = await retryLaneOnAlternateAccount(hqRoot, classified, accountRetryReason, "reconcile", {
972
- ...(deps.spawnLaneDeps ?? {}),
973
- admissionHost: deps.host,
984
+ ...spawnDepsForHost(deps),
974
985
  });
975
986
  summary.dead_host.push({
976
987
  id: lane.lane_id,
@@ -1197,7 +1208,7 @@ async function admitQueued(hqRoot, lanes, summary, deps, now) {
1197
1208
  nowMs,
1198
1209
  });
1199
1210
  const spawnFn = deps.spawn ??
1200
- ((root, lane) => spawnLaneWorker(root, lane, deps.spawnLaneDeps));
1211
+ ((root, lane) => spawnLaneWorker(root, lane, spawnDepsForHost(deps)));
1201
1212
  const running = lanes.filter((lane) => (lane.state === "running" ||
1202
1213
  hasFreshAdmissionReservation(lane, now.getTime()) ||
1203
1214
  hasActivePendingResume(lane, now.getTime())) &&
@@ -1336,7 +1347,7 @@ async function resumeAwaiting(hqRoot, lanes, questions, summary, deps, onlyLaneI
1336
1347
  const throttle = await decideAdmission(hqRoot, config.throttle, throttleDeps);
1337
1348
  throttleDecision = throttle.decision;
1338
1349
  }
1339
- const spawnFn = deps.spawn ?? ((root, lane) => spawnLaneWorker(root, lane, deps.spawnLaneDeps));
1350
+ const spawnFn = deps.spawn ?? ((root, lane) => spawnLaneWorker(root, lane, spawnDepsForHost(deps)));
1340
1351
  const nowMs = deps.now().getTime();
1341
1352
  const fallbackAnswerAt = new Date(nowMs).toISOString();
1342
1353
  const answerAtByLane = new Map();
@@ -68,6 +68,7 @@ export type ProveDetachedFn = (pid: number) => DetachProof;
68
68
  export type KillProcessFn = (pid: number, signal?: NodeJS.Signals) => void;
69
69
  export interface SpawnLaneDeps {
70
70
  env?: NodeJS.ProcessEnv;
71
+ hostname?: string;
71
72
  /** The caller's host scope for admission checks; reconcile supplies its resolved host. */
72
73
  admissionHost?: AdmissionHost;
73
74
  /** Provider model catalog dependencies, shared with create in tests. */