@indigoai-us/hq-cli 5.345.123 → 5.345.125

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.
Files changed (44) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/assets/scaffold/core/scripts/work-mesh-project-registration-offer.sh +77 -0
  3. package/assets/scaffold/core/scripts/workflow-gate.sh +247 -0
  4. package/dist/command-catalog.generated.d.ts +239 -0
  5. package/dist/command-catalog.generated.js +291 -0
  6. package/dist/commands/core-bucket-c-work-mesh-offer-native.d.ts +7 -0
  7. package/dist/commands/core-bucket-c-work-mesh-offer-native.js +147 -0
  8. package/dist/commands/core-bucket-c-workflow-gate-fast.d.ts +3 -0
  9. package/dist/commands/core-bucket-c-workflow-gate-fast.js +17 -0
  10. package/dist/commands/core-bucket-c-workflow-gate-native.d.ts +7 -0
  11. package/dist/commands/core-bucket-c-workflow-gate-native.js +250 -0
  12. package/dist/commands/core.js +12 -0
  13. package/dist/commands/lanes-accounts.js +22 -3
  14. package/dist/commands/lanes.js +25 -32
  15. package/dist/commands/mesh.js +457 -16
  16. package/dist/fast-scaffold.js +26 -0
  17. package/dist/lib/lanes/accounts.d.ts +48 -2
  18. package/dist/lib/lanes/accounts.js +160 -9
  19. package/dist/lib/lanes/index.d.ts +2 -2
  20. package/dist/lib/lanes/index.js +1 -1
  21. package/dist/lib/lanes/message-replay.js +6 -1
  22. package/dist/lib/lanes/provider-rejection.d.ts +2 -6
  23. package/dist/lib/lanes/provider-rejection.js +19 -0
  24. package/dist/lib/lanes/quota-notice.d.ts +24 -0
  25. package/dist/lib/lanes/quota-notice.js +61 -0
  26. package/dist/lib/lanes/quota.d.ts +55 -15
  27. package/dist/lib/lanes/quota.js +231 -49
  28. package/dist/lib/lanes/reconcile.js +17 -1
  29. package/dist/lib/lanes/session-relink.d.ts +2 -0
  30. package/dist/lib/lanes/session-relink.js +98 -4
  31. package/dist/lib/lanes/spawn.d.ts +1 -1
  32. package/dist/lib/lanes/spawn.js +58 -12
  33. package/dist/lib/lanes/types.d.ts +5 -0
  34. package/dist/lib/lanes/types.js +1 -1
  35. package/dist/lib/lanes/watcher.js +13 -1
  36. package/dist/lib/mesh/api.d.ts +2 -0
  37. package/dist/lib/mesh/api.js +3 -0
  38. package/dist/lib/mesh/client.d.ts +6 -1
  39. package/dist/lib/mesh/client.js +18 -5
  40. package/dist/lib/mesh/team-coordination.d.ts +106 -0
  41. package/dist/lib/mesh/team-coordination.js +297 -0
  42. package/dist/lib/work-context/auto-bind.d.ts +75 -0
  43. package/dist/lib/work-context/auto-bind.js +245 -0
  44. package/package.json +1 -1
@@ -10,6 +10,45 @@
10
10
  import { type LaneRecord, type LaneProvider } from "./types.js";
11
11
  import type { LanesConfig } from "./types.js";
12
12
  export declare const PROVIDER_HOME_ENV: Record<LaneProvider, string>;
