@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.
- package/CHANGELOG.md +39 -0
- package/assets/scaffold/core/scripts/work-mesh-project-registration-offer.sh +77 -0
- package/assets/scaffold/core/scripts/workflow-gate.sh +247 -0
- package/dist/command-catalog.generated.d.ts +239 -0
- package/dist/command-catalog.generated.js +291 -0
- package/dist/commands/core-bucket-c-work-mesh-offer-native.d.ts +7 -0
- package/dist/commands/core-bucket-c-work-mesh-offer-native.js +147 -0
- package/dist/commands/core-bucket-c-workflow-gate-fast.d.ts +3 -0
- package/dist/commands/core-bucket-c-workflow-gate-fast.js +17 -0
- package/dist/commands/core-bucket-c-workflow-gate-native.d.ts +7 -0
- package/dist/commands/core-bucket-c-workflow-gate-native.js +250 -0
- package/dist/commands/core.js +12 -0
- package/dist/commands/lanes-accounts.js +22 -3
- package/dist/commands/lanes.js +25 -32
- package/dist/commands/mesh.js +457 -16
- package/dist/fast-scaffold.js +26 -0
- package/dist/lib/lanes/accounts.d.ts +48 -2
- package/dist/lib/lanes/accounts.js +160 -9
- package/dist/lib/lanes/index.d.ts +2 -2
- package/dist/lib/lanes/index.js +1 -1
- package/dist/lib/lanes/message-replay.js +6 -1
- package/dist/lib/lanes/provider-rejection.d.ts +2 -6
- package/dist/lib/lanes/provider-rejection.js +19 -0
- package/dist/lib/lanes/quota-notice.d.ts +24 -0
- package/dist/lib/lanes/quota-notice.js +61 -0
- package/dist/lib/lanes/quota.d.ts +55 -15
- package/dist/lib/lanes/quota.js +231 -49
- package/dist/lib/lanes/reconcile.js +17 -1
- package/dist/lib/lanes/session-relink.d.ts +2 -0
- package/dist/lib/lanes/session-relink.js +98 -4
- package/dist/lib/lanes/spawn.d.ts +1 -1
- package/dist/lib/lanes/spawn.js +58 -12
- package/dist/lib/lanes/types.d.ts +5 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/watcher.js +13 -1
- package/dist/lib/mesh/api.d.ts +2 -0
- package/dist/lib/mesh/api.js +3 -0
- package/dist/lib/mesh/client.d.ts +6 -1
- package/dist/lib/mesh/client.js +18 -5
- package/dist/lib/mesh/team-coordination.d.ts +106 -0
- package/dist/lib/mesh/team-coordination.js +297 -0
- package/dist/lib/work-context/auto-bind.d.ts +75 -0
- package/dist/lib/work-context/auto-bind.js +245 -0
- 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
|
-
/**
|
|
116
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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"],
|
|
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
|
-
/**
|
|
385
|
-
|
|
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";
|
package/dist/lib/lanes/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|