@prohost/cli 0.6.0 → 0.7.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 +75 -0
- package/README.md +47 -0
- package/dist/agent/account_runtime.d.ts +75 -0
- package/dist/agent/account_runtime.js +130 -0
- package/dist/agent/accounts.d.ts +162 -0
- package/dist/agent/accounts.js +413 -0
- package/dist/agent/agent_commands.d.ts +22 -0
- package/dist/agent/agent_commands.js +170 -0
- package/dist/agent/api.d.ts +11 -0
- package/dist/agent/api.js +13 -1
- package/dist/agent/claude.d.ts +35 -0
- package/dist/agent/claude.js +205 -0
- package/dist/agent/command.d.ts +18 -1
- package/dist/agent/command.js +88 -4
- package/dist/agent/contract.d.ts +41 -0
- package/dist/agent/contract.js +11 -0
- package/dist/agent/credentials.d.ts +5 -0
- package/dist/agent/events.d.ts +104 -0
- package/dist/agent/events.js +175 -0
- package/dist/agent/list.d.ts +41 -0
- package/dist/agent/list.js +126 -0
- package/dist/agent/lock.d.ts +37 -0
- package/dist/agent/lock.js +91 -0
- package/dist/agent/run.d.ts +8 -2
- package/dist/agent/run.js +135 -16
- package/dist/agent/runtime_status.d.ts +70 -4
- package/dist/agent/runtime_status.js +145 -20
- package/dist/agent/workspace.d.ts +40 -0
- package/dist/agent/workspace.js +97 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +27 -5
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/agent/run.js
CHANGED
|
@@ -25,9 +25,12 @@
|
|
|
25
25
|
import os from 'node:os';
|
|
26
26
|
import { spawnSync } from 'node:child_process';
|
|
27
27
|
import WebSocket from 'ws';
|
|
28
|
+
import { HarnessAccount } from './account_runtime.js';
|
|
29
|
+
import { EVENT_AGENT_CONFIG_UPDATED, machineId } from './accounts.js';
|
|
28
30
|
import { completeRun, describeFailure, isIdempotencyInFlight, postReply } from './api.js';
|
|
29
31
|
import { MAX_SALVAGE_CHARS, claudeCommand, isClaudeExec, looksLikeResumeFailure, readClaudeStream, writeMcpConfig, } from './claude.js';
|
|
30
32
|
import { APPS_FEATURE, MCP_API_KEY_ENV_VAR, PLUGINS_DISABLED_OVERRIDE, codexBinary, codexCommand, codexMcpInventoryFlag, codexProfileName, codexShellSyntax, extraMcpEntries, isCodexExec, isolateConfiguredServers, listMcpServers, mcpServersOverride, prohostMcpEntry, looksLikeCodexResumeFailure, parseCodexOutput, } from './codex.js';
|
|
33
|
+
import { ToolEventQueue } from './events.js';
|
|
31
34
|
import { EXTRA_MCP_FILENAME, loadExtraMcpServers } from './extras.js';
|
|
32
35
|
import { MCP_SERVER_NAME, MCP_TOOL_PATTERN } from './mcp.js';
|
|
33
36
|
import { EVENT_AGENT_RUN_REQUESTED, EVENT_MENTION_CREATED, EVENT_MESSAGE_TEAM_CHAT, RUN_ERROR_CANCELLED_BY_USER, eventData, heartbeatPathFor, parseRunRequest, } from './contract.js';
|
|
@@ -257,6 +260,13 @@ function buildRuntime(options, log) {
|
|
|
257
260
|
model,
|
|
258
261
|
};
|
|
259
262
|
}
|
|
263
|
+
const account = new HarnessAccount({
|
|
264
|
+
runtime: brain === 'claude' ? 'claude_code' : 'codex',
|
|
265
|
+
env: options.env,
|
|
266
|
+
initialLabel: options.credentials.account,
|
|
267
|
+
binary: brain === 'claude' ? (options.exec.trim().split(/\s+/)[0] ?? 'claude') : codexBinary(options.exec),
|
|
268
|
+
run: options.accountCommandRunner,
|
|
269
|
+
});
|
|
260
270
|
const mcpUrl = options.credentials.mcp_url;
|
|
261
271
|
let announced = '';
|
|
262
272
|
const announce = (lines) => {
|
|
@@ -294,7 +304,7 @@ function buildRuntime(options, log) {
|
|
|
294
304
|
};
|
|
295
305
|
}
|
|
296
306
|
}
|
|
297
|
-
: codexMcpEntryProvider(options, log, workdir);
|
|
307
|
+
: codexMcpEntryProvider(options, log, workdir, () => account.spawnEnv());
|
|
298
308
|
const tools = mcpUrl ? `, ProhostAI tools prepared per run as ${MCP_SERVER_NAME}` : '';
|
|
299
309
|
const where = workdir ? ` (workspace: ${workdir})` : '';
|
|
300
310
|
log(brain === 'claude'
|
|
@@ -310,7 +320,28 @@ function buildRuntime(options, log) {
|
|
|
310
320
|
'access, on instructions from your workspace. Use --safe-tools to gate them.'
|
|
311
321
|
: `${timestamp()} ⚙ --safe-tools: tools are permission-gated; anything needing ` +
|
|
312
322
|
'approval will not run (nobody is at a terminal to approve it).');
|
|
313
|
-
|
|
323
|
+
// The account is resolved before anything is prepared or spawned, and its
|
|
324
|
+
// environment rides every spawn of this run. A missing account fails the
|
|
325
|
+
// run closed, the same way an unprovable MCP boundary does.
|
|
326
|
+
const prepareWithAccount = () => {
|
|
327
|
+
const resolved = account.resolveForSpawn();
|
|
328
|
+
if (!resolved.ok)
|
|
329
|
+
return { mcpWired: false, error: resolved.error };
|
|
330
|
+
const prepared = prepareMcp();
|
|
331
|
+
return { ...prepared, execEnv: { ...account.spawnEnv(), ...prepared.execEnv } };
|
|
332
|
+
};
|
|
333
|
+
log(`${timestamp()} ⚙ account: ${account.label()}`);
|
|
334
|
+
return {
|
|
335
|
+
workdir,
|
|
336
|
+
sessions,
|
|
337
|
+
brain,
|
|
338
|
+
prepareMcp: prepareWithAccount,
|
|
339
|
+
skipPermissions,
|
|
340
|
+
runtimeName,
|
|
341
|
+
observe,
|
|
342
|
+
model,
|
|
343
|
+
account,
|
|
344
|
+
};
|
|
314
345
|
}
|
|
315
346
|
/**
|
|
316
347
|
* The `runtime` a status frame names. A plain command is `custom` unless it is
|
|
@@ -328,11 +359,12 @@ function runtimeNameFor(brain, exec) {
|
|
|
328
359
|
* Idle capacity source for the detected brain: the agent CLI itself, asked
|
|
329
360
|
* through its own protocol so its own login answers. `plain` has none.
|
|
330
361
|
*/
|
|
331
|
-
function defaultQuotaProvider(brain, exec) {
|
|
362
|
+
function defaultQuotaProvider(brain, exec, env) {
|
|
332
363
|
if (brain === 'claude')
|
|
333
|
-
return claudeQuotaProvider({ binary: exec.trim().split(/\s+/)[0] ?? 'claude' });
|
|
334
|
-
if (brain === 'codex')
|
|
335
|
-
return codexQuotaProvider({ binary: codexBinary(exec), clientVersion: CLI_VERSION });
|
|
364
|
+
return claudeQuotaProvider({ binary: exec.trim().split(/\s+/)[0] ?? 'claude', env });
|
|
365
|
+
if (brain === 'codex') {
|
|
366
|
+
return codexQuotaProvider({ binary: codexBinary(exec), clientVersion: CLI_VERSION, env });
|
|
367
|
+
}
|
|
336
368
|
return undefined;
|
|
337
369
|
}
|
|
338
370
|
/**
|
|
@@ -342,12 +374,12 @@ function defaultQuotaProvider(brain, exec) {
|
|
|
342
374
|
* extras file is the operator's live grant list. Fresh reads make both a new
|
|
343
375
|
* configured server and a revoked extra visible to the very next run.
|
|
344
376
|
*/
|
|
345
|
-
export function codexMcpEntryProvider(options, log, workdir) {
|
|
377
|
+
export function codexMcpEntryProvider(options, log, workdir, accountEnv) {
|
|
346
378
|
let announced = '';
|
|
347
379
|
return () => {
|
|
348
380
|
const extras = loadExtraMcpServers({ env: options.env });
|
|
349
381
|
const lines = extras.warnings.map((warning) => `${timestamp()} ! ${warning}`);
|
|
350
|
-
const prepared = codexMcpEntries(options, extras.servers, (line) => lines.push(line), workdir);
|
|
382
|
+
const prepared = codexMcpEntries(options, extras.servers, (line) => lines.push(line), workdir, accountEnv?.());
|
|
351
383
|
const said = lines.join('\n');
|
|
352
384
|
if (said !== announced) {
|
|
353
385
|
for (const line of lines)
|
|
@@ -362,12 +394,15 @@ export function codexMcpEntryProvider(options, log, workdir) {
|
|
|
362
394
|
* verified switches that keep the operator's own servers out. Preparation is
|
|
363
395
|
* fail-closed: an unprovable boundary never reaches an unsandboxed agent.
|
|
364
396
|
*/
|
|
365
|
-
function codexMcpEntries(options, extraServers, log, workdir) {
|
|
397
|
+
function codexMcpEntries(options, extraServers, log, workdir, accountEnv) {
|
|
366
398
|
const fail = (detail) => {
|
|
367
399
|
log(`${timestamp()} ! ${detail}`);
|
|
368
400
|
return { mcpWired: false, error: detail };
|
|
369
401
|
};
|
|
370
|
-
|
|
402
|
+
// Every probe asks the same CODEX_HOME the run will use — the MCP servers to
|
|
403
|
+
// isolate live in that account's config.toml, not the default one.
|
|
404
|
+
const baseProbe = options.probeImpl ?? defaultProbe;
|
|
405
|
+
const probe = (command, cwd) => accountEnv && Object.keys(accountEnv).length > 0 ? baseProbe(command, cwd, accountEnv) : baseProbe(command, cwd);
|
|
371
406
|
if (codexShellSyntax(options.exec)) {
|
|
372
407
|
return fail('--exec uses shell quoting, expansion, glob, comment, escape, or control syntax that the Codex ' +
|
|
373
408
|
'isolation verifier cannot tokenize safely; use a plain argv-shaped Codex command');
|
|
@@ -493,10 +528,11 @@ function codexMcpEntries(options, extraServers, log, workdir) {
|
|
|
493
528
|
* `cwd` must match the eventual run exactly, because Codex loads trusted
|
|
494
529
|
* project `.codex/config.toml` layers from there.
|
|
495
530
|
*/
|
|
496
|
-
function defaultProbe(command, cwd) {
|
|
531
|
+
function defaultProbe(command, cwd, env) {
|
|
497
532
|
const result = spawnSync(command, {
|
|
498
533
|
shell: true,
|
|
499
534
|
cwd,
|
|
535
|
+
...(env ? { env: { ...process.env, ...env } } : {}),
|
|
500
536
|
encoding: 'utf8',
|
|
501
537
|
// Generous for a config read, short enough that a wedged binary delays
|
|
502
538
|
// a run rather than wedging the daemon.
|
|
@@ -532,7 +568,12 @@ function partialStdout(tail) {
|
|
|
532
568
|
* tell a failed turn from a successful one. A plain command's behaviour here is
|
|
533
569
|
* byte-for-byte what it was before this function existed.
|
|
534
570
|
*/
|
|
535
|
-
async function runTurn(run, prompt, options, runtime, mcp, control, log
|
|
571
|
+
async function runTurn(run, prompt, options, runtime, mcp, control, log,
|
|
572
|
+
/**
|
|
573
|
+
* Where the streaming brain's tool-call events go, if anywhere. Only the
|
|
574
|
+
* Claude Code reader produces any; absent, they are simply never collected.
|
|
575
|
+
*/
|
|
576
|
+
toolEvents) {
|
|
536
577
|
// `--timeout` is a ceiling on the run, not on each attempt: the retry below
|
|
537
578
|
// must not let one run take twice what the operator allowed. Both attempts
|
|
538
579
|
// share one deadline, which costs the retry nothing in practice — a rejected
|
|
@@ -590,6 +631,9 @@ async function runTurn(run, prompt, options, runtime, mcp, control, log) {
|
|
|
590
631
|
const activity = reader.activity();
|
|
591
632
|
if (activity)
|
|
592
633
|
control.report(activity);
|
|
634
|
+
// The heartbeat line above is unchanged for older servers; the events are
|
|
635
|
+
// the richer channel a newer server renders beneath it.
|
|
636
|
+
toolEvents?.push(reader.drainToolEvents());
|
|
593
637
|
};
|
|
594
638
|
/**
|
|
595
639
|
* Undo an eagerly recorded Claude session after an ordinary failed turn.
|
|
@@ -746,7 +790,7 @@ async function runTurn(run, prompt, options, runtime, mcp, control, log) {
|
|
|
746
790
|
const output = parseCodexOutput(result.stdout);
|
|
747
791
|
// Codex's `--json` stream carries neither model nor limits; its rollout
|
|
748
792
|
// file for this thread does.
|
|
749
|
-
runtime.observe(readCodexObservation(output.sessionId, { ...process.env, ...options.env }));
|
|
793
|
+
runtime.observe(readCodexObservation(output.sessionId, { ...process.env, ...options.env, ...runtime.account?.spawnEnv() }));
|
|
750
794
|
reportDenials(output.denials);
|
|
751
795
|
if (!output.ok) {
|
|
752
796
|
return {
|
|
@@ -764,6 +808,10 @@ async function runTurn(run, prompt, options, runtime, mcp, control, log) {
|
|
|
764
808
|
// crash banner, a `claude` that ignored `--output-format` — is a failure, and
|
|
765
809
|
// the whole-output parser is what says so in words.
|
|
766
810
|
const parsed = (stream ?? readClaudeStream()).finish(result.stdout);
|
|
811
|
+
// `finish` handles a trailing event with no newline after it; a tool event
|
|
812
|
+
// in that tail is real and must not be lost to the drain timing.
|
|
813
|
+
if (stream)
|
|
814
|
+
toolEvents?.push(stream.drainToolEvents());
|
|
767
815
|
reportDenials(parsed.denials);
|
|
768
816
|
if (!parsed.ok) {
|
|
769
817
|
forgetRecordedSession();
|
|
@@ -809,7 +857,21 @@ async function handleRun(run, options, runtime, api, log, sleep, control) {
|
|
|
809
857
|
timeoutMs: options.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS,
|
|
810
858
|
idleTimeoutMs: options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS,
|
|
811
859
|
});
|
|
812
|
-
|
|
860
|
+
// Tool-call narration for the thread. A dry run makes no writes, so it gets
|
|
861
|
+
// no path and every event is dropped; so does a server that advertised none.
|
|
862
|
+
const toolEvents = new ToolEventQueue({
|
|
863
|
+
api,
|
|
864
|
+
path: options.dryRun ? undefined : run.events_path,
|
|
865
|
+
log,
|
|
866
|
+
label,
|
|
867
|
+
});
|
|
868
|
+
const result = await runTurn(run, prompt, options, runtime, mcp, control, log, toolEvents);
|
|
869
|
+
// Whatever the turn did, the timeline is whole BEFORE the reply is posted or
|
|
870
|
+
// the run is closed — a completion that beats its own last tool row would
|
|
871
|
+
// leave a `calling` row on a finished run. Never rejects, and after `stop`
|
|
872
|
+
// nothing straggling can post against a run the server has closed.
|
|
873
|
+
await toolEvents.flush();
|
|
874
|
+
toolEvents.stop();
|
|
813
875
|
const seconds = (result.durationMs / 1000).toFixed(1);
|
|
814
876
|
// Stopped from outside. Checked before the failure branch below, because a
|
|
815
877
|
// stopped run *is* a failed command — reporting it as one would put "timed
|
|
@@ -934,6 +996,9 @@ export async function runAgentHarness(options) {
|
|
|
934
996
|
const url = options.url ?? streamUrlFor(options.credentials, DEFAULT_WS_URL);
|
|
935
997
|
const metrics = newMetrics();
|
|
936
998
|
const runtime = buildRuntime(options, log);
|
|
999
|
+
// Keys this computer's accounts server-side; the label above is display
|
|
1000
|
+
// only. Only a runtime that reports accounts needs one.
|
|
1001
|
+
const machineIdValue = runtime.account ? machineId({ ...process.env, ...options.env }) : undefined;
|
|
937
1002
|
// A credential file written by 0.1.0 stores a socket URL on a host that only
|
|
938
1003
|
// ever answers 403. Upgrading the CLI would not have fixed those installs on
|
|
939
1004
|
// its own — the stored value wins over the constant — so say what happened
|
|
@@ -960,8 +1025,11 @@ export async function runAgentHarness(options) {
|
|
|
960
1025
|
const status = new RuntimeStatusReporter({
|
|
961
1026
|
runtime: runtime.runtimeName,
|
|
962
1027
|
provider: options.quotaProvider === undefined
|
|
963
|
-
? defaultQuotaProvider(runtime.brain, options.exec)
|
|
1028
|
+
? defaultQuotaProvider(runtime.brain, options.exec, () => runtime.account?.spawnEnv() ?? {})
|
|
964
1029
|
: (options.quotaProvider ?? undefined),
|
|
1030
|
+
accounts: runtime.account
|
|
1031
|
+
? () => runtime.account.snapshot([...accepted.values()].some((entry) => entry.executing))
|
|
1032
|
+
: undefined,
|
|
965
1033
|
// A run in flight reports fresh numbers when it ends; don't probe beside it.
|
|
966
1034
|
isBusy: () => [...accepted.values()].some((entry) => entry.executing),
|
|
967
1035
|
log,
|
|
@@ -992,9 +1060,29 @@ export async function runAgentHarness(options) {
|
|
|
992
1060
|
status.observe({
|
|
993
1061
|
runtime_version: version,
|
|
994
1062
|
model: modelFromExec(options.exec) ??
|
|
995
|
-
(runtime.brain === 'codex'
|
|
1063
|
+
(runtime.brain === 'codex'
|
|
1064
|
+
? codexConfiguredModel({ ...process.env, ...options.env, ...runtime.account?.spawnEnv() })
|
|
1065
|
+
: undefined),
|
|
996
1066
|
});
|
|
997
1067
|
};
|
|
1068
|
+
/**
|
|
1069
|
+
* An operator picked another account in ProhostAI. Applied from the next
|
|
1070
|
+
* run: the run in flight (if any) finishes on the account it started on.
|
|
1071
|
+
*/
|
|
1072
|
+
const applyAccountPick = (key, via, requestedAt) => {
|
|
1073
|
+
if (!runtime.account)
|
|
1074
|
+
return false;
|
|
1075
|
+
const outcome = runtime.account.apply(key, requestedAt);
|
|
1076
|
+
if (!outcome.applied) {
|
|
1077
|
+
log(`${timestamp()} ! account change from ProhostAI ignored (${via}): ${outcome.reason}`);
|
|
1078
|
+
return false;
|
|
1079
|
+
}
|
|
1080
|
+
if (outcome.changed) {
|
|
1081
|
+
log(`${timestamp()} ⚙ account: ${outcome.record.label} (picked in ProhostAI) — used from the next run`);
|
|
1082
|
+
status.accountsChanged();
|
|
1083
|
+
}
|
|
1084
|
+
return true;
|
|
1085
|
+
};
|
|
998
1086
|
let queue = Promise.resolve();
|
|
999
1087
|
let fatal;
|
|
1000
1088
|
let attempt = 0;
|
|
@@ -1151,6 +1239,7 @@ export async function runAgentHarness(options) {
|
|
|
1151
1239
|
type: 'auth',
|
|
1152
1240
|
api_key: options.credentials.api_key,
|
|
1153
1241
|
machine,
|
|
1242
|
+
...(machineIdValue ? { machine_id: machineIdValue } : {}),
|
|
1154
1243
|
client_version: CLI_VERSION,
|
|
1155
1244
|
...(options.credentials.subscription_id
|
|
1156
1245
|
? { subscription_id: options.credentials.subscription_id }
|
|
@@ -1186,6 +1275,17 @@ export async function runAgentHarness(options) {
|
|
|
1186
1275
|
`${EVENT_AGENT_RUN_REQUESTED} (exec: ${options.exec})`);
|
|
1187
1276
|
// Only a server that says it understands the frame gets one; an
|
|
1188
1277
|
// older server may treat an unknown frame type as a protocol error.
|
|
1278
|
+
// A pick made while this agent was offline: the server repeats it on
|
|
1279
|
+
// every connect until a status frame reports the agent on it.
|
|
1280
|
+
// Its `requested_at` is the newest pick's stamp — a floor for any
|
|
1281
|
+
// live pick still in flight from before this connect.
|
|
1282
|
+
const pending = f.agent_config;
|
|
1283
|
+
if (pending && typeof pending === 'object') {
|
|
1284
|
+
if (pending.account_key !== undefined) {
|
|
1285
|
+
applyAccountPick(pending.account_key, 'on connect', pending.requested_at);
|
|
1286
|
+
}
|
|
1287
|
+
runtime.account?.noteRequestedAt(pending.requested_at);
|
|
1288
|
+
}
|
|
1189
1289
|
if (Array.isArray(f.features) && f.features.includes(RUNTIME_STATUS_FEATURE)) {
|
|
1190
1290
|
primeStatus();
|
|
1191
1291
|
status.attach((frame) => ws.send(JSON.stringify(frame)));
|
|
@@ -1220,6 +1320,21 @@ export async function runAgentHarness(options) {
|
|
|
1220
1320
|
/* best-effort */
|
|
1221
1321
|
}
|
|
1222
1322
|
};
|
|
1323
|
+
if (frame.event === EVENT_AGENT_CONFIG_UPDATED) {
|
|
1324
|
+
// It changes which subscription this machine spends, so it is held to
|
|
1325
|
+
// the same signature rule as a run.
|
|
1326
|
+
if (!verifySignature(options.credentials.webhook_secret, frame.payload, frame.signature) &&
|
|
1327
|
+
!options.allowUnverified) {
|
|
1328
|
+
metrics.signatureRejections += 1;
|
|
1329
|
+
log(`${timestamp()} ✗ rejected ${EVENT_AGENT_CONFIG_UPDATED} — signature did not verify`);
|
|
1330
|
+
ack(ACK_REJECTED);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
const data = eventData(frame.payload);
|
|
1334
|
+
const applied = applyAccountPick(data.account_key, 'live', data.requested_at);
|
|
1335
|
+
ack(applied ? ACK_ACCEPTED : ACK_IGNORED);
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1223
1338
|
if (frame.event !== EVENT_AGENT_RUN_REQUESTED) {
|
|
1224
1339
|
if (frame.event === EVENT_MENTION_CREATED || frame.event === EVENT_MESSAGE_TEAM_CHAT) {
|
|
1225
1340
|
// "runs are triggered by mentions" was wrong twice over: the gate
|
|
@@ -1332,6 +1447,10 @@ export async function runAgentHarness(options) {
|
|
|
1332
1447
|
// whole of `handleRun`, including the ten minutes of completion
|
|
1333
1448
|
// retries at the end of it.
|
|
1334
1449
|
release(run.run_id);
|
|
1450
|
+
// An account picked mid-run was held back from the status frame
|
|
1451
|
+
// until this run (on the old account) was done; report it now.
|
|
1452
|
+
if (runtime.account?.changedSinceSpawn())
|
|
1453
|
+
status.accountsChanged();
|
|
1335
1454
|
// Both terminal outcomes reached the completion endpoint; only then
|
|
1336
1455
|
// is the run settled and safe to skip on redelivery.
|
|
1337
1456
|
if (outcome === 'succeeded')
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* shapes belong to other tools and grow without asking us.
|
|
26
26
|
*/
|
|
27
27
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
28
|
+
import type { AccountBlock } from './accounts.js';
|
|
28
29
|
/** Feature name the server lists in `auth_ok.features` when it accepts the frame. */
|
|
29
30
|
export declare const RUNTIME_STATUS_FEATURE = "runtime_status";
|
|
30
31
|
/** Routine report cadence while connected. */
|
|
@@ -39,13 +40,20 @@ export declare const MIN_FRAME_SPACING_MS = 31000;
|
|
|
39
40
|
export declare const QUOTA_REFRESH_AFTER_MS: number;
|
|
40
41
|
/** Ceiling on one idle quota probe. */
|
|
41
42
|
export declare const PROVIDER_TIMEOUT_MS = 10000;
|
|
42
|
-
export type QuotaKind = 'session' | 'weekly';
|
|
43
|
+
export type QuotaKind = 'session' | 'weekly' | 'weekly_model';
|
|
43
44
|
export interface QuotaWindow {
|
|
44
45
|
kind: QuotaKind;
|
|
45
46
|
/** 0–100. */
|
|
46
47
|
used_percent: number;
|
|
47
|
-
/**
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* ISO-8601, or `null` for a window whose usage is known but which has not
|
|
50
|
+
* started — Claude's 5-hour window after five idle hours. Reported as such
|
|
51
|
+
* rather than dropped: "0%, starts on next use" is an answer, a missing
|
|
52
|
+
* window is not.
|
|
53
|
+
*/
|
|
54
|
+
resets_at: string | null;
|
|
55
|
+
/** The model a `weekly_model` window is scoped to, e.g. `Fable`. */
|
|
56
|
+
model_label?: string;
|
|
49
57
|
}
|
|
50
58
|
/** What one source managed to learn. Every field is optional; absent means "didn't learn it". */
|
|
51
59
|
export interface RuntimeObservation {
|
|
@@ -68,7 +76,17 @@ export declare function parseVersion(stdout: string): string | undefined;
|
|
|
68
76
|
* The field is marked internal upstream, so an unexpected shape yields nothing.
|
|
69
77
|
*/
|
|
70
78
|
export declare function fromClaudeRateLimitInfo(info: unknown): QuotaWindow[];
|
|
71
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Windows from a `get_usage` control response (0–100, ISO strings).
|
|
81
|
+
*
|
|
82
|
+
* A window with a usage but a `null` reset is kept, with `resets_at: null` —
|
|
83
|
+
* that is how an idle account's 5-hour window reads, and dropping it is what
|
|
84
|
+
* left paired agents showing a weekly window and nothing else (B3).
|
|
85
|
+
*
|
|
86
|
+
* Per-model weekly windows come from `rate_limits.limits[]` rows of kind
|
|
87
|
+
* `weekly_scoped` (classified on `kind`, as the schema asks, never on a
|
|
88
|
+
* label), falling back to `model_scoped[]`.
|
|
89
|
+
*/
|
|
72
90
|
export declare function fromClaudeGetUsage(usage: unknown): QuotaWindow[];
|
|
73
91
|
/** `get_usage.subscription_type` → display label; `undefined` for anything else. */
|
|
74
92
|
export declare function claudePlanLabel(subscriptionType: unknown): string | undefined;
|
|
@@ -129,6 +147,8 @@ export declare function claudeQuotaProvider(options: {
|
|
|
129
147
|
binary: string;
|
|
130
148
|
spawnImpl?: SpawnFn;
|
|
131
149
|
timeoutMs?: number;
|
|
150
|
+
/** Account environment, read at each probe so a switch applies to the next one. */
|
|
151
|
+
env?: () => NodeJS.ProcessEnv;
|
|
132
152
|
}): QuotaProvider;
|
|
133
153
|
/** Codex's `account/rateLimits/read` over `codex app-server` stdio JSON-RPC. */
|
|
134
154
|
export declare function codexQuotaProvider(options: {
|
|
@@ -136,12 +156,42 @@ export declare function codexQuotaProvider(options: {
|
|
|
136
156
|
clientVersion: string;
|
|
137
157
|
spawnImpl?: SpawnFn;
|
|
138
158
|
timeoutMs?: number;
|
|
159
|
+
/** Account environment, read at each probe so a switch applies to the next one. */
|
|
160
|
+
env?: () => NodeJS.ProcessEnv;
|
|
139
161
|
}): QuotaProvider;
|
|
162
|
+
/**
|
|
163
|
+
* Fold new windows into the ones already known, one KIND at a time.
|
|
164
|
+
*
|
|
165
|
+
* Sources see different slices: a run's `rate_limit_event` carries only the
|
|
166
|
+
* 5-hour and weekly windows (and omits one whose reset has passed), while the
|
|
167
|
+
* idle `get_usage` probe also carries the per-model weekly ones. Replacing the
|
|
168
|
+
* whole list with whatever arrived last is how a stream event used to erase
|
|
169
|
+
* the windows it simply didn't mention. A kind the observation names replaces
|
|
170
|
+
* every window of that kind; a kind it doesn't name is kept.
|
|
171
|
+
*/
|
|
172
|
+
export declare function mergeWindows(known: QuotaWindow[] | undefined, observed: QuotaWindow[]): QuotaWindow[];
|
|
173
|
+
/** What the harness knows about accounts on this machine, for the frame. */
|
|
174
|
+
export interface AccountsSnapshot {
|
|
175
|
+
/** The account this agent runs on. */
|
|
176
|
+
current?: AccountBlock;
|
|
177
|
+
/**
|
|
178
|
+
* Every account on the machine, identity and sign-in state only. Omitted
|
|
179
|
+
* when the list would be partial: the server treats it as authoritative
|
|
180
|
+
* and would forget whatever it doesn't name.
|
|
181
|
+
*/
|
|
182
|
+
all?: AccountBlock[];
|
|
183
|
+
}
|
|
140
184
|
export interface RuntimeStatusReporterOptions {
|
|
141
185
|
/** `claude_code` | `codex` | `goose` | `custom`. */
|
|
142
186
|
runtime: string;
|
|
143
187
|
/** Idle quota source; omit for a runtime that has none. */
|
|
144
188
|
provider?: QuotaProvider;
|
|
189
|
+
/**
|
|
190
|
+
* Accounts on this machine and which one this agent uses. Re-read on the
|
|
191
|
+
* same cadence as the idle quota probe (it asks each login whether it is
|
|
192
|
+
* signed in), and straight away after {@link RuntimeStatusReporter.accountsChanged}.
|
|
193
|
+
*/
|
|
194
|
+
accounts?: () => Promise<AccountsSnapshot | undefined>;
|
|
145
195
|
/** A run is executing — it will bring fresh numbers, so don't probe beside it. */
|
|
146
196
|
isBusy?: () => boolean;
|
|
147
197
|
now?: () => number;
|
|
@@ -170,9 +220,25 @@ export declare class RuntimeStatusReporter {
|
|
|
170
220
|
private cancelPending;
|
|
171
221
|
private cancelTick;
|
|
172
222
|
private refreshing;
|
|
223
|
+
private accountGeneration;
|
|
224
|
+
private accounts;
|
|
225
|
+
private accountsObservedAt;
|
|
173
226
|
constructor(options: RuntimeStatusReporterOptions);
|
|
174
227
|
/** Merge what a source learned. Absent fields keep their previous value. */
|
|
175
228
|
observe(observation: RuntimeObservation | undefined): void;
|
|
229
|
+
/**
|
|
230
|
+
* Record the accounts snapshot. Moving to another account drops what was
|
|
231
|
+
* known about the previous one's capacity and plan — they describe a
|
|
232
|
+
* different subscription — and asks for fresh numbers straight away.
|
|
233
|
+
*/
|
|
234
|
+
setAccounts(snapshot: AccountsSnapshot | undefined, options?: {
|
|
235
|
+
quiet?: boolean;
|
|
236
|
+
}): void;
|
|
237
|
+
/**
|
|
238
|
+
* The account choice changed (an operator picked one in ProhostAI): re-read
|
|
239
|
+
* accounts and capacity on the next tick rather than in fifteen minutes.
|
|
240
|
+
*/
|
|
241
|
+
accountsChanged(): void;
|
|
176
242
|
/** The model to report on a run's completion, when nothing more specific is known. */
|
|
177
243
|
model(): string | undefined;
|
|
178
244
|
/** Start reporting on a socket that advertised the feature. */
|