13
+ /**
14
+ * Claude Code keys its OS keychain credential by `CLAUDE_CONFIG_DIR`: with
15
+ * the variable unset it reads the plain `Claude Code-credentials` item, and
16
+ * with it set, even to the default `~/.claude`, it reads a hash-suffixed item
17
+ * for that path. Setting the variable for the default home therefore makes a
18
+ * lane use a different login than the interactive session (incident
19
+ * 2026-10-13). A Claude account whose home is the default is launched with
20
+ * the variable unset instead.
21
+ */
22
+ export declare function isDefaultProviderHome(provider: LaneProvider, home: string, homedir?: () => string): boolean;
23
+ /**
24
+ * Return a copy of `base` with the provider home variable set to `home`,
25
+ * or, for a Claude account on the default home, with the variable removed
26
+ * so the worker and the interactive session share one credential.
27
+ */
28
+ export declare function withProviderHomeEnv(base: NodeJS.ProcessEnv, provider: LaneProvider, home: string, homedir?: () => string): NodeJS.ProcessEnv;
29
+ /** Who `claude auth status` says a home is logged in as. Never a token. */
30
+ export interface ClaudeIdentity {
31
+ email: string | null;
32
+ org_id: string | null;
33
+ org_name: string | null;
34
+ subscription_type: string | null;
35
+ }
36
+ /**
37
+ * Email and org from a `claude auth status` JSON payload. Only identity
38
+ * fields are kept; everything else in the payload is dropped.
39
+ */
40
+ export declare function claudeIdentityFromProbe(result: AccountStatusProbeResult): ClaudeIdentity | null;
41
+ /** "me@example.com (Acme)" label for hold and failover messages. */
42
+ export declare function describeClaudeIdentity(identity: ClaudeIdentity | undefined | null): string | null;
43
+ /**
44
+ * The identity the interactive session uses: `claude auth status` with no
45
+ * `CLAUDE_CONFIG_DIR`. Null when the probe fails or names nobody.
46
+ */
47
+ export declare function probeInteractiveClaudeIdentity(probe: NonNullable<AccountDeps["probe"]>, base?: NodeJS.ProcessEnv): ClaudeIdentity | null;
48
+ /** True when both identities are known and name different logins; null when unknown. */
49
+ export declare function claudeIdentityDiffers(a: ClaudeIdentity | undefined | null, b: ClaudeIdentity | undefined | null): boolean | null;
50
+ /** The operator command that lifts a recorded quota hold. */
51
+ export declare function clearQuotaCommand(provider: LaneProvider, accountId: string): string;
13
52
  export declare const AUTH_JSON_NAME = "auth.json";
14
53
  export declare const CLAUDE_CREDENTIALS_NAME = ".credentials.json";
15
54
  /**
@@ -28,6 +67,7 @@ export interface AccountStatusProbeResult {
28
67
  export interface AccountDeps {
29
68
  existsSync: (p: string) => boolean;
30
69
  hostname?: string;
70
+ homedir?: () => string;
31
71
  /** Provider status output is parsed for a boolean only; it is never printed. */
32
72
  probe?: (command: string, args: string[], env: NodeJS.ProcessEnv) => AccountStatusProbeResult;
33
73
  env?: NodeJS.ProcessEnv;
