@indigoai-us/hq-cli 5.202.1 → 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.
- package/CHANGELOG.md +15 -0
- package/README.md +24 -0
- package/assets/lanes-hooks/hq-lanes-drain.mjs +36 -15
- package/dist/command-catalog.generated.d.ts +195 -0
- package/dist/command-catalog.generated.js +249 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/lanes.d.ts +4 -0
- package/dist/commands/lanes.js +350 -108
- package/dist/commands/monitor.d.ts +58 -0
- package/dist/commands/monitor.js +965 -0
- package/dist/lib/lanes/account-home.d.ts +8 -0
- package/dist/lib/lanes/account-home.js +10 -0
- package/dist/lib/lanes/accounts.d.ts +4 -1
- package/dist/lib/lanes/accounts.js +15 -10
- package/dist/lib/lanes/claude-account-balance.d.ts +3 -0
- package/dist/lib/lanes/claude-account-balance.js +19 -4
- package/dist/lib/lanes/config.d.ts +2 -1
- package/dist/lib/lanes/config.js +55 -13
- package/dist/lib/lanes/delivery-core.d.ts +15 -0
- package/dist/lib/lanes/delivery-core.js +21 -0
- package/dist/lib/lanes/delivery.d.ts +21 -5
- package/dist/lib/lanes/delivery.js +71 -7
- package/dist/lib/lanes/dropbox.d.ts +13 -1
- package/dist/lib/lanes/dropbox.js +61 -5
- package/dist/lib/lanes/host-capacity.d.ts +4 -0
- package/dist/lib/lanes/host-capacity.js +8 -1
- package/dist/lib/lanes/index.d.ts +3 -1
- package/dist/lib/lanes/index.js +2 -1
- package/dist/lib/lanes/liveness.d.ts +1 -0
- package/dist/lib/lanes/liveness.js +3 -1
- package/dist/lib/lanes/move-account.js +1 -1
- package/dist/lib/lanes/reconcile.js +20 -9
- package/dist/lib/lanes/spawn.d.ts +7 -3
- package/dist/lib/lanes/spawn.js +71 -22
- package/dist/lib/lanes/types.d.ts +8 -0
- package/dist/lib/monitor.d.ts +153 -0
- package/dist/lib/monitor.js +519 -0
- package/dist/main.js +40 -28
- package/dist/utils/cli-network-lifecycle.d.ts +4 -0
- package/dist/utils/cli-network-lifecycle.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AccountConfig } from "./types.js";
|
|
2
|
+
export type EffectiveAccountHome = {
|
|
3
|
+
home: string;
|
|
4
|
+
source: "host" | "default";
|
|
5
|
+
};
|
|
6
|
+
/** Resolve the account home for this host; an empty host identity fails back to the default. */
|
|
7
|
+
export declare function effectiveAccountHome(account: AccountConfig, hostname?: string): EffectiveAccountHome;
|
|
8
|
+
//# sourceMappingURL=account-home.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
/** Resolve the account home for this host; an empty host identity fails back to the default. */
|
|
3
|
+
export function effectiveAccountHome(account, hostname = os.hostname()) {
|
|
4
|
+
const host = hostname.trim();
|
|
5
|
+
const hostHome = host ? account.hosts?.[host]?.home : undefined;
|
|
6
|
+
return hostHome === undefined
|
|
7
|
+
? { home: account.home, source: "default" }
|
|
8
|
+
: { home: hostHome, source: "host" };
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=account-home.js.map
|
|
@@ -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
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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;
|
package/dist/lib/lanes/config.js
CHANGED
|
@@ -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
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type DeliveryLane, type LaneDeliveryMechanism } from "./delivery.js";
|
|
2
|
+
export interface LaneDeliveryHandlers<T> {
|
|
3
|
+
dropbox: () => T | Promise<T>;
|
|
4
|
+
resume: () => T | Promise<T>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The shared side-effect seam for lane messages. Both `hq lanes message` and
|
|
8
|
+
* `hq monitor` use the same delivery decision and cannot choose a provider
|
|
9
|
+
* path from state independently.
|
|
10
|
+
*/
|
|
11
|
+
export declare function dispatchLaneDelivery<T>(lane: DeliveryLane, handlers: LaneDeliveryHandlers<T>, resolvedMechanism?: LaneDeliveryMechanism): Promise<{
|
|
12
|
+
mechanism: LaneDeliveryMechanism;
|
|
13
|
+
value: T;
|
|
14
|
+
}>;
|
|
15
|
+
//# sourceMappingURL=delivery-core.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { deliveryRefusalReason, laneDeliveryMechanism, } from "./delivery.js";
|
|
2
|
+
import { LanesError } from "./errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* The shared side-effect seam for lane messages. Both `hq lanes message` and
|
|
5
|
+
* `hq monitor` use the same delivery decision and cannot choose a provider
|
|
6
|
+
* path from state independently.
|
|
7
|
+
*/
|
|
8
|
+
export async function dispatchLaneDelivery(lane, handlers, resolvedMechanism = laneDeliveryMechanism(lane)) {
|
|
9
|
+
const mechanism = resolvedMechanism;
|
|
10
|
+
if (mechanism === "none") {
|
|
11
|
+
const message = deliveryRefusalReason(lane);
|
|
12
|
+
throw new LanesError("lane_not_accepting_messages", message, {
|
|
13
|
+
state: lane.state,
|
|
14
|
+
provider: lane.provider,
|
|
15
|
+
message,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const value = await handlers[mechanism]();
|
|
19
|
+
return { mechanism, value };
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=delivery-core.js.map
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* which continues the recorded session and keeps its prompt cache. Codex
|
|
8
8
|
* (0.155.1) has `codex exec resume <thread-id>`; the id is the `thread_id`
|
|
9
9
|
* of the `thread.started` event on the `--json` stdout stream, and the same
|
|
10
|
-
* id comes back on every resumed turn. Grok's CLI (1.0.34)
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* id comes back on every resumed turn. Grok's CLI (1.0.34) prints a
|
|
11
|
+
* `sessionId` in its `--single --output-format json` result, and
|
|
12
|
+
* `grok --single <prompt> --resume <id>` continues that session with the same
|
|
13
|
+
* id and a warm prompt cache (verified live 2026-09-26: the resumed turn
|
|
14
|
+
* recalled the earlier turn's tool calls and read 33,792 cached tokens).
|
|
15
15
|
*
|
|
16
16
|
* Every dispatch site (`hq lanes message`, spawn, reconcile resume) asks this
|
|
17
17
|
* module rather than testing provider or state inline. A guard bound at one
|
|
@@ -21,6 +21,14 @@ import { type LaneProvider, type LaneRecord, type LaneState } from "./types.js";
|
|
|
21
21
|
/** Which providers can continue an exited worker from a recorded session id. */
|
|
22
22
|
export declare const PROVIDER_SESSION_RESUME: Record<LaneProvider, boolean>;
|
|
23
23
|
export declare function providerSupportsSessionResume(provider: LaneProvider): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Which providers' sessions can be copied to another account's home before a
|
|
26
|
+
* resume (move-account.ts). Grok resumes on the account that recorded the
|
|
27
|
+
* session, but copying its session directory between homes is not proven.
|
|
28
|
+
*/
|
|
29
|
+
export declare const PROVIDER_SESSION_PORTABLE: Record<LaneProvider, boolean>;
|
|
30
|
+
export declare function providerSessionPortable(provider: LaneProvider): boolean;
|
|
31
|
+
export declare function sessionNotPortableMessage(provider: LaneProvider): string;
|
|
24
32
|
/**
|
|
25
33
|
* States an operator message may resume from, given a session id.
|
|
26
34
|
*
|
|
@@ -96,6 +104,13 @@ export declare function parseClaudeResultOutput(text: string): WorkerRunResult;
|
|
|
96
104
|
* Nothing else on the stream is read out.
|
|
97
105
|
*/
|
|
98
106
|
export declare function parseCodexEventOutput(text: string): WorkerRunResult;
|
|
107
|
+
/**
|
|
108
|
+
* Parse the `grok --single --output-format json` result. Grok prints one
|
|
109
|
+
* pretty-printed JSON object (`sessionId`, `usage`, ...) at the end of the
|
|
110
|
+
* run, so the last top-level object in this run's stdout is the result.
|
|
111
|
+
* Only `sessionId` and the two cache counters are read out.
|
|
112
|
+
*/
|
|
113
|
+
export declare function parseGrokResultOutput(text: string): WorkerRunResult;
|
|
99
114
|
/** Per-provider stdout parser; null for a provider whose exit carries nothing to record. */
|
|
100
115
|
export declare function parseWorkerRunOutput(provider: LaneProvider, text: string): WorkerRunResult | null;
|
|
101
116
|
/**
|
|
@@ -118,6 +133,7 @@ export type LaunchTrigger = {
|
|
|
118
133
|
kind: "message";
|
|
119
134
|
text: string;
|
|
120
135
|
message_id: string;
|
|
136
|
+
monitor_event?: true;
|
|
121
137
|
} | {
|
|
122
138
|
/** `hq lanes resume`: the operator's explicit opt-in to continue an exited lane. */
|
|
123
139
|
kind: "resume";
|
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
* which continues the recorded session and keeps its prompt cache. Codex
|
|
8
8
|
* (0.155.1) has `codex exec resume <thread-id>`; the id is the `thread_id`
|
|
9
9
|
* of the `thread.started` event on the `--json` stdout stream, and the same
|
|
10
|
-
* id comes back on every resumed turn. Grok's CLI (1.0.34)
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* id comes back on every resumed turn. Grok's CLI (1.0.34) prints a
|
|
11
|
+
* `sessionId` in its `--single --output-format json` result, and
|
|
12
|
+
* `grok --single <prompt> --resume <id>` continues that session with the same
|
|
13
|
+
* id and a warm prompt cache (verified live 2026-09-26: the resumed turn
|
|
14
|
+
* recalled the earlier turn's tool calls and read 33,792 cached tokens).
|
|
15
15
|
*
|
|
16
16
|
* Every dispatch site (`hq lanes message`, spawn, reconcile resume) asks this
|
|
17
17
|
* module rather than testing provider or state inline. A guard bound at one
|
|
@@ -25,11 +25,30 @@ import { parseLaneProvider, ENVELOPE_INVALID_REASON, } from "./types.js";
|
|
|
25
25
|
export const PROVIDER_SESSION_RESUME = {
|
|
26
26
|
claude: true,
|
|
27
27
|
codex: true,
|
|
28
|
-
grok:
|
|
28
|
+
grok: true,
|
|
29
29
|
};
|
|
30
30
|
export function providerSupportsSessionResume(provider) {
|
|
31
31
|
return PROVIDER_SESSION_RESUME[parseLaneProvider(provider)];
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Which providers' sessions can be copied to another account's home before a
|
|
35
|
+
* resume (move-account.ts). Grok resumes on the account that recorded the
|
|
36
|
+
* session, but copying its session directory between homes is not proven.
|
|
37
|
+
*/
|
|
38
|
+
export const PROVIDER_SESSION_PORTABLE = {
|
|
39
|
+
claude: true,
|
|
40
|
+
codex: true,
|
|
41
|
+
grok: false,
|
|
42
|
+
};
|
|
43
|
+
export function providerSessionPortable(provider) {
|
|
44
|
+
const parsed = parseLaneProvider(provider);
|
|
45
|
+
return PROVIDER_SESSION_RESUME[parsed] && PROVIDER_SESSION_PORTABLE[parsed];
|
|
46
|
+
}
|
|
47
|
+
export function sessionNotPortableMessage(provider) {
|
|
48
|
+
return providerSupportsSessionResume(provider)
|
|
49
|
+
? `Provider ${provider} session files are not copied between accounts, so the lane cannot move to another ${provider} account`
|
|
50
|
+
: `Provider ${provider} has no session resume, so a lane cannot move between its accounts`;
|
|
51
|
+
}
|
|
33
52
|
/**
|
|
34
53
|
* States an operator message may resume from, given a session id.
|
|
35
54
|
*
|
|
@@ -230,6 +249,51 @@ export function parseCodexEventOutput(text) {
|
|
|
230
249
|
cache_creation_input_tokens: nonNegativeInt(usage.cache_write_input_tokens),
|
|
231
250
|
};
|
|
232
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* Parse the `grok --single --output-format json` result. Grok prints one
|
|
254
|
+
* pretty-printed JSON object (`sessionId`, `usage`, ...) at the end of the
|
|
255
|
+
* run, so the last top-level object in this run's stdout is the result.
|
|
256
|
+
* Only `sessionId` and the two cache counters are read out.
|
|
257
|
+
*/
|
|
258
|
+
export function parseGrokResultOutput(text) {
|
|
259
|
+
const lines = text.split("\n");
|
|
260
|
+
let sawJson = false;
|
|
261
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
262
|
+
if (!(lines[i] ?? "").startsWith("{"))
|
|
263
|
+
continue;
|
|
264
|
+
let parsed;
|
|
265
|
+
try {
|
|
266
|
+
parsed = JSON.parse(lines.slice(i).join("\n").trim());
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
272
|
+
continue;
|
|
273
|
+
sawJson = true;
|
|
274
|
+
const rec = parsed;
|
|
275
|
+
if (!("sessionId" in rec) && !("stopReason" in rec))
|
|
276
|
+
continue;
|
|
277
|
+
const usage = rec.usage && typeof rec.usage === "object" && !Array.isArray(rec.usage)
|
|
278
|
+
? rec.usage
|
|
279
|
+
: {};
|
|
280
|
+
const sessionId = typeof rec.sessionId === "string" && rec.sessionId.trim().length > 0
|
|
281
|
+
? rec.sessionId.trim()
|
|
282
|
+
: null;
|
|
283
|
+
return {
|
|
284
|
+
session_id: sessionId,
|
|
285
|
+
session_id_absent: sessionId === null ? "no_session_id_field" : null,
|
|
286
|
+
cache_read_input_tokens: nonNegativeInt(usage.cache_read_input_tokens),
|
|
287
|
+
cache_creation_input_tokens: nonNegativeInt(usage.cache_creation_input_tokens),
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
session_id: null,
|
|
292
|
+
session_id_absent: sawJson ? "no_session_id_field" : "no_result_json",
|
|
293
|
+
cache_read_input_tokens: null,
|
|
294
|
+
cache_creation_input_tokens: null,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
233
297
|
/** Per-provider stdout parser; null for a provider whose exit carries nothing to record. */
|
|
234
298
|
export function parseWorkerRunOutput(provider, text) {
|
|
235
299
|
switch (parseLaneProvider(provider)) {
|
|
@@ -238,7 +302,7 @@ export function parseWorkerRunOutput(provider, text) {
|
|
|
238
302
|
case "codex":
|
|
239
303
|
return parseCodexEventOutput(text);
|
|
240
304
|
case "grok":
|
|
241
|
-
return
|
|
305
|
+
return parseGrokResultOutput(text);
|
|
242
306
|
default:
|
|
243
307
|
return null;
|
|
244
308
|
}
|
|
@@ -20,6 +20,9 @@ export interface DropboxMessage {
|
|
|
20
20
|
kind?: string;
|
|
21
21
|
question_id?: string;
|
|
22
22
|
choice?: string;
|
|
23
|
+
monitor_id?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
sequence?: number;
|
|
23
26
|
text: string;
|
|
24
27
|
[key: string]: unknown;
|
|
25
28
|
}
|
|
@@ -38,6 +41,8 @@ export interface DrainDeps {
|
|
|
38
41
|
readStarttime?: (pid: number) => number | null;
|
|
39
42
|
/** Test seam: after observing inbox bytes, before the consume rename. */
|
|
40
43
|
afterObserveInbox?: () => void;
|
|
44
|
+
/** Abort a drain that lost its session waiter claim before consuming bytes. */
|
|
45
|
+
canDrain?: () => boolean;
|
|
41
46
|
/** Test seam: runs after this process has claimed and read its bytes. */
|
|
42
47
|
afterRead?: () => void;
|
|
43
48
|
}
|
|
@@ -62,14 +67,21 @@ export interface ToolActivityRecord {
|
|
|
62
67
|
export declare function recordToolActivity(hqRoot: string, laneId: string, at?: string): ToolActivityRecord;
|
|
63
68
|
export declare function readToolActivity(hqRoot: string, laneId: string): ToolActivityRecord | null;
|
|
64
69
|
export declare function appendDropboxMessage(hqRoot: string, laneId: string, message: DropboxMessage): DropboxMessage;
|
|
70
|
+
/** Append using the same on-disk layout for lane and monitor-session inboxes. */
|
|
71
|
+
export declare function appendDropboxMessageAtPath(box: string, message: DropboxMessage): DropboxMessage;
|
|
65
72
|
/**
|
|
66
73
|
* Drain this lane's inbox exactly once. Concurrent callers: one rename wins;
|
|
67
74
|
* the loser sees ENOENT and returns whatever it separately claimed (usually
|
|
68
75
|
* nothing). Leftovers in draining/ and dead inflight dirs are recovered.
|
|
69
76
|
*/
|
|
70
77
|
export declare function drainDropbox(hqRoot: string, laneId: string, depsInput?: Partial<DrainDeps>): DropboxMessage[];
|
|
78
|
+
/** Drain an existing inbox directory with the same atomic rename/archive path. */
|
|
79
|
+
export declare function drainDropboxAtPath(box: string, depsInput?: Partial<DrainDeps>): DropboxMessage[];
|
|
71
80
|
export declare function formatOperatorInstruction(messages: DropboxMessage[]): string;
|
|
72
|
-
export declare function
|
|
81
|
+
export declare function formatMonitorEvent(message: DropboxMessage): string;
|
|
82
|
+
/** Keep event order while framing monitor output separately from operator messages. */
|
|
83
|
+
export declare function formatDropboxMessages(messages: DropboxMessage[]): string;
|
|
84
|
+
export declare function hookAdditionalContextJson(messages: DropboxMessage[], hookEventName?: "PreToolUse" | "UserPromptSubmit"): string;
|
|
73
85
|
export interface DrainHookResult {
|
|
74
86
|
exitCode: number;
|
|
75
87
|
stdout: string;
|