@runuai/host 0.9.162 → 0.9.164
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/lib/agent-cli.ts +9 -5
- package/lib/agents/claude.ts +112 -5
- package/lib/agents/codex.ts +177 -25
- package/lib/agents/grok.ts +2 -1
- package/lib/agents/opencode.ts +35 -12
- package/lib/agents/rate-limit.ts +299 -3
- package/lib/agents/types.ts +36 -1
- package/lib/engine-accounts.ts +94 -6
- package/lib/orchestrator.ts +211 -12
- package/package.json +9 -1
- package/src/engine-models.ts +31 -0
- package/src/engine-usage.ts +104 -0
- package/src/main.ts +16 -0
- package/src/protocol.ts +65 -0
package/lib/agent-cli.ts
CHANGED
|
@@ -321,6 +321,7 @@ async function main() {
|
|
|
321
321
|
const handleOf = (v) => (typeof v === "string" && v.startsWith("@") ? v.slice(1) : v);
|
|
322
322
|
const launch = (a) => a.kind + (a.model ? "/" + a.model : "") + (a.effort ? "/" + a.effort : "");
|
|
323
323
|
if (action === "list") { out(await api("GET", "/api/agent/crew")); return; }
|
|
324
|
+
if (action === "status") { out(await api("GET", "/api/agent/crew-status")); return; }
|
|
324
325
|
if (action === "add") {
|
|
325
326
|
const personaId = pos[0];
|
|
326
327
|
if (!personaId || !flags.why) {
|
|
@@ -332,14 +333,15 @@ async function main() {
|
|
|
332
333
|
if (flags.engine) body.kind = flags.engine;
|
|
333
334
|
if (flags.model) body.model = flags.model;
|
|
334
335
|
if (flags.effort) body.effort = flags.effort;
|
|
336
|
+
if (flags.account) body.account = flags.account === "default" ? null : flags.account;
|
|
335
337
|
const r = await api("POST", "/api/agent/crew", body);
|
|
336
338
|
out("added @" + r.agent.id + " (" + r.agent.label + ") on " + launch(r.agent) + ". It stays idle until you dispatch work to it.");
|
|
337
339
|
return;
|
|
338
340
|
}
|
|
339
341
|
if (action === "model") {
|
|
340
342
|
const id = handleOf(pos[0] || "");
|
|
341
|
-
if (!id || !flags.why || !(flags.model || flags.engine || flags.effort)) {
|
|
342
|
-
console.error('uai: crew model needs @agent, at least one of --engine, --model and --
|
|
343
|
+
if (!id || !flags.why || !(flags.model || flags.engine || flags.effort || flags.account)) {
|
|
344
|
+
console.error('uai: crew model needs @agent, at least one of --engine, --model, --effort and --account, and --why "<reason>"');
|
|
343
345
|
process.exit(1);
|
|
344
346
|
}
|
|
345
347
|
// "default" clears the setting back to the engine's own default (null).
|
|
@@ -348,6 +350,7 @@ async function main() {
|
|
|
348
350
|
if (flags.engine) body.kind = flags.engine;
|
|
349
351
|
if (flags.model) body.model = cleared(flags.model);
|
|
350
352
|
if (flags.effort) body.effort = cleared(flags.effort);
|
|
353
|
+
if (flags.account) body.account = cleared(flags.account);
|
|
351
354
|
const r = await api("PATCH", "/api/agent/crew/" + encodeURIComponent(id), body);
|
|
352
355
|
out(r.changed
|
|
353
356
|
? "@" + id + " now runs on " + launch(r.agent) + "; its session restarts after its current turn"
|
|
@@ -364,7 +367,7 @@ async function main() {
|
|
|
364
367
|
out("removed @" + id);
|
|
365
368
|
return;
|
|
366
369
|
}
|
|
367
|
-
console.error("uai: crew commands are list, add, model and remove (Astronaut only)");
|
|
370
|
+
console.error("uai: crew commands are list, status, add, model and remove (Astronaut only)");
|
|
368
371
|
process.exit(1);
|
|
369
372
|
}
|
|
370
373
|
const key = group + " " + (action || "");
|
|
@@ -570,8 +573,9 @@ async function main() {
|
|
|
570
573
|
" uai task read <taskId> (full chat of a past task you were on)",
|
|
571
574
|
' uai dispatch @agent [@agent…] "instruction" (Secretary only)',
|
|
572
575
|
" uai crew list (Astronaut only: personas, engines, models, limits, roster)",
|
|
573
|
-
|
|
574
|
-
' uai crew
|
|
576
|
+
" uai crew status (Astronaut only: each agent's engine, model, account; account usage; credits)",
|
|
577
|
+
' uai crew add <personaId> [--as <handle>] [--engine <kind>] [--model <m>] [--effort <e>] [--account <id|label|default>] --why "<reason>"',
|
|
578
|
+
' uai crew model @agent [--engine <kind>] [--model <m|default>] [--effort <e|default>] [--account <id|label|default>] --why "<reason>"',
|
|
575
579
|
' uai crew remove @agent --why "<reason>"',
|
|
576
580
|
" uai react <heart|check|x> [--msg #id] (no --msg = the message you were last handed)",
|
|
577
581
|
" uai whoami",
|
package/lib/agents/claude.ts
CHANGED
|
@@ -23,8 +23,9 @@ import { requireContainerRuntimeOperational } from "../runtime-guard";
|
|
|
23
23
|
import type { TaskEnvironmentAgentSessionSurface } from "../task-environment/types";
|
|
24
24
|
import { newId } from "../ulid";
|
|
25
25
|
import { createAgentTransport, type LineTransport } from "./transport";
|
|
26
|
-
import {
|
|
26
|
+
import { rateLimitErrorUsage, resetSecondsToEpochMs } from "./rate-limit";
|
|
27
27
|
import { register } from "./registry";
|
|
28
|
+
import { CLAUDE_MODELS } from "../../src/engine-models";
|
|
28
29
|
import {
|
|
29
30
|
DISPATCH_INPUT_SCHEMA,
|
|
30
31
|
DISPATCH_RECIPIENT_ID_PATTERN,
|
|
@@ -39,6 +40,7 @@ import { extractResultUsage } from "./usage";
|
|
|
39
40
|
import type {
|
|
40
41
|
AgentEvent,
|
|
41
42
|
AgentEventHandler,
|
|
43
|
+
AgentAccountUsage,
|
|
42
44
|
AgentKind,
|
|
43
45
|
AgentSession,
|
|
44
46
|
RosterAgent,
|
|
@@ -48,7 +50,7 @@ import type {
|
|
|
48
50
|
// resolves these aliases to the current dated snapshots, so this list is
|
|
49
51
|
// stable across point releases. UPDATE WHEN MODELS CHANGE (new family or a
|
|
50
52
|
// retired alias). Order is display order in the cloud picker.
|
|
51
|
-
|
|
53
|
+
// The model list is host-agent/src/engine-models.ts (CLAUDE_MODELS).
|
|
52
54
|
|
|
53
55
|
// Opus ("opus" alias = Opus 4.8) stays the default; "fable" (Claude
|
|
54
56
|
// Fable 5, the Mythos-class flagship) is opt-in per agent. Update
|
|
@@ -251,12 +253,20 @@ function parseLineObject(raw: string): Record<string, unknown> | null {
|
|
|
251
253
|
* A malformed line yields []. The mapper is intentionally total and
|
|
252
254
|
* side-effect free.
|
|
253
255
|
*/
|
|
254
|
-
export function mapClaudeLine(raw: string): AgentEvent[] {
|
|
256
|
+
export function mapClaudeLine(raw: string, now: number = Date.now()): AgentEvent[] {
|
|
255
257
|
const json = parseLineObject(raw);
|
|
256
258
|
if (!json) return [];
|
|
257
259
|
|
|
258
260
|
const type = json.type;
|
|
259
261
|
|
|
262
|
+
// Claude Code 2.1.306 emits this exact stream-json envelope. Its embedded
|
|
263
|
+
// schema defines reset timestamps as Unix seconds and utilization as a
|
|
264
|
+
// fraction (normally 0..1).
|
|
265
|
+
if (type === "rate_limit_event" && isObj(json.rate_limit_info)) {
|
|
266
|
+
const usage = mapClaudeRateLimitInfo(json.rate_limit_info, now);
|
|
267
|
+
return usage ? [{ type: "account_usage", usage }] : [];
|
|
268
|
+
}
|
|
269
|
+
|
|
260
270
|
// --- streaming text deltas --------------------------------------------
|
|
261
271
|
if (type === "stream_event" && isObj(json.event)) {
|
|
262
272
|
const event = json.event;
|
|
@@ -303,11 +313,22 @@ export function mapClaudeLine(raw: string): AgentEvent[] {
|
|
|
303
313
|
if (json.is_error === true) {
|
|
304
314
|
// An errored turn still cost tokens — meter it.
|
|
305
315
|
const message = text || "claude returned an error";
|
|
316
|
+
const accountUsage = rateLimitErrorUsage(message, now);
|
|
306
317
|
return [
|
|
318
|
+
...(accountUsage
|
|
319
|
+
? [{ type: "account_usage", usage: accountUsage } as const]
|
|
320
|
+
: []),
|
|
307
321
|
// ADR-076: a rate-limit/usage-cap error is failed over to another
|
|
308
322
|
// account by the orchestrator rather than surfaced to the user.
|
|
309
|
-
|
|
310
|
-
? {
|
|
323
|
+
accountUsage
|
|
324
|
+
? {
|
|
325
|
+
type: "error",
|
|
326
|
+
message,
|
|
327
|
+
retryable: "rate_limit",
|
|
328
|
+
...(accountUsage.resetsAt === undefined
|
|
329
|
+
? {}
|
|
330
|
+
: { resetsAt: accountUsage.resetsAt }),
|
|
331
|
+
}
|
|
311
332
|
: { type: "error", message },
|
|
312
333
|
{ type: "turn_complete", usage },
|
|
313
334
|
];
|
|
@@ -341,6 +362,92 @@ export function mapClaudeLine(raw: string): AgentEvent[] {
|
|
|
341
362
|
return [];
|
|
342
363
|
}
|
|
343
364
|
|
|
365
|
+
const CLAUDE_WINDOW_MINUTES: Readonly<Record<string, number>> = {
|
|
366
|
+
five_hour: 5 * 60,
|
|
367
|
+
seven_day: 7 * 24 * 60,
|
|
368
|
+
seven_day_opus: 7 * 24 * 60,
|
|
369
|
+
seven_day_sonnet: 7 * 24 * 60,
|
|
370
|
+
seven_day_overage_included: 7 * 24 * 60,
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
function utilizationPercent(value: unknown): number | undefined {
|
|
374
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
return Math.min(100, value * 100);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Map Claude Code 2.1.306's embedded `RateLimitInfo` schema. */
|
|
381
|
+
export function mapClaudeRateLimitInfo(
|
|
382
|
+
info: Record<string, unknown>,
|
|
383
|
+
observedAt: number = Date.now(),
|
|
384
|
+
): AgentAccountUsage | null {
|
|
385
|
+
const status =
|
|
386
|
+
info.status === "allowed"
|
|
387
|
+
? "allowed"
|
|
388
|
+
: info.status === "allowed_warning"
|
|
389
|
+
? "warning"
|
|
390
|
+
: info.status === "rejected"
|
|
391
|
+
? "limited"
|
|
392
|
+
: null;
|
|
393
|
+
if (!status) return null;
|
|
394
|
+
|
|
395
|
+
const windows: AgentAccountUsage["windows"] = [];
|
|
396
|
+
const seen = new Set<string>();
|
|
397
|
+
if (isObj(info.unifiedWindows)) {
|
|
398
|
+
for (const window of [
|
|
399
|
+
"five_hour",
|
|
400
|
+
"seven_day",
|
|
401
|
+
"seven_day_overage_included",
|
|
402
|
+
] as const) {
|
|
403
|
+
const value = info.unifiedWindows[window];
|
|
404
|
+
if (!isObj(value)) continue;
|
|
405
|
+
const usedPercent = utilizationPercent(value.utilization);
|
|
406
|
+
const resetsAt = resetSecondsToEpochMs(value.resetsAt, observedAt);
|
|
407
|
+
windows.push({
|
|
408
|
+
window,
|
|
409
|
+
windowMinutes: CLAUDE_WINDOW_MINUTES[window],
|
|
410
|
+
...(usedPercent === undefined ? {} : { usedPercent }),
|
|
411
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
412
|
+
});
|
|
413
|
+
seen.add(window);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const rateLimitType =
|
|
418
|
+
typeof info.rateLimitType === "string" &&
|
|
419
|
+
info.rateLimitType.length > 0 &&
|
|
420
|
+
info.rateLimitType.length <= 64
|
|
421
|
+
? info.rateLimitType
|
|
422
|
+
: undefined;
|
|
423
|
+
const resetsAt = resetSecondsToEpochMs(info.resetsAt, observedAt);
|
|
424
|
+
if (rateLimitType && !seen.has(rateLimitType)) {
|
|
425
|
+
const usedPercent = utilizationPercent(info.utilization);
|
|
426
|
+
windows.push({
|
|
427
|
+
window: rateLimitType,
|
|
428
|
+
...(CLAUDE_WINDOW_MINUTES[rateLimitType] === undefined
|
|
429
|
+
? {}
|
|
430
|
+
: { windowMinutes: CLAUDE_WINDOW_MINUTES[rateLimitType] }),
|
|
431
|
+
...(usedPercent === undefined ? {} : { usedPercent }),
|
|
432
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
status,
|
|
438
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
439
|
+
...(typeof info.isUsingOverage === "boolean"
|
|
440
|
+
? { usingOverage: info.isUsingOverage }
|
|
441
|
+
: {}),
|
|
442
|
+
windows,
|
|
443
|
+
source: "claude-rate-limit-event",
|
|
444
|
+
observedAt,
|
|
445
|
+
...(rateLimitType === undefined
|
|
446
|
+
? {}
|
|
447
|
+
: { coverage: { kind: "window" as const, window: rateLimitType } }),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
344
451
|
function safeStringify(v: unknown): string {
|
|
345
452
|
if (v === undefined) return "";
|
|
346
453
|
if (typeof v === "string") return v;
|
package/lib/agents/codex.ts
CHANGED
|
@@ -40,33 +40,19 @@ import type { TaskEnvironmentAgentSessionSurface } from "../task-environment/typ
|
|
|
40
40
|
import { newId } from "../ulid";
|
|
41
41
|
import { attachCompatibilityKeysMatch } from "./attach-compatibility";
|
|
42
42
|
import { createAgentTransport, type LineTransport } from "./transport";
|
|
43
|
-
import {
|
|
43
|
+
import { rateLimitErrorUsage, resetSecondsToEpochMs } from "./rate-limit";
|
|
44
44
|
import { register } from "./registry";
|
|
45
|
+
import { CODEX_MODELS } from "../../src/engine-models";
|
|
45
46
|
import type {
|
|
46
47
|
AgentEvent,
|
|
48
|
+
AgentAccountUsage,
|
|
47
49
|
AgentEventHandler,
|
|
48
50
|
AgentKind,
|
|
49
51
|
AgentSession,
|
|
50
52
|
RosterAgent,
|
|
51
53
|
} from "./types";
|
|
52
54
|
|
|
53
|
-
//
|
|
54
|
-
// machine-readable "list models" command — the canonical list is the
|
|
55
|
-
// interactive `codex` model picker (and `codex -m <slug>` / config.toml).
|
|
56
|
-
// Sourced from that picker; UPDATE WHEN MODELS CHANGE. Order = display order
|
|
57
|
-
// in the cloud picker. Legacy models remain reachable via config.toml.
|
|
58
|
-
const CODEX_MODELS = [
|
|
59
|
-
"gpt-6-astra",
|
|
60
|
-
"gpt-5.6-sol",
|
|
61
|
-
"gpt-5.6-terra",
|
|
62
|
-
"gpt-5.6-luna",
|
|
63
|
-
"gpt-5.5",
|
|
64
|
-
"gpt-5.4",
|
|
65
|
-
"gpt-5.4-mini",
|
|
66
|
-
"gpt-5.3-codex",
|
|
67
|
-
"gpt-5.3-codex-spark",
|
|
68
|
-
"gpt-5.2",
|
|
69
|
-
];
|
|
55
|
+
// The model list is host-agent/src/engine-models.ts (CODEX_MODELS).
|
|
70
56
|
|
|
71
57
|
// gpt-5.6-sol is Codex's current frontier coding model (the CLI default).
|
|
72
58
|
// Update alongside CODEX_MODELS. When an agent's model is null the adapter
|
|
@@ -84,6 +70,11 @@ const CODEX_EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"];
|
|
|
84
70
|
// agentic task work). Update alongside CODEX_EFFORTS.
|
|
85
71
|
const CODEX_DEFAULT_EFFORT = "medium";
|
|
86
72
|
|
|
73
|
+
/** Account headroom bands are product policy, not protocol magic numbers. */
|
|
74
|
+
export const CODEX_USAGE_WARNING_PERCENT = 90;
|
|
75
|
+
export const CODEX_USAGE_LIMITED_PERCENT = 100;
|
|
76
|
+
export const CODEX_USAGE_POLL_INTERVAL_MS = 5 * 60_000;
|
|
77
|
+
|
|
87
78
|
// ---------------------------------------------------------------------------
|
|
88
79
|
// Pure protocol mapping — JSON-RPC notification → AgentEvent[].
|
|
89
80
|
// ---------------------------------------------------------------------------
|
|
@@ -112,9 +103,15 @@ const TOOL_ITEM_TYPES = new Set([
|
|
|
112
103
|
export function mapCodexNotification(
|
|
113
104
|
method: string,
|
|
114
105
|
params: unknown,
|
|
106
|
+
now: number = Date.now(),
|
|
115
107
|
): AgentEvent[] {
|
|
116
108
|
const p = isObj(params) ? params : {};
|
|
117
109
|
|
|
110
|
+
if (method === "account/rateLimits/updated") {
|
|
111
|
+
const usage = mapCodexRateLimits(p.rateLimits, now);
|
|
112
|
+
return usage ? [{ type: "account_usage", usage }] : [];
|
|
113
|
+
}
|
|
114
|
+
|
|
118
115
|
if (method === "item/agentMessage/delta") {
|
|
119
116
|
const text = str(p.delta);
|
|
120
117
|
return text ? [{ type: "message_delta", text }] : [];
|
|
@@ -143,9 +140,20 @@ export function mapCodexNotification(
|
|
|
143
140
|
const turn = isObj(p.turn) ? p.turn : {};
|
|
144
141
|
if (turn.status === "failed" && isObj(turn.error)) {
|
|
145
142
|
const message = str(turn.error.message, "codex turn failed");
|
|
143
|
+
const accountUsage = rateLimitErrorUsage(message, now);
|
|
146
144
|
return [
|
|
147
|
-
|
|
148
|
-
? { type: "
|
|
145
|
+
...(accountUsage
|
|
146
|
+
? [{ type: "account_usage", usage: accountUsage } as const]
|
|
147
|
+
: []),
|
|
148
|
+
accountUsage
|
|
149
|
+
? {
|
|
150
|
+
type: "error",
|
|
151
|
+
message,
|
|
152
|
+
retryable: "rate_limit",
|
|
153
|
+
...(accountUsage.resetsAt === undefined
|
|
154
|
+
? {}
|
|
155
|
+
: { resetsAt: accountUsage.resetsAt }),
|
|
156
|
+
}
|
|
149
157
|
: { type: "error", message },
|
|
150
158
|
{ type: "turn_complete" },
|
|
151
159
|
];
|
|
@@ -157,14 +165,88 @@ export function mapCodexNotification(
|
|
|
157
165
|
const message = isObj(p.error)
|
|
158
166
|
? str(p.error.message, "codex error")
|
|
159
167
|
: "codex error";
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
const accountUsage = rateLimitErrorUsage(message, now);
|
|
169
|
+
return accountUsage
|
|
170
|
+
? [
|
|
171
|
+
{ type: "account_usage", usage: accountUsage },
|
|
172
|
+
{
|
|
173
|
+
type: "error",
|
|
174
|
+
message,
|
|
175
|
+
retryable: "rate_limit",
|
|
176
|
+
...(accountUsage.resetsAt === undefined
|
|
177
|
+
? {}
|
|
178
|
+
: { resetsAt: accountUsage.resetsAt }),
|
|
179
|
+
},
|
|
180
|
+
]
|
|
162
181
|
: [{ type: "error", message }];
|
|
163
182
|
}
|
|
164
183
|
|
|
165
184
|
return [];
|
|
166
185
|
}
|
|
167
186
|
|
|
187
|
+
function codexWindow(
|
|
188
|
+
name: "primary" | "secondary",
|
|
189
|
+
value: unknown,
|
|
190
|
+
observedAt: number,
|
|
191
|
+
): AgentAccountUsage["windows"][number] | null {
|
|
192
|
+
if (!isObj(value) || typeof value.usedPercent !== "number") return null;
|
|
193
|
+
const usedPercent = Math.min(100, Math.max(0, value.usedPercent));
|
|
194
|
+
if (!Number.isFinite(usedPercent)) return null;
|
|
195
|
+
const windowMinutes =
|
|
196
|
+
typeof value.windowDurationMins === "number" &&
|
|
197
|
+
Number.isSafeInteger(value.windowDurationMins) &&
|
|
198
|
+
value.windowDurationMins > 0
|
|
199
|
+
? value.windowDurationMins
|
|
200
|
+
: undefined;
|
|
201
|
+
// The 0.157 schema says int64; the live app-server read confirms seconds.
|
|
202
|
+
const resetsAt = resetSecondsToEpochMs(value.resetsAt, observedAt);
|
|
203
|
+
return {
|
|
204
|
+
window: name,
|
|
205
|
+
...(windowMinutes === undefined ? {} : { windowMinutes }),
|
|
206
|
+
usedPercent,
|
|
207
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Map the shared RateLimitSnapshot used by the read response and update
|
|
212
|
+
* notification in Codex app-server 0.157. */
|
|
213
|
+
export function mapCodexRateLimits(
|
|
214
|
+
value: unknown,
|
|
215
|
+
observedAt: number = Date.now(),
|
|
216
|
+
): AgentAccountUsage | null {
|
|
217
|
+
if (!isObj(value)) return null;
|
|
218
|
+
const windows = (["primary", "secondary"] as const)
|
|
219
|
+
.map((name) => codexWindow(name, value[name], observedAt))
|
|
220
|
+
.filter((window): window is NonNullable<typeof window> => window !== null);
|
|
221
|
+
if (windows.length === 0) return null;
|
|
222
|
+
const limited = windows.filter(
|
|
223
|
+
(window) =>
|
|
224
|
+
(window.usedPercent ?? 0) >= CODEX_USAGE_LIMITED_PERCENT,
|
|
225
|
+
);
|
|
226
|
+
const status =
|
|
227
|
+
limited.length > 0
|
|
228
|
+
? "limited"
|
|
229
|
+
: windows.some(
|
|
230
|
+
(window) =>
|
|
231
|
+
(window.usedPercent ?? 0) >= CODEX_USAGE_WARNING_PERCENT,
|
|
232
|
+
)
|
|
233
|
+
? "warning"
|
|
234
|
+
: "allowed";
|
|
235
|
+
const limitedResets = limited
|
|
236
|
+
.map((window) => window.resetsAt)
|
|
237
|
+
.filter((reset): reset is number => reset !== undefined);
|
|
238
|
+
const resetsAt =
|
|
239
|
+
limitedResets.length > 0 ? Math.max(...limitedResets) : undefined;
|
|
240
|
+
return {
|
|
241
|
+
status,
|
|
242
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
243
|
+
windows,
|
|
244
|
+
source: "codex-rate-limits",
|
|
245
|
+
observedAt,
|
|
246
|
+
coverage: { kind: "all-windows" },
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
168
250
|
function codexToolTitle(
|
|
169
251
|
itemType: string,
|
|
170
252
|
item: Record<string, unknown>,
|
|
@@ -377,10 +459,15 @@ export class CodexSession implements AgentSession {
|
|
|
377
459
|
>();
|
|
378
460
|
/** permission_request id → the JSON-RPC request id to answer with. */
|
|
379
461
|
private readonly approvals = new Map<string, number | string>();
|
|
462
|
+
/** Fire-and-forget turn/start ids whose error responses end turn polling. */
|
|
463
|
+
private readonly turnStartRpcIds = new Set<number>();
|
|
380
464
|
|
|
381
465
|
private threadId: string | null = null;
|
|
382
466
|
private readonly ready: Promise<void>;
|
|
383
467
|
private readonly attachCompatibilityKey: string | undefined;
|
|
468
|
+
private turnRunning = false;
|
|
469
|
+
private lastRateLimitsReadAt = 0;
|
|
470
|
+
private rateLimitsTimer: ReturnType<typeof setTimeout> | null = null;
|
|
384
471
|
|
|
385
472
|
constructor(args: {
|
|
386
473
|
taskId: string;
|
|
@@ -458,6 +545,9 @@ export class CodexSession implements AgentSession {
|
|
|
458
545
|
this.proc.onLine((line) => this.onLine(line));
|
|
459
546
|
this.proc.onExit((code) => {
|
|
460
547
|
if (this.closed) return;
|
|
548
|
+
this.turnRunning = false;
|
|
549
|
+
this.clearRateLimitsTimer();
|
|
550
|
+
this.turnStartRpcIds.clear();
|
|
461
551
|
if (code !== 0) {
|
|
462
552
|
const tail = this.proc.stderrTail.trim();
|
|
463
553
|
this.emit({
|
|
@@ -507,6 +597,13 @@ export class CodexSession implements AgentSession {
|
|
|
507
597
|
// Response to one of our requests (id, no method).
|
|
508
598
|
if (hasId && !("method" in msg)) {
|
|
509
599
|
const id = typeof msg.id === "number" ? msg.id : -1;
|
|
600
|
+
if (this.turnStartRpcIds.delete(id)) {
|
|
601
|
+
if (isObj(msg.error)) {
|
|
602
|
+
this.turnRunning = false;
|
|
603
|
+
this.clearRateLimitsTimer();
|
|
604
|
+
}
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
510
607
|
const waiter = this.pending.get(id);
|
|
511
608
|
if (waiter) {
|
|
512
609
|
this.pending.delete(id);
|
|
@@ -540,6 +637,11 @@ export class CodexSession implements AgentSession {
|
|
|
540
637
|
// Notification (method, no id).
|
|
541
638
|
if ("method" in msg) {
|
|
542
639
|
const method = str(msg.method);
|
|
640
|
+
if (method === "turn/completed") {
|
|
641
|
+
this.turnRunning = false;
|
|
642
|
+
this.clearRateLimitsTimer();
|
|
643
|
+
this.turnStartRpcIds.clear();
|
|
644
|
+
}
|
|
543
645
|
// The thread id arrives on `thread/started`; also captured from
|
|
544
646
|
// the `thread/start` response in handshake().
|
|
545
647
|
if (method === "thread/started" && isObj(msg.params)) {
|
|
@@ -612,13 +714,15 @@ export class CodexSession implements AgentSession {
|
|
|
612
714
|
/** Send a JSON-RPC request without awaiting it — for `turn/start`,
|
|
613
715
|
* whose output streams as notifications and whose response can take
|
|
614
716
|
* minutes. The eventual response is ignored (no pending waiter). */
|
|
615
|
-
private fireRequest(method: string, params: unknown):
|
|
717
|
+
private fireRequest(method: string, params: unknown): number {
|
|
718
|
+
const id = this.nextRpcId++;
|
|
616
719
|
this.proc.writeLine({
|
|
617
720
|
jsonrpc: "2.0",
|
|
618
|
-
id
|
|
721
|
+
id,
|
|
619
722
|
method,
|
|
620
723
|
params,
|
|
621
724
|
});
|
|
725
|
+
return id;
|
|
622
726
|
}
|
|
623
727
|
|
|
624
728
|
private notify(method: string, params: unknown): void {
|
|
@@ -654,10 +758,52 @@ export class CodexSession implements AgentSession {
|
|
|
654
758
|
// (thread gone/corrupt, or the old writer never released the lock).
|
|
655
759
|
if (this.resumeThreadId && (await this.tryResumeThread(this.resumeThreadId))) {
|
|
656
760
|
this.handshakeOk = true;
|
|
761
|
+
this.readRateLimits();
|
|
657
762
|
return;
|
|
658
763
|
}
|
|
659
764
|
await this.startThread();
|
|
660
765
|
this.handshakeOk = true;
|
|
766
|
+
this.readRateLimits();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** Best-effort account snapshot. It is intentionally outside `ready`: a
|
|
770
|
+
* slow usage endpoint must not delay the agent's first turn. */
|
|
771
|
+
private readRateLimits(): void {
|
|
772
|
+
if (this.closed) return;
|
|
773
|
+
this.lastRateLimitsReadAt = Date.now();
|
|
774
|
+
void this.request("account/rateLimits/read", null)
|
|
775
|
+
.then((result) => {
|
|
776
|
+
const usage =
|
|
777
|
+
isObj(result) && "rateLimits" in result
|
|
778
|
+
? mapCodexRateLimits(result.rateLimits)
|
|
779
|
+
: null;
|
|
780
|
+
if (usage) this.emit({ type: "account_usage", usage });
|
|
781
|
+
})
|
|
782
|
+
.catch(() => {
|
|
783
|
+
// Usage is advisory. Authentication/turn errors still follow their
|
|
784
|
+
// normal app-server notifications and must not be duplicated here.
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
private clearRateLimitsTimer(): void {
|
|
789
|
+
if (this.rateLimitsTimer !== null) clearTimeout(this.rateLimitsTimer);
|
|
790
|
+
this.rateLimitsTimer = null;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
private scheduleRateLimitsRead(): void {
|
|
794
|
+
this.clearRateLimitsTimer();
|
|
795
|
+
if (!this.turnRunning || this.closed) return;
|
|
796
|
+
const wait = Math.max(
|
|
797
|
+
0,
|
|
798
|
+
this.lastRateLimitsReadAt + CODEX_USAGE_POLL_INTERVAL_MS - Date.now(),
|
|
799
|
+
);
|
|
800
|
+
this.rateLimitsTimer = setTimeout(() => {
|
|
801
|
+
this.rateLimitsTimer = null;
|
|
802
|
+
if (!this.turnRunning || this.closed) return;
|
|
803
|
+
this.readRateLimits();
|
|
804
|
+
this.scheduleRateLimitsRead();
|
|
805
|
+
}, wait);
|
|
806
|
+
this.rateLimitsTimer.unref?.();
|
|
661
807
|
}
|
|
662
808
|
|
|
663
809
|
private async startThread(): Promise<void> {
|
|
@@ -758,10 +904,13 @@ export class CodexSession implements AgentSession {
|
|
|
758
904
|
// `ready` may have waited across a runtime generation change. Recheck at
|
|
759
905
|
// the turn/start write rather than trusting session construction time.
|
|
760
906
|
requireContainerRuntimeOperational();
|
|
761
|
-
this.fireRequest("turn/start", {
|
|
907
|
+
const turnStartId = this.fireRequest("turn/start", {
|
|
762
908
|
threadId: this.threadId,
|
|
763
909
|
input: [{ type: "text", text }],
|
|
764
910
|
});
|
|
911
|
+
this.turnStartRpcIds.add(turnStartId);
|
|
912
|
+
this.turnRunning = true;
|
|
913
|
+
this.scheduleRateLimitsRead();
|
|
765
914
|
}
|
|
766
915
|
|
|
767
916
|
async interrupt(): Promise<void> {
|
|
@@ -791,6 +940,9 @@ export class CodexSession implements AgentSession {
|
|
|
791
940
|
return;
|
|
792
941
|
}
|
|
793
942
|
this.closed = true;
|
|
943
|
+
this.turnRunning = false;
|
|
944
|
+
this.clearRateLimitsTimer();
|
|
945
|
+
this.turnStartRpcIds.clear();
|
|
794
946
|
for (const waiter of this.pending.values()) {
|
|
795
947
|
waiter.reject(new Error("session closed"));
|
|
796
948
|
}
|
package/lib/agents/grok.ts
CHANGED
|
@@ -43,6 +43,7 @@ import {
|
|
|
43
43
|
type AgentEnvironmentProcess,
|
|
44
44
|
} from "./proc";
|
|
45
45
|
import { register } from "./registry";
|
|
46
|
+
import { GROK_MODELS } from "../../src/engine-models";
|
|
46
47
|
import type {
|
|
47
48
|
AgentEvent,
|
|
48
49
|
AgentEventHandler,
|
|
@@ -56,7 +57,7 @@ export const GROK_LOADER_LOGIN_ENV = "0";
|
|
|
56
57
|
|
|
57
58
|
// `grok models` lists what the account can run. grok-4.5 is the current
|
|
58
59
|
// default; UPDATE WHEN xAI CHANGES the lineup.
|
|
59
|
-
|
|
60
|
+
// The model list is host-agent/src/engine-models.ts (GROK_MODELS).
|
|
60
61
|
const GROK_DEFAULT_MODEL = "grok-4.5";
|
|
61
62
|
// Grok has no per-invocation reasoning-effort flag.
|
|
62
63
|
const GROK_EFFORTS: string[] = [];
|
package/lib/agents/opencode.ts
CHANGED
|
@@ -44,7 +44,7 @@ import {
|
|
|
44
44
|
spawnEnvironmentProcess,
|
|
45
45
|
type AgentEnvironmentProcess,
|
|
46
46
|
} from "./proc";
|
|
47
|
-
import {
|
|
47
|
+
import { rateLimitErrorUsage } from "./rate-limit";
|
|
48
48
|
import { register } from "./registry";
|
|
49
49
|
import type {
|
|
50
50
|
AgentEvent,
|
|
@@ -191,7 +191,10 @@ function toolDetail(state: unknown): string {
|
|
|
191
191
|
* Map a single OpenCode `run --format json` line. Non-JSON lines and events
|
|
192
192
|
* with no user-facing meaning (step_start) map to no events. Total + pure.
|
|
193
193
|
*/
|
|
194
|
-
export function mapOpencodeLine(
|
|
194
|
+
export function mapOpencodeLine(
|
|
195
|
+
line: string,
|
|
196
|
+
now: number = Date.now(),
|
|
197
|
+
): MappedOpencodeLine {
|
|
195
198
|
const trimmed = line.trim();
|
|
196
199
|
if (!trimmed.startsWith("{")) return { events: [] };
|
|
197
200
|
let msg: Record<string, unknown>;
|
|
@@ -248,12 +251,23 @@ export function mapOpencodeLine(line: string): MappedOpencodeLine {
|
|
|
248
251
|
const data = isObj(err.data) ? err.data : {};
|
|
249
252
|
const message =
|
|
250
253
|
str(data.message) || str(err.name) || "opencode error";
|
|
254
|
+
const accountUsage = rateLimitErrorUsage(message, now);
|
|
251
255
|
return {
|
|
252
|
-
events:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
256
|
+
events: accountUsage
|
|
257
|
+
? [
|
|
258
|
+
{ type: "account_usage", usage: accountUsage },
|
|
259
|
+
{
|
|
260
|
+
type: "error",
|
|
261
|
+
message,
|
|
262
|
+
retryable: "rate_limit",
|
|
263
|
+
...(accountUsage.resetsAt === undefined
|
|
264
|
+
? {}
|
|
265
|
+
: { resetsAt: accountUsage.resetsAt }),
|
|
266
|
+
},
|
|
267
|
+
]
|
|
268
|
+
: [
|
|
269
|
+
{ type: "error", message },
|
|
270
|
+
],
|
|
257
271
|
sessionId,
|
|
258
272
|
};
|
|
259
273
|
}
|
|
@@ -421,11 +435,20 @@ export class OpencodeSession implements AgentSession {
|
|
|
421
435
|
if (code !== 0 && !sawError) {
|
|
422
436
|
const tail = stderr.trim().slice(-500);
|
|
423
437
|
const message = `opencode exited ${code ?? "null"}${tail ? `: ${tail}` : ""}`;
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
438
|
+
const accountUsage = rateLimitErrorUsage(message);
|
|
439
|
+
if (accountUsage) {
|
|
440
|
+
this.emit({ type: "account_usage", usage: accountUsage });
|
|
441
|
+
this.emit({
|
|
442
|
+
type: "error",
|
|
443
|
+
message,
|
|
444
|
+
retryable: "rate_limit",
|
|
445
|
+
...(accountUsage.resetsAt === undefined
|
|
446
|
+
? {}
|
|
447
|
+
: { resetsAt: accountUsage.resetsAt }),
|
|
448
|
+
});
|
|
449
|
+
} else {
|
|
450
|
+
this.emit({ type: "error", message });
|
|
451
|
+
}
|
|
429
452
|
}
|
|
430
453
|
this.emit({
|
|
431
454
|
type: "turn_complete",
|