@@ -64,6 +104,8 @@ export interface AccountStatus {
64
104
  id: string;
65
105
  home: string;
66
106
  home_source?: "host" | "default";
107
+ /** Email and org from the provider status command, when it was consulted. */
108
+ identity?: ClaudeIdentity;
67
109
  enabled: boolean;
68
110
  disabled_reason?: string;
69
111
  /** Present in personal/lanes/config.json. */
@@ -112,6 +154,10 @@ export declare function resolveAccount(provider: LaneProvider, requestedId: stri
112
154
  export declare function authJsonPath(home: string): string;
113
155
  export declare function accountStatus(provider: LaneProvider, accountId: string, config: LanesConfig, deps?: AccountDeps): AccountStatus;
114
156
  export declare function listAccountStatuses(provider: LaneProvider, config: LanesConfig, deps?: AccountDeps): AccountStatus[];
115
- /** Environment assignment the worker process should receive. */
116
- export declare function accountEnv(resolved: ResolvedAccount): Record<string, string>;
157
+ /**
158
+ * Environment assignment the worker process should receive. Empty for a
159
+ * Claude account on the default home, which runs with the variable unset
160
+ * (see isDefaultProviderHome).
161
+ */
162
+ export declare function accountEnv(resolved: ResolvedAccount, homedir?: () => string): Record<string, string>;
117
163
  //# sourceMappingURL=accounts.d.ts.map
@@ -8,11 +8,12 @@
8
8
  * module never reads, prints, copies, or hashes credential files.
9
9
  */
10
10
  import * as fs from "node:fs";
11
+ import * as os from "node:os";
11
12
  import { effectiveAccountHome } from "./account-home.js";
12
13
  import * as path from "node:path";
13
14
  import { AccountResolutionError } from "./errors.js";
14
15
  import { isTerminalLaneState } from "./types.js";
15
- import { accountMaxRunning } from "./config.js";
16
+ import { DEFAULT_ACCOUNT_HOMES, accountMaxRunning, canonicalizeHome } from "./config.js";
16
17
  import { listLaneIds, readLane } from "./store.js";
17
18
  import { loadQuotaState, quotaBlockFor } from "./quota.js";
18
19
  export const PROVIDER_HOME_ENV = {
@@ -20,6 +21,112 @@ export const PROVIDER_HOME_ENV = {
20
21
  codex: "CODEX_HOME",
21
22
  grok: "GROK_HOME",
22
23
  };
24
+ /**
25
+ * Claude Code keys its OS keychain credential by `CLAUDE_CONFIG_DIR`: with
26
+ * the variable unset it reads the plain `Claude Code-credentials` item, and
27
+ * with it set, even to the default `~/.claude`, it reads a hash-suffixed item
28
+ * for that path. Setting the variable for the default home therefore makes a
29
+ * lane use a different login than the interactive session (incident
30
+ * 2026-10-13). A Claude account whose home is the default is launched with
31
+ * the variable unset instead.
32
+ */
33
+ export function isDefaultProviderHome(provider, home, homedir = () => os.homedir()) {
34
+ const deps = {
35
+ homedir,
36
+ realpathSync: (p) => fs.realpathSync(p),
37
+ existsSync: (p) => fs.existsSync(p),
38
+ };
39
+ try {
40
+ return canonicalizeHome(home, deps) === canonicalizeHome(DEFAULT_ACCOUNT_HOMES[provider], deps);
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ /**
47
+ * Return a copy of `base` with the provider home variable set to `home`,
48
+ * or, for a Claude account on the default home, with the variable removed
49
+ * so the worker and the interactive session share one credential.
50
+ */
51
+ export function withProviderHomeEnv(base, provider, home, homedir) {
52
+ const next = { ...base };
53
+ const envVar = PROVIDER_HOME_ENV[provider];
54
+ if (provider === "claude" && isDefaultProviderHome(provider, home, homedir)) {
55
+ delete next[envVar];
56
+ }
57
+ else {
58
+ next[envVar] = home;
59
+ }
60
+ return next;
61
+ }
62
+ function optionalString(value) {
63
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
64
+ }
65
+ /**
66
+ * Email and org from a `claude auth status` JSON payload. Only identity
67
+ * fields are kept; everything else in the payload is dropped.
68
+ */
69
+ export function claudeIdentityFromProbe(result) {
70
+ const text = result.stdout.trim();
71
+ if (!text.startsWith("{"))
72
+ return null;
73
+ let parsed;
74
+ try {
75
+ parsed = JSON.parse(text);
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
81
+ return null;
82
+ const rec = parsed;
83
+ const identity = {
84
+ email: optionalString(rec.email),
85
+ org_id: optionalString(rec.orgId),
86
+ org_name: optionalString(rec.orgName),
87
+ subscription_type: optionalString(rec.subscriptionType),
88
+ };
89
+ if (!identity.email && !identity.org_id && !identity.org_name)
90
+ return null;
91
+ return identity;
92
+ }
93
+ /** "me@example.com (Acme)" label for hold and failover messages. */
94
+ export function describeClaudeIdentity(identity) {
95
+ if (!identity)
96
+ return null;
97
+ const org = identity.org_name ?? identity.org_id;
98
+ if (identity.email && org)
99
+ return `${identity.email} (${org})`;
100
+ return identity.email ?? org;
101
+ }
102
+ /**
103
+ * The identity the interactive session uses: `claude auth status` with no
104
+ * `CLAUDE_CONFIG_DIR`. Null when the probe fails or names nobody.
105
+ */
106
+ export function probeInteractiveClaudeIdentity(probe, base = process.env) {
107
+ const bare = { ...base };
108
+ delete bare[PROVIDER_HOME_ENV.claude];
109
+ try {
110
+ return claudeIdentityFromProbe(probe("claude", ["auth", "status"], bare));
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ /** True when both identities are known and name different logins; null when unknown. */
117
+ export function claudeIdentityDiffers(a, b) {
118
+ if (!a || !b)
119
+ return null;
120
+ if (a.email && b.email)
121
+ return a.email.toLowerCase() !== b.email.toLowerCase();
122
+ if (a.org_id && b.org_id)
123
+ return a.org_id !== b.org_id;
124
+ return null;
125
+ }
126
+ /** The operator command that lifts a recorded quota hold. */
127
+ export function clearQuotaCommand(provider, accountId) {
128
+ return `hq lanes accounts clear-quota ${provider} ${accountId}`;
129
+ }
23
130
  export const AUTH_JSON_NAME = "auth.json";
24
131
  export const CLAUDE_CREDENTIALS_NAME = ".credentials.json";
25
132
  /**
@@ -161,23 +268,34 @@ function modelForAccount(provider, accountId, config, ctx) {
161
268
  return providerModel.trim();
162
269
  return undefined;
163
270
  }
164
- function throwIfQuotaBlocked(block, provider, accountId) {
271
+ /** "account "a" (me@example.com (Acme))" when the identity is known. */
272
+ function accountLabel(accountId, identity) {
273
+ const who = describeClaudeIdentity(identity);
274
+ return `Account ${JSON.stringify(accountId)}${who ? ` (${who})` : ""}`;
275
+ }
276
+ function throwIfQuotaBlocked(block, provider, accountId, identity) {
165
277
  if (!block.blocked || !block.reason)
166
278
  return;
167
279
  const model = block.model ?? "(no model)";
280
+ const label = accountLabel(accountId, identity);
281
+ const identityFields = identity ? { identity } : {};
168
282
  if (block.reason === "account_model_unavailable") {
169
- throw new AccountResolutionError("account_model_unavailable", `Account ${JSON.stringify(accountId)} cannot run model ${model}: ${block.marker ?? "needs credits"}`, {
283
+ throw new AccountResolutionError("account_model_unavailable", `${label} cannot run model ${model}: ${block.marker ?? "needs credits"}`, {
170
284
  provider,
171
285
  account: accountId,
172
286
  model: block.model ?? null,
173
287
  marker: block.marker ?? null,
288
+ ...identityFields,
174
289
  });
175
290
  }
176
- throw new AccountResolutionError("account_quota_exhausted", `Account ${JSON.stringify(accountId)} is out of quota${block.quota_reset_at ? ` until ${block.quota_reset_at}` : ""} for model ${model}`, {
291
+ const clear = clearQuotaCommand(provider, accountId);
292
+ throw new AccountResolutionError("account_quota_exhausted", `${label} is out of quota${block.quota_reset_at ? ` until ${block.quota_reset_at}` : ""} for model ${model}. Clear the hold with: ${clear}`, {
177
293
  provider,
178
294
  account: accountId,
179
295
  model: block.model ?? null,
180
296
  quota_reset_at: block.quota_reset_at ?? null,
297
+ clear_command: clear,
298
+ ...identityFields,
181
299
  });
182
300
  }
183
301
  function boundAccount(provider, id, account, hostname) {
@@ -205,15 +323,19 @@ export function selectBalancedAccount(provider, config, ctx = {}) {
205
323
  throw new AccountResolutionError("no_enabled_account", `No enabled account for provider ${provider}`, { provider, ineligible: "none_enabled" });
206
324
  }
207
325
  const proven = [];
326
+ const identities = new Map();
208
327
  for (const id of enabled) {
209
328
  const status = accountStatus(provider, id, config, {
210
329
  existsSync,
211
330
  ...(ctx.accountDeps?.probe ? { probe: ctx.accountDeps.probe } : {}),
212
331
  ...(ctx.accountDeps?.env ? { env: ctx.accountDeps.env } : {}),
332
+ ...(ctx.accountDeps?.homedir ? { homedir: ctx.accountDeps.homedir } : {}),
213
333
  ...(ctx.hostname !== undefined ? { hostname: ctx.hostname } : {}),
214
334
  });
215
335
  if (status.logged_in === true)
216
336
  proven.push(id);
337
+ if (status.identity)
338
+ identities.set(id, status.identity);
217
339
  }
218
340
  if (proven.length === 0) {
219
341
  throw new AccountResolutionError("no_enabled_account", `No logged-in account for provider ${provider}`, { provider, ineligible: "none_logged_in", enabled });
@@ -247,17 +369,25 @@ export function selectBalancedAccount(provider, config, ctx = {}) {
247
369
  account: id,
248
370
  reason: block.reason,
249
371
  quota_reset_at: block.quota_reset_at ?? null,
372
+ identity: identities.get(id) ?? null,
373
+ clear_command: clearQuotaCommand(provider, id),
250
374
  }));
251
- throw new AccountResolutionError("account_quota_exhausted", `All logged-in accounts for ${provider} are quota-limited for the selected model`, {
375
+ const held = blocked
376
+ .filter(({ block }) => block.reason === "account_quota_exhausted")
377
+ .map(({ id, block }) => `${accountLabel(id, identities.get(id))}${block.quota_reset_at ? ` until ${block.quota_reset_at}` : ""}; clear with: ${clearQuotaCommand(provider, id)}`)
378
+ .join("; ");
379
+ throw new AccountResolutionError("account_quota_exhausted", `All logged-in accounts for ${provider} are quota-limited for the selected model. ${held}`, {
252
380
  provider,
253
381
  account: earliest.id,
254
382
  model: earliest.block.model ?? null,
255
383
  quota_reset_at: resetTimes[0] ?? null,
256
384
  account_reasons: resetsByAccount,
385
+ ...(identities.get(earliest.id) ? { identity: identities.get(earliest.id) } : {}),
386
+ clear_command: clearQuotaCommand(provider, earliest.id),
257
387
  });
258
388
  }
259
389
  const sample = proven[0];
260
- throwIfQuotaBlocked(blocked[0].block, provider, sample);
390
+ throwIfQuotaBlocked(blocked[0].block, provider, sample, identities.get(sample));
261
391
  }
262
392
  const underCap = runnable.filter((id) => {
263
393
  const cap = accountMaxRunning(providerCfg, id);
@@ -298,6 +428,16 @@ export function resolveAccount(provider, requestedId, config, ctx = {}) {
298
428
  if (ctx.hqRoot !== undefined) {
299
429
  const now = ctx.now ?? new Date();
300
430
  const block = quotaBlockFor(loadQuotaState(ctx.hqRoot), provider, requestedId, modelForAccount(provider, requestedId, config, ctx), now);
431
+ if (block.blocked && provider === "claude" && ctx.accountDeps?.probe) {
432
+ const status = accountStatus(provider, requestedId, config, {
433
+ existsSync: ctx.existsSync ?? ((p) => fs.existsSync(p)),
434
+ probe: ctx.accountDeps.probe,
435
+ ...(ctx.accountDeps.env ? { env: ctx.accountDeps.env } : {}),
436
+ ...(ctx.accountDeps.homedir ? { homedir: ctx.accountDeps.homedir } : {}),
437
+ ...(ctx.hostname !== undefined ? { hostname: ctx.hostname } : {}),
438
+ });
439
+ throwIfQuotaBlocked(block, provider, requestedId, status.identity);
440
+ }
301
441
  throwIfQuotaBlocked(block, provider, requestedId);
302
442
  }
303
443
  return boundAccount(provider, requestedId, account, ctx.hostname);
@@ -328,14 +468,16 @@ export function accountStatus(provider, accountId, config, deps = defaultAccount
328
468
  const present = homeExists && hasCredentialFile(provider, effective.home, deps.existsSync);
329
469
  let auth_state = homeExists ? authStateFor(provider, present) : "home_missing";
330
470
  let logged_in = homeExists ? loggedInFromAuthState(auth_state) : false;
471
+ let identity;
331
472
  if (homeExists && !present && provider === "claude" && deps.probe !== undefined) {
332
473
  try {
333
- const result = deps.probe("claude", ["auth", "status"], { ...(deps.env ?? process.env), CLAUDE_CONFIG_DIR: effective.home });
474
+ const result = deps.probe("claude", ["auth", "status"], withProviderHomeEnv(deps.env ?? process.env, "claude", effective.home, deps.homedir));
334
475
  const providerStatus = claudeLoginStatus(result);
335
476
  if (providerStatus !== null) {
336
477
  auth_state = "status";
337
478
  logged_in = providerStatus;
338
479
  }
480
+ identity = claudeIdentityFromProbe(result) ?? undefined;
339
481
  }
340
482
  catch {
341
483
  process.stderr.write(`hq lanes: Claude auth status probe failed for account ${accountId}\n`);
@@ -352,6 +494,8 @@ export function accountStatus(provider, accountId, config, deps = defaultAccount
352
494
  auth_state,
353
495
  envVar,
354
496
  };
497
+ if (identity)
498
+ status.identity = identity;
355
499
  if (account.disabled_reason !== undefined) {
356
500
  status.disabled_reason = account.disabled_reason;
357
501
  }
@@ -381,8 +525,15 @@ function claudeLoginStatus(result) {
381
525
  return false;
382
526
  return null;
383
527
  }
384
- /** Environment assignment the worker process should receive. */
385
- export function accountEnv(resolved) {
528
+ /**
529
+ * Environment assignment the worker process should receive. Empty for a
530
+ * Claude account on the default home, which runs with the variable unset
531
+ * (see isDefaultProviderHome).
532
+ */
533
+ export function accountEnv(resolved, homedir) {
534
+ if (resolved.provider === "claude" && isDefaultProviderHome("claude", resolved.home, homedir)) {
535
+ return {};
536
+ }
386
537
  return { [resolved.envVar]: resolved.home };
387
538
  }
388
539
  //# sourceMappingURL=accounts.js.map
@@ -40,8 +40,8 @@ export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO,
40
40
  export type { CpuTimes, ThrottleAdmitReason, ThrottleDecision, ThrottleDeferReason, ThrottleDeps, ThrottleState, } from "./throttle.js";
41
41
  export { effectiveAccountHome, } from "./account-home.js";
42
42
  export type { EffectiveAccountHome, } from "./account-home.js";
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";
44
- export type { AccountResolveContext, AccountStatus, AuthState, ResolvedAccount, } from "./accounts.js";
43
+ export { AUTH_JSON_NAME, CLAUDE_CREDENTIALS_NAME, PROVIDER_CREDENTIAL_FILES, PROVIDER_HOME_ENV, accountEnv, accountStatus, authJsonPath, authStateFor, claudeIdentityDiffers, claudeIdentityFromProbe, clearQuotaCommand, credentialPaths, describeClaudeIdentity, hasCredentialFile, isCredentialProven, isDefaultProviderHome, isLoggedOut, probeInteractiveClaudeIdentity, withProviderHomeEnv, isOccupyingLaneState, lastStartedAt, listAccountStatuses, occupyingLaneCount, resolveAccount, runningLaneCount, selectBalancedAccount, } from "./accounts.js";
44
+ export type { AccountResolveContext, AccountStatus, AuthState, ClaudeIdentity, ResolvedAccount, } from "./accounts.js";
45
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";
46
46
  export type { LivenessDeps, LivenessEventKind, LivenessIndex, SessionIndexEntry, } from "./liveness.js";
47
47
  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";
@@ -22,7 +22,7 @@ export { ENVELOPE_INVALID_REASON, decideEnvelopeRepair, emitMeshSoft, envelopeQu
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, VM_STAT_BIN, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, parseMemAvailableKb, parseVmStatAvailableMb, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
24
24
  export { effectiveAccountHome, } from "./account-home.js";
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
+ export { AUTH_JSON_NAME, CLAUDE_CREDENTIALS_NAME, PROVIDER_CREDENTIAL_FILES, PROVIDER_HOME_ENV, accountEnv, accountStatus, authJsonPath, authStateFor, claudeIdentityDiffers, claudeIdentityFromProbe, clearQuotaCommand, credentialPaths, describeClaudeIdentity, hasCredentialFile, isCredentialProven, isDefaultProviderHome, isLoggedOut, probeInteractiveClaudeIdentity, withProviderHomeEnv, isOccupyingLaneState, lastStartedAt, listAccountStatuses, occupyingLaneCount, resolveAccount, runningLaneCount, selectBalancedAccount, } from "./accounts.js";
26
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";
27
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";
28
28
  export { RECONCILE_TIMER_INTERVAL, defaultSystemdUserDir, installReconcileTimer, providerPathEnv, renderReconcileService, renderReconcileTimer, uninstallReconcileTimer, } from "./reconcile-install.js";
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
+ import * as os from "node:os";
3
4
  import * as path from "node:path";
4
5
  import { probeProviderCompactionsSince, providerTranscriptSize, } from "./compaction.js";
5
6
  import { LanesError } from "./errors.js";
@@ -55,7 +56,11 @@ export function writeMessageReplayState(filePath, state) {
55
56
  function safeProviderTranscriptPath(transcriptPath, provider, env) {
56
57
  if (provider === "grok")
57
58
  return transcriptPath;
58
- const home = provider === "claude" ? env.CLAUDE_CONFIG_DIR : env.CODEX_HOME;
59
+ // A Claude worker on the default home runs with CLAUDE_CONFIG_DIR unset
60
+ // (accounts.ts isDefaultProviderHome); its transcripts live under ~/.claude.
61
+ const home = provider === "claude"
62
+ ? env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude")
63
+ : env.CODEX_HOME;
59
64
  if (!home) {
60
65
  throw new LanesError("message_replay_provider_home_missing", `Cannot validate ${provider} transcript without its provider home`);
61
66
  }
@@ -38,6 +38,8 @@ export interface ProviderRejection {
38
38
  event: "codex.turn.failed" | "claude.result.is_error" | "claude.result.login_failure" | "grok.error";
39
39
  /** Filesystem time when the provider output was appended, when read from worker.out. */
40
40
  output_at?: string;
41
+ /** The provider's structured reset (`resetsAt`), when the result carried one. */
42
+ resets_at?: unknown;
41
43
  }
42
44
  export declare function isProviderLoginFailure(failure: ProviderRejection | null): boolean;
43
45
  /**
@@ -56,12 +58,6 @@ export declare function retryableFromStatus(status: number | null): boolean;
56
58
  * read when it parses; otherwise the outer text is the message.
57
59
  */
58
60
  export declare function parseCodexTurnFailed(text: string): ProviderRejection | null;
59
- /**
60
- * Claude `-p --output-format json`: the last `type: "result"` object with
61
- * `is_error: true` and a numeric `api_error_status`, or the CLI's explicit
62
- * unauthenticated message. Other status-less errors keep the existing
63
- * classification because they may be quota text or a worker failure.
64
- */
65
61
  export declare function parseClaudeResultRejection(text: string): ProviderRejection | null;
66
62
  /**
67
63
  * Grok `--output-format json` prints one object. Measured 2026-09-22 (lane
@@ -166,6 +166,23 @@ export function parseCodexTurnFailed(text) {
166
166
  * unauthenticated message. Other status-less errors keep the existing
167
167
  * classification because they may be quota text or a worker failure.
168
168
  */
169
+ function isRecord(value) {
170
+ return value !== null && typeof value === "object" && !Array.isArray(value);
171
+ }
172
+ /** `resetsAt` on the result, or under `rate_limit_info` (directly or in the API error params). */
173
+ function claudeStructuredReset(rec) {
174
+ const direct = rec.resets_at ?? rec.resetsAt;
175
+ if (direct !== undefined)
176
+ return direct;
177
+ for (const holder of [rec, rec.api_error_params, rec.apiErrorParams]) {
178
+ if (!isRecord(holder))
179
+ continue;
180
+ const info = holder.rate_limit_info ?? holder.rateLimitInfo;
181
+ if (isRecord(info) && info.resetsAt !== undefined)
182
+ return info.resetsAt;
183
+ }
184
+ return undefined;
185
+ }
169
186
  export function parseClaudeResultRejection(text) {
170
187
  // The last `result` decides: a success or a status-less error there is
171
188
  // final (NOT_REJECTED), so the scan does not continue to earlier results.
@@ -182,6 +199,7 @@ export function parseClaudeResultRejection(text) {
182
199
  if (status === null && !loginFailure && !retryableProviderFailure(status, message)) {
183
200
  return NOT_REJECTED;
184
201
  }
202
+ const resetsAt = claudeStructuredReset(rec);
185
203
  return {
186
204
  reason: PROVIDER_REJECTED_REASON,
187
205
  provider: "claude",
@@ -192,6 +210,7 @@ export function parseClaudeResultRejection(text) {
192
210
  event: loginFailure && rec.is_error !== true
193
211
  ? "claude.result.login_failure"
194
212
  : "claude.result.is_error",
213
+ ...(resetsAt !== undefined ? { resets_at: resetsAt } : {}),
195
214
  };
196
215
  });
197
216
  return found === NOT_REJECTED ? null : found;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Tell a lane's senior when a provider 429 put an account on a quota hold.
3
+ *
4
+ * An account-wide hold stops every lane on that account, so the senior needs
5
+ * to see it with the provider's own words and the command that lifts it. One
6
+ * message per (provider, account, hold), so several lanes failing on the same
7
+ * hold produce one notice.
8
+ */
9
+ import type { QuotaState } from "./quota.js";
10
+ import { type LaneProvider, type LaneRecord } from "./types.js";
11
+ export interface QuotaHoldNotice {
12
+ message_id: string;
13
+ text: string;
14
+ }
15
+ /** Build the notice for the account's current hold, or null when there is none. */
16
+ export declare function quotaHoldNotice(state: QuotaState, provider: LaneProvider, accountId: string, laneId: string): QuotaHoldNotice | null;
17
+ /**
18
+ * Deliver the notice to the lane's senior inbox. A session senior gets it in
19
+ * its monitor-session inbox, a lane senior in that lane's dropbox. The notice
20
+ * is always written to stderr as well, so a senior with no inbox still has it
21
+ * in the watcher log. Returns where it was delivered.
22
+ */
23
+ export declare function deliverQuotaHoldNotice(hqRoot: string, lane: Pick<LaneRecord, "lane_id" | "senior">, notice: QuotaHoldNotice, at: string): "session_inbox" | "lane_inbox" | "stderr_only";
24
+ //# sourceMappingURL=quota-notice.d.ts.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Tell a lane's senior when a provider 429 put an account on a quota hold.
3
+ *
4
+ * An account-wide hold stops every lane on that account, so the senior needs
5
+ * to see it with the provider's own words and the command that lifts it. One
6
+ * message per (provider, account, hold), so several lanes failing on the same
7
+ * hold produce one notice.
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import { monitorSessionDir } from "../monitor.js";
12
+ import { appendDropboxMessageOnce, appendDropboxMessageOnceAtPath } from "./dropbox.js";
13
+ import { LANE_PROVIDERS } from "./types.js";
14
+ /** Build the notice for the account's current hold, or null when there is none. */
15
+ export function quotaHoldNotice(state, provider, accountId, laneId) {
16
+ const account = state.providers[provider]?.[accountId];
17
+ const holdUntil = account?.all_models_quota_reset_at;
18
+ if (!account || !holdUntil)
19
+ return null;
20
+ const said = account.provider_text ? `Provider said: "${account.provider_text}".` : "Provider gave no message text.";
21
+ const clear = `If the account is not actually exhausted (check /usage), clear the hold with: hq lanes accounts clear-quota ${provider} ${accountId}`;
22
+ const lines = [
23
+ `Quota hold: ${provider} account ${accountId} is held until ${holdUntil}; no lane will run on it until then. Set by lane ${laneId}.`,
24
+ said,
25
+ ];
26
+ if (account.reset_source === "unconfirmed" && account.unconfirmed_reset_at) {
27
+ lines.push(`The provider named a reset at ${account.unconfirmed_reset_at}. That is more than a day away, so this is a short hold; the account gets the full hold only if the next lane after it also gets a 429.`);
28
+ }
29
+ else if (account.reset_source === "derived") {
30
+ lines.push("The provider named no reset time, so this is the default hold.");
31
+ }
32
+ lines.push(clear);
33
+ return {
34
+ message_id: `quota-hold:${provider}:${accountId}:${holdUntil}`,
35
+ text: lines.join("\n"),
36
+ };
37
+ }
38
+ /**
39
+ * Deliver the notice to the lane's senior inbox. A session senior gets it in
40
+ * its monitor-session inbox, a lane senior in that lane's dropbox. The notice
41
+ * is always written to stderr as well, so a senior with no inbox still has it
42
+ * in the watcher log. Returns where it was delivered.
43
+ */
44
+ export function deliverQuotaHoldNotice(hqRoot, lane, notice, at) {
45
+ process.stderr.write(`hq lanes: ${notice.text.replace(/\n/g, " ")}\n`);
46
+ const message = { at, message_id: notice.message_id, kind: "quota_hold", text: notice.text };
47
+ if (lane.senior.kind === "lane" && lane.senior.id) {
48
+ appendDropboxMessageOnce(hqRoot, lane.senior.id, message);
49
+ return "lane_inbox";
50
+ }
51
+ if (lane.senior.kind === "session" && lane.senior.id) {
52
+ const sessionId = lane.senior.id;
53
+ const dirs = LANE_PROVIDERS.map((p) => monitorSessionDir(hqRoot, p, sessionId)).filter((dir) => fs.existsSync(dir));
54
+ if (dirs.length === 1) {
55
+ appendDropboxMessageOnceAtPath(path.join(dirs[0], "dropbox"), message);
56
+ return "session_inbox";
57
+ }
58
+ }
59
+ return "stderr_only";
60
+ }
61
+ //# sourceMappingURL=quota-notice.js.map