@runuai/host 0.9.166 → 0.9.167
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/images/standard/Dockerfile +3 -3
- package/lib/agents/codex.ts +2 -2
- package/lib/agents/cursor-period-usage.ts +253 -0
- package/lib/agents/cursor.ts +195 -25
- package/lib/agents/grok.ts +21 -8
- package/lib/agents/registry.ts +19 -19
- package/lib/agents/types.ts +1 -1
- package/lib/agents/usage.ts +16 -9
- package/lib/orchestrator.ts +27 -2
- package/lib/standard-image.ts +1 -1
- package/package.json +1 -1
- package/src/engine-models.ts +16 -8
- package/src/protocol.ts +5 -2
|
@@ -32,9 +32,9 @@ ENV ASDF_VERSION=v0.14.1
|
|
|
32
32
|
|
|
33
33
|
# Qualified agent builds. These are execution-contract pins: per-command env
|
|
34
34
|
# freshness is requalified before any of them moves.
|
|
35
|
-
ENV UAI_CODEX_CLI_VERSION=0.
|
|
36
|
-
ENV UAI_CURSOR_AGENT_VERSION=2026.
|
|
37
|
-
ENV UAI_GROK_CLI_VERSION=1.0.
|
|
35
|
+
ENV UAI_CODEX_CLI_VERSION=0.164.0
|
|
36
|
+
ENV UAI_CURSOR_AGENT_VERSION=2026.10.22-27dcf5f
|
|
37
|
+
ENV UAI_GROK_CLI_VERSION=1.0.70
|
|
38
38
|
|
|
39
39
|
# ---------------------------------------------------------------------------
|
|
40
40
|
# 1. System packages.
|
package/lib/agents/codex.ts
CHANGED
|
@@ -56,11 +56,11 @@ import type {
|
|
|
56
56
|
|
|
57
57
|
// The model list is host-agent/src/engine-models.ts (CODEX_MODELS).
|
|
58
58
|
|
|
59
|
-
// gpt-
|
|
59
|
+
// gpt-6.1-sol is Codex's current workhorse coding model (the CLI default).
|
|
60
60
|
// Update alongside CODEX_MODELS. When an agent's model is null the adapter
|
|
61
61
|
// omits the override entirely and Codex uses the user's own configured
|
|
62
62
|
// default.
|
|
63
|
-
const CODEX_DEFAULT_MODEL = "gpt-
|
|
63
|
+
const CODEX_DEFAULT_MODEL = "gpt-6.1-sol";
|
|
64
64
|
|
|
65
65
|
// Reasoning levels set via `-c model_reasoning_effort=<level>`. "xhigh" is
|
|
66
66
|
// the picker's "Extra high"; gpt-5.6 added "max" and "ultra" (ultra =
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor account room, read from the same dashboard calls the CLI's `/usage`
|
|
3
|
+
* screen uses. The headless stream does not report it.
|
|
4
|
+
*
|
|
5
|
+
* The CLI session access token (not the user API key, and not a team admin
|
|
6
|
+
* key) calls `aiserver.v1.DashboardService/GetPlanInfo` and
|
|
7
|
+
* `GetCurrentPeriodUsage` on api2.cursor.sh. A failure, a missing token, or a
|
|
8
|
+
* shape we do not recognise produces no report. Nothing here logs the token,
|
|
9
|
+
* an email, or a user id.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { TaskEnvironmentAgentSessionSurface } from "../task-environment/types";
|
|
13
|
+
import type { AgentAccountUsage } from "./types";
|
|
14
|
+
|
|
15
|
+
/** Same lines Codex uses for a window that is full or close to full. */
|
|
16
|
+
export const CURSOR_PERIOD_WARNING_PERCENT = 90;
|
|
17
|
+
export const CURSOR_PERIOD_LIMITED_PERCENT = 100;
|
|
18
|
+
|
|
19
|
+
/** One probe per leased credential, including a failed one. */
|
|
20
|
+
export const CURSOR_PERIOD_PROBE_INTERVAL_MS = 5 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
/** One bound for the whole probe: reading auth.json and both dashboard calls.
|
|
23
|
+
* A hung read must not hold turn_complete after the CLI has finished. */
|
|
24
|
+
export const CURSOR_PERIOD_PROBE_TIMEOUT_MS = 4_000;
|
|
25
|
+
|
|
26
|
+
export const CURSOR_PERIOD_USAGE_LOG = "cursor period usage unavailable";
|
|
27
|
+
|
|
28
|
+
export const CURSOR_AUTH_JSON = "/home/node/.config/cursor/auth.json";
|
|
29
|
+
|
|
30
|
+
const DASHBOARD_ORIGIN = "https://api2.cursor.sh";
|
|
31
|
+
const WEEKLY_PERIOD = "INCLUDED_USAGE_PERIOD_WEEKLY";
|
|
32
|
+
const WEEK_MINUTES = 7 * 24 * 60;
|
|
33
|
+
|
|
34
|
+
const probedAt = new Map<string, number>();
|
|
35
|
+
|
|
36
|
+
/** Whether this credential may be probed at `now`. */
|
|
37
|
+
export function cursorPeriodProbeDue(credentialId: string, now: number): boolean {
|
|
38
|
+
const at = probedAt.get(credentialId);
|
|
39
|
+
return at === undefined || now - at >= CURSOR_PERIOD_PROBE_INTERVAL_MS;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Count a probe, successful or not, so the next one waits out the interval. */
|
|
43
|
+
export function markCursorPeriodProbed(credentialId: string, now: number): void {
|
|
44
|
+
probedAt.set(credentialId, now);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Test hook. Production never clears the gate except by process restart. */
|
|
48
|
+
export function resetCursorPeriodProbes(): void {
|
|
49
|
+
probedAt.clear();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The CLI session access token from `auth.json`, or null. The raw file is not returned. */
|
|
53
|
+
export function cursorAccessTokenFromAuthJson(text: string): string | null {
|
|
54
|
+
try {
|
|
55
|
+
const parsed: unknown = JSON.parse(text);
|
|
56
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
57
|
+
const token = (parsed as { accessToken?: unknown }).accessToken;
|
|
58
|
+
return typeof token === "string" && token.length > 0 ? token : null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isObj(value: unknown): value is Record<string, unknown> {
|
|
65
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function epochMs(value: unknown): number | undefined {
|
|
69
|
+
const n =
|
|
70
|
+
typeof value === "number"
|
|
71
|
+
? value
|
|
72
|
+
: typeof value === "string" && /^[0-9]+$/.test(value)
|
|
73
|
+
? Number(value)
|
|
74
|
+
: Number.NaN;
|
|
75
|
+
return Number.isSafeInteger(n) && n > 0 ? n : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A 0–100 share, as the dashboard sends it. Anything else is not a window. */
|
|
79
|
+
function percent(value: unknown): number | undefined {
|
|
80
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 100) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function onDemandSpend(period: Record<string, unknown>): boolean {
|
|
87
|
+
if (!isObj(period.spendLimitUsage)) return false;
|
|
88
|
+
for (const key of ["individualUsed", "pooledUsed", "totalSpend", "overallUsed"]) {
|
|
89
|
+
const value = period.spendLimitUsage[key];
|
|
90
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) return true;
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Map a GetPlanInfo body and a GetCurrentPeriodUsage body into one account
|
|
97
|
+
* observation. Null when the percent fields are missing or not 0–100, so a
|
|
98
|
+
* schema change sends nothing.
|
|
99
|
+
*/
|
|
100
|
+
export function mapCursorPeriodUsage(
|
|
101
|
+
plan: unknown,
|
|
102
|
+
period: unknown,
|
|
103
|
+
observedAt: number,
|
|
104
|
+
): AgentAccountUsage | null {
|
|
105
|
+
if (!isObj(period) || !isObj(period.planUsage)) return null;
|
|
106
|
+
const total = percent(period.planUsage.totalPercentUsed);
|
|
107
|
+
const api = percent(period.planUsage.apiPercentUsed);
|
|
108
|
+
if (total === undefined || api === undefined) return null;
|
|
109
|
+
|
|
110
|
+
const planInfo = isObj(plan) && isObj(plan.planInfo) ? plan.planInfo : {};
|
|
111
|
+
const weekly = planInfo.includedUsagePeriod === WEEKLY_PERIOD;
|
|
112
|
+
const resetsAt = epochMs(planInfo.includedUsageResetsAt);
|
|
113
|
+
const window = (name: string, usedPercent: number) => ({
|
|
114
|
+
window: name,
|
|
115
|
+
...(weekly ? { windowMinutes: WEEK_MINUTES } : {}),
|
|
116
|
+
usedPercent,
|
|
117
|
+
...(resetsAt === undefined ? {} : { resetsAt }),
|
|
118
|
+
});
|
|
119
|
+
const windows = [window("included", total), window("api", api)];
|
|
120
|
+
const limited = windows.filter((item) => item.usedPercent >= CURSOR_PERIOD_LIMITED_PERCENT);
|
|
121
|
+
const status =
|
|
122
|
+
limited.length > 0
|
|
123
|
+
? "limited"
|
|
124
|
+
: windows.some((item) => item.usedPercent >= CURSOR_PERIOD_WARNING_PERCENT)
|
|
125
|
+
? "warning"
|
|
126
|
+
: "allowed";
|
|
127
|
+
const limitedResets = limited
|
|
128
|
+
.map((item) => item.resetsAt)
|
|
129
|
+
.filter((reset): reset is number => reset !== undefined);
|
|
130
|
+
const usage: AgentAccountUsage = {
|
|
131
|
+
status,
|
|
132
|
+
...(limitedResets.length > 0 ? { resetsAt: Math.max(...limitedResets) } : {}),
|
|
133
|
+
...(onDemandSpend(period) ? { usingOverage: true } : {}),
|
|
134
|
+
windows,
|
|
135
|
+
source: "cursor-period-usage",
|
|
136
|
+
observedAt,
|
|
137
|
+
coverage: { kind: "all-windows" },
|
|
138
|
+
};
|
|
139
|
+
return usage;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function readCursorAuthFromEnvironment(
|
|
143
|
+
environment: TaskEnvironmentAgentSessionSurface,
|
|
144
|
+
signal?: AbortSignal,
|
|
145
|
+
): Promise<string | null> {
|
|
146
|
+
if (signal?.aborted) return null;
|
|
147
|
+
let child: Awaited<ReturnType<TaskEnvironmentAgentSessionSurface["spawnSession"]>> | undefined;
|
|
148
|
+
const kill = (): void => {
|
|
149
|
+
if (!child) return;
|
|
150
|
+
void child.terminate("SIGKILL").catch(() => {});
|
|
151
|
+
};
|
|
152
|
+
signal?.addEventListener("abort", kill);
|
|
153
|
+
try {
|
|
154
|
+
child = await environment.spawnSession({
|
|
155
|
+
argv: ["cat", "--", CURSOR_AUTH_JSON],
|
|
156
|
+
user: "node",
|
|
157
|
+
projectEnvProfile: "task",
|
|
158
|
+
env: {},
|
|
159
|
+
maxOutputBytes: 32_768,
|
|
160
|
+
});
|
|
161
|
+
if (signal?.aborted) {
|
|
162
|
+
kill();
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
let text = "";
|
|
166
|
+
for await (const chunk of child.stdout) {
|
|
167
|
+
if (signal?.aborted) {
|
|
168
|
+
kill();
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
text += Buffer.from(chunk).toString("utf8");
|
|
172
|
+
if (text.length > 32_768) {
|
|
173
|
+
kill();
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (signal?.aborted) {
|
|
178
|
+
kill();
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const exit = await child.completion;
|
|
182
|
+
return !signal?.aborted && exit.exitCode === 0 ? text : null;
|
|
183
|
+
} catch {
|
|
184
|
+
return null;
|
|
185
|
+
} finally {
|
|
186
|
+
signal?.removeEventListener("abort", kill);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function postDashboard(
|
|
191
|
+
fetchImpl: typeof fetch,
|
|
192
|
+
token: string,
|
|
193
|
+
method: string,
|
|
194
|
+
signal: AbortSignal,
|
|
195
|
+
): Promise<unknown | null> {
|
|
196
|
+
if (signal.aborted) return null;
|
|
197
|
+
try {
|
|
198
|
+
const response = await fetchImpl(
|
|
199
|
+
`${DASHBOARD_ORIGIN}/aiserver.v1.DashboardService/${method}`,
|
|
200
|
+
{
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: {
|
|
203
|
+
Authorization: `Bearer ${token}`,
|
|
204
|
+
"Content-Type": "application/json",
|
|
205
|
+
"Connect-Protocol-Version": "1",
|
|
206
|
+
},
|
|
207
|
+
body: "{}",
|
|
208
|
+
signal,
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
if (response.status !== 200) return null;
|
|
212
|
+
return (await response.json()) as unknown;
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Read both dashboard methods and map them. Null on a non-200, a timeout, a
|
|
220
|
+
* bad body, or a shape we do not recognise. The token is sent only as the
|
|
221
|
+
* bearer for these two calls.
|
|
222
|
+
*/
|
|
223
|
+
export async function readCursorPeriodUsage(args: {
|
|
224
|
+
token: string;
|
|
225
|
+
fetchImpl?: typeof fetch;
|
|
226
|
+
timeoutMs?: number;
|
|
227
|
+
/** When set, this call does not start a second timer. The caller's deadline covers it. */
|
|
228
|
+
signal?: AbortSignal;
|
|
229
|
+
now?: number;
|
|
230
|
+
}): Promise<AgentAccountUsage | null> {
|
|
231
|
+
const fetchImpl = args.fetchImpl ?? fetch;
|
|
232
|
+
const owned = new AbortController();
|
|
233
|
+
const onParentAbort = (): void => owned.abort();
|
|
234
|
+
if (args.signal) {
|
|
235
|
+
if (args.signal.aborted) owned.abort();
|
|
236
|
+
else args.signal.addEventListener("abort", onParentAbort);
|
|
237
|
+
}
|
|
238
|
+
const timer =
|
|
239
|
+
args.signal === undefined
|
|
240
|
+
? setTimeout(() => owned.abort(), args.timeoutMs ?? CURSOR_PERIOD_PROBE_TIMEOUT_MS)
|
|
241
|
+
: undefined;
|
|
242
|
+
try {
|
|
243
|
+
const [plan, period] = await Promise.all([
|
|
244
|
+
postDashboard(fetchImpl, args.token, "GetPlanInfo", owned.signal),
|
|
245
|
+
postDashboard(fetchImpl, args.token, "GetCurrentPeriodUsage", owned.signal),
|
|
246
|
+
]);
|
|
247
|
+
if (owned.signal.aborted || plan === null || period === null) return null;
|
|
248
|
+
return mapCursorPeriodUsage(plan, period, args.now ?? Date.now());
|
|
249
|
+
} finally {
|
|
250
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
251
|
+
args.signal?.removeEventListener("abort", onParentAbort);
|
|
252
|
+
}
|
|
253
|
+
}
|
package/lib/agents/cursor.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* CURSOR_API_KEY=… /home/node/.local/bin/cursor-agent -p "<prompt>" \
|
|
6
6
|
* --output-format stream-json --stream-partial-output --force --trust \
|
|
7
|
-
*
|
|
7
|
+
* --model <model> [--resume <sessionId>]
|
|
8
8
|
*
|
|
9
9
|
* One-shot per turn (like Kimi/Grok); sends serialized; continuity via
|
|
10
10
|
* `--resume <sessionId>` captured from the stream. Cursor's stream-json is
|
|
@@ -26,6 +26,9 @@
|
|
|
26
26
|
* `--trust` (skip the workspace-trust prompt). Cursor has no system-prompt
|
|
27
27
|
* flag, so the briefing folds into the first turn's prompt.
|
|
28
28
|
*/
|
|
29
|
+
import { stripVTControlCharacters } from "node:util";
|
|
30
|
+
|
|
31
|
+
import { ENGINE_ACCOUNT_ENV } from "../credential-consumers/engine-anchor";
|
|
29
32
|
import { requireContainerRuntimeOperational } from "../runtime-guard";
|
|
30
33
|
import type {
|
|
31
34
|
TaskEnvironmentAgentSessionSurface,
|
|
@@ -38,8 +41,19 @@ import {
|
|
|
38
41
|
spawnEnvironmentProcess,
|
|
39
42
|
type AgentEnvironmentProcess,
|
|
40
43
|
} from "./proc";
|
|
44
|
+
import {
|
|
45
|
+
CURSOR_PERIOD_PROBE_TIMEOUT_MS,
|
|
46
|
+
CURSOR_PERIOD_USAGE_LOG,
|
|
47
|
+
cursorAccessTokenFromAuthJson,
|
|
48
|
+
cursorPeriodProbeDue,
|
|
49
|
+
markCursorPeriodProbed,
|
|
50
|
+
readCursorAuthFromEnvironment,
|
|
51
|
+
readCursorPeriodUsage,
|
|
52
|
+
} from "./cursor-period-usage";
|
|
53
|
+
import { rateLimitErrorUsage } from "./rate-limit";
|
|
41
54
|
import { extractResultUsage } from "./usage";
|
|
42
55
|
import type {
|
|
56
|
+
AgentAccountUsage,
|
|
43
57
|
AgentEvent,
|
|
44
58
|
AgentEventHandler,
|
|
45
59
|
AgentKind,
|
|
@@ -57,19 +71,25 @@ const CURSOR_BIN = "/home/node/.local/bin/cursor-agent";
|
|
|
57
71
|
export const CURSOR_SANDBOX_ENV_RESTORE =
|
|
58
72
|
". /usr/local/share/uai/env-hook.sh; __uai_env_sync";
|
|
59
73
|
|
|
60
|
-
// A curated slice of Cursor's
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"auto",
|
|
74
|
+
// A curated fallback slice of Cursor's signed-in 2026.10.22 catalog. The
|
|
75
|
+
// newest version of each tier comes first; older known-good ids follow.
|
|
76
|
+
export const CURSOR_MODELS = [
|
|
77
|
+
"grok-4.7-high",
|
|
78
|
+
"grok-4.7-high-fast",
|
|
66
79
|
"composer-2.5",
|
|
67
|
-
"
|
|
80
|
+
"composer-2.5-fast",
|
|
81
|
+
"claude-opus-5-5-high",
|
|
82
|
+
"claude-fable-5-1-thinking-high",
|
|
83
|
+
"claude-sonnet-5-5-high",
|
|
68
84
|
"gpt-5.6-sol-high",
|
|
85
|
+
"gemini-4-argon-high",
|
|
86
|
+
"kimi-k3-high",
|
|
87
|
+
"auto",
|
|
88
|
+
"claude-opus-4-8-thinking-high",
|
|
69
89
|
"cursor-grok-4.5-high",
|
|
70
90
|
"claude-fable-5-thinking-high",
|
|
71
91
|
];
|
|
72
|
-
const CURSOR_DEFAULT_MODEL = "
|
|
92
|
+
const CURSOR_DEFAULT_MODEL = "grok-4.7-high";
|
|
73
93
|
const CURSOR_EFFORTS: string[] = []; // effort is baked into the model id
|
|
74
94
|
|
|
75
95
|
// ---------------------------------------------------------------------------
|
|
@@ -99,12 +119,30 @@ export interface MappedCursorLine {
|
|
|
99
119
|
finalText?: string;
|
|
100
120
|
sessionId?: string;
|
|
101
121
|
errorText?: string;
|
|
102
|
-
/**
|
|
103
|
-
|
|
122
|
+
/** This model needs paid Cursor credits; the account itself is still usable. */
|
|
123
|
+
creditRefusal?: boolean;
|
|
124
|
+
/** Set when `errorText` is a usage cap. Secret-free; source `error-text`. */
|
|
125
|
+
accountUsage?: AgentAccountUsage;
|
|
126
|
+
/** Tokens from the result envelope. Claude-shaped lines carry cost;
|
|
127
|
+
* cursor-agent 2026.10.x carries camelCase counts and no cost. */
|
|
104
128
|
usage?: AgentUsage;
|
|
105
129
|
}
|
|
106
130
|
|
|
107
|
-
|
|
131
|
+
/** Cursor Premium can refuse one paid-credit model while the same account can
|
|
132
|
+
* still run Cursor's Grok and Composer models. This is not account exhaustion:
|
|
133
|
+
* keep it non-retryable so the orchestrator neither rotates nor cools the
|
|
134
|
+
* account, and give the person an actionable model-specific error. */
|
|
135
|
+
export function cursorCreditsRequiredMessage(message: string): string | null {
|
|
136
|
+
const match = stripVTControlCharacters(message).match(
|
|
137
|
+
/(?:ActionRequiredError:\s*)?([^\r\n]+?)\s+is not included in your weekly usage\b/i,
|
|
138
|
+
);
|
|
139
|
+
const model = match?.[1]?.trim();
|
|
140
|
+
return model
|
|
141
|
+
? `${model} needs Cursor credits on this plan; use a Grok or Composer model`
|
|
142
|
+
: null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function mapCursorLine(line: string, now: number = Date.now()): MappedCursorLine {
|
|
108
146
|
const t = line.trim();
|
|
109
147
|
if (!t.startsWith("{")) return {};
|
|
110
148
|
let m: Record<string, unknown>;
|
|
@@ -141,15 +179,23 @@ export function mapCursorLine(line: string): MappedCursorLine {
|
|
|
141
179
|
}
|
|
142
180
|
if (m.type === "result") {
|
|
143
181
|
const errored = m.is_error === true;
|
|
182
|
+
const rawErrorText = errored
|
|
183
|
+
? typeof m.result === "string"
|
|
184
|
+
? m.result
|
|
185
|
+
: "cursor turn failed"
|
|
186
|
+
: undefined;
|
|
187
|
+
const creditError = rawErrorText
|
|
188
|
+
? cursorCreditsRequiredMessage(rawErrorText)
|
|
189
|
+
: null;
|
|
190
|
+
const errorText = creditError ?? rawErrorText;
|
|
191
|
+
const accountUsage = errorText ? rateLimitErrorUsage(errorText, now) : null;
|
|
144
192
|
return {
|
|
145
193
|
end: true,
|
|
146
194
|
sessionId: typeof m.session_id === "string" ? m.session_id : undefined,
|
|
147
195
|
finalText: typeof m.result === "string" ? m.result : undefined,
|
|
148
|
-
errorText
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
: "cursor turn failed"
|
|
152
|
-
: undefined,
|
|
196
|
+
errorText,
|
|
197
|
+
...(creditError ? { creditRefusal: true } : {}),
|
|
198
|
+
...(accountUsage ? { accountUsage } : {}),
|
|
153
199
|
usage: extractResultUsage(m),
|
|
154
200
|
};
|
|
155
201
|
}
|
|
@@ -175,6 +221,9 @@ export class CursorSession implements AgentSession {
|
|
|
175
221
|
private readonly agentEnv: Record<string, string>;
|
|
176
222
|
private readonly spawner: Spawner;
|
|
177
223
|
private readonly apiKey: string;
|
|
224
|
+
private readonly readAuthFile: (signal: AbortSignal) => Promise<string | null>;
|
|
225
|
+
private readonly fetchImpl: typeof fetch;
|
|
226
|
+
private readonly probeTimeoutMs: number;
|
|
178
227
|
|
|
179
228
|
private readonly handlers = new Set<AgentEventHandler>();
|
|
180
229
|
private sessionId: string | null = null;
|
|
@@ -191,6 +240,14 @@ export class CursorSession implements AgentSession {
|
|
|
191
240
|
agentEnv?: Record<string, string>;
|
|
192
241
|
spawner?: Spawner;
|
|
193
242
|
apiKey?: string;
|
|
243
|
+
/** Reads the CLI's auth.json from the task environment. Tests inject this.
|
|
244
|
+
* The signal aborts when the probe deadline passes; the default reader
|
|
245
|
+
* kills the auth process. */
|
|
246
|
+
readAuthFile?: (signal: AbortSignal) => Promise<string | null>;
|
|
247
|
+
/** Dashboard fetch. Tests inject this. Production uses the host's fetch. */
|
|
248
|
+
fetchImpl?: typeof fetch;
|
|
249
|
+
/** Whole-probe bound, including the auth.json read. Tests shrink it. */
|
|
250
|
+
probeTimeoutMs?: number;
|
|
194
251
|
}) {
|
|
195
252
|
this.agentId = args.agent.id;
|
|
196
253
|
this.environment = args.environment;
|
|
@@ -205,6 +262,68 @@ export class CursorSession implements AgentSession {
|
|
|
205
262
|
// key is usually absent anyway.
|
|
206
263
|
this.apiKey =
|
|
207
264
|
args.apiKey ?? this.agentEnv.CURSOR_API_KEY ?? process.env.CURSOR_API_KEY ?? "";
|
|
265
|
+
this.readAuthFile =
|
|
266
|
+
args.readAuthFile ?? ((signal) => readCursorAuthFromEnvironment(this.environment, signal));
|
|
267
|
+
this.fetchImpl = args.fetchImpl ?? fetch;
|
|
268
|
+
this.probeTimeoutMs = args.probeTimeoutMs ?? CURSOR_PERIOD_PROBE_TIMEOUT_MS;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* One dashboard probe for a leased account, overlapped with the turn. A
|
|
273
|
+
* missing lease, a probe still inside the five-minute gate, or any failure
|
|
274
|
+
* sends nothing. The token never leaves this method except as the bearer
|
|
275
|
+
* of the two dashboard calls.
|
|
276
|
+
*
|
|
277
|
+
* The credential is marked before the first await. Two sessions that start
|
|
278
|
+
* together must not both pass the gate.
|
|
279
|
+
*/
|
|
280
|
+
private probePeriodUsage(): Promise<void> {
|
|
281
|
+
const credentialId = this.agentEnv[ENGINE_ACCOUNT_ENV];
|
|
282
|
+
if (!credentialId || this.closed) return Promise.resolve();
|
|
283
|
+
const now = Date.now();
|
|
284
|
+
if (!cursorPeriodProbeDue(credentialId, now)) return Promise.resolve();
|
|
285
|
+
markCursorPeriodProbed(credentialId, now);
|
|
286
|
+
return this.readPeriodUsage();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Auth read and both dashboard calls share one deadline. On expiry the
|
|
290
|
+
* auth reader is aborted (the default reader kills its process) and this
|
|
291
|
+
* promise settles so turn_complete is not held. */
|
|
292
|
+
private readPeriodUsage(): Promise<void> {
|
|
293
|
+
const controller = new AbortController();
|
|
294
|
+
const timer = setTimeout(() => controller.abort(), this.probeTimeoutMs);
|
|
295
|
+
let finished = false;
|
|
296
|
+
const usagePromise = this.collectPeriodUsage(controller.signal);
|
|
297
|
+
return new Promise((resolve) => {
|
|
298
|
+
const finish = (usage: AgentAccountUsage | null): void => {
|
|
299
|
+
if (finished) return;
|
|
300
|
+
finished = true;
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
if (!this.closed) {
|
|
303
|
+
if (!usage) console.warn(CURSOR_PERIOD_USAGE_LOG);
|
|
304
|
+
else this.emit({ type: "account_usage", usage });
|
|
305
|
+
}
|
|
306
|
+
resolve();
|
|
307
|
+
};
|
|
308
|
+
controller.signal.addEventListener("abort", () => finish(null));
|
|
309
|
+
void usagePromise.then(
|
|
310
|
+
(usage) => finish(controller.signal.aborted ? null : usage),
|
|
311
|
+
() => finish(null),
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private async collectPeriodUsage(signal: AbortSignal): Promise<AgentAccountUsage | null> {
|
|
317
|
+
if (signal.aborted) return null;
|
|
318
|
+
const raw = await this.readAuthFile(signal);
|
|
319
|
+
if (signal.aborted) return null;
|
|
320
|
+
const token = raw ? cursorAccessTokenFromAuthJson(raw) : null;
|
|
321
|
+
if (!token) return null;
|
|
322
|
+
return readCursorPeriodUsage({
|
|
323
|
+
token,
|
|
324
|
+
fetchImpl: this.fetchImpl,
|
|
325
|
+
signal,
|
|
326
|
+
});
|
|
208
327
|
}
|
|
209
328
|
|
|
210
329
|
onEvent(handler: AgentEventHandler): () => void {
|
|
@@ -227,6 +346,7 @@ export class CursorSession implements AgentSession {
|
|
|
227
346
|
private buildRequest(prompt: string): TaskEnvironmentSessionRequest {
|
|
228
347
|
const argv = [
|
|
229
348
|
CURSOR_BIN,
|
|
349
|
+
"--disable-auto-update",
|
|
230
350
|
"-p",
|
|
231
351
|
prompt,
|
|
232
352
|
"--output-format",
|
|
@@ -236,7 +356,9 @@ export class CursorSession implements AgentSession {
|
|
|
236
356
|
"--trust", // skip the workspace-trust prompt in headless
|
|
237
357
|
"--approve-mcps", // load the gateway MCP servers from ~/.cursor/mcp.json (ADR-057)
|
|
238
358
|
];
|
|
239
|
-
|
|
359
|
+
// Long form only: the pinned CLI (2026.08.25) and its successors have no
|
|
360
|
+
// `-m` short flag and exit on it before the turn starts.
|
|
361
|
+
if (this.model && this.model !== "auto") argv.push("--model", this.model);
|
|
240
362
|
if (this.sessionId) argv.push("--resume", this.sessionId);
|
|
241
363
|
return {
|
|
242
364
|
argv: argv as [string, ...string[]],
|
|
@@ -266,6 +388,7 @@ export class CursorSession implements AgentSession {
|
|
|
266
388
|
|
|
267
389
|
const child = this.spawner(this.buildRequest(prompt));
|
|
268
390
|
this.current = child;
|
|
391
|
+
const periodUsage = this.probePeriodUsage();
|
|
269
392
|
|
|
270
393
|
let acc = "";
|
|
271
394
|
let sawText = false;
|
|
@@ -273,6 +396,13 @@ export class CursorSession implements AgentSession {
|
|
|
273
396
|
// Usage rides the result line but turn_complete fires on process exit;
|
|
274
397
|
// capture it here and attach it below (ADR-071 metering).
|
|
275
398
|
let turnUsage: AgentUsage | undefined;
|
|
399
|
+
// A result line that already explained the failure owns the error. The
|
|
400
|
+
// exit path must not add a second "cursor exited N" for the same turn.
|
|
401
|
+
// A usage cap is tracked on its own: a generic JSON error must not hide
|
|
402
|
+
// a cap that only shows up on stderr.
|
|
403
|
+
let sawError = false;
|
|
404
|
+
let reportedRateLimit = false;
|
|
405
|
+
let reportedCreditRefusal = false;
|
|
276
406
|
const consume = (chunk: string): void => {
|
|
277
407
|
buf += chunk;
|
|
278
408
|
let nl: number;
|
|
@@ -318,7 +448,24 @@ export class CursorSession implements AgentSession {
|
|
|
318
448
|
}
|
|
319
449
|
if (m.end) {
|
|
320
450
|
if (m.usage) turnUsage = m.usage;
|
|
321
|
-
if (m.errorText)
|
|
451
|
+
if (m.errorText) {
|
|
452
|
+
sawError = true;
|
|
453
|
+
if (m.creditRefusal) reportedCreditRefusal = true;
|
|
454
|
+
if (m.accountUsage) {
|
|
455
|
+
reportedRateLimit = true;
|
|
456
|
+
this.emit({ type: "account_usage", usage: m.accountUsage });
|
|
457
|
+
this.emit({
|
|
458
|
+
type: "error",
|
|
459
|
+
message: m.errorText,
|
|
460
|
+
retryable: "rate_limit",
|
|
461
|
+
...(m.accountUsage.resetsAt === undefined
|
|
462
|
+
? {}
|
|
463
|
+
: { resetsAt: m.accountUsage.resetsAt }),
|
|
464
|
+
});
|
|
465
|
+
} else {
|
|
466
|
+
this.emit({ type: "error", message: m.errorText });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
322
469
|
const finalText = m.finalText ?? acc;
|
|
323
470
|
if (sawText || finalText) {
|
|
324
471
|
this.emit({ type: "message_complete", text: finalText });
|
|
@@ -340,18 +487,41 @@ export class CursorSession implements AgentSession {
|
|
|
340
487
|
if (spawnErr) {
|
|
341
488
|
this.emit({ type: "error", message: `cursor spawn failed: ${spawnErr}` });
|
|
342
489
|
} else if (code !== 0) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
490
|
+
// Some cursor-agent errors (e.g. an invalid key) are coloured; strip
|
|
491
|
+
// the escape codes so the cap check and the error a human sees are
|
|
492
|
+
// plain text. A generic result error must not skip this check.
|
|
493
|
+
const plain = stripVTControlCharacters(stderr);
|
|
494
|
+
const tail = plain.trim().slice(-500);
|
|
495
|
+
const creditRefusal = cursorCreditsRequiredMessage(plain);
|
|
496
|
+
const message =
|
|
497
|
+
creditRefusal ??
|
|
498
|
+
`cursor exited ${code ?? "null"}${tail ? `: ${tail}` : ""}`;
|
|
499
|
+
const accountUsage = reportedRateLimit ? undefined : rateLimitErrorUsage(plain);
|
|
500
|
+
if (accountUsage) {
|
|
501
|
+
this.emit({ type: "account_usage", usage: accountUsage });
|
|
502
|
+
}
|
|
503
|
+
if (!sawError || (creditRefusal && !reportedCreditRefusal)) {
|
|
504
|
+
if (accountUsage) {
|
|
505
|
+
this.emit({
|
|
506
|
+
type: "error",
|
|
507
|
+
message,
|
|
508
|
+
retryable: "rate_limit",
|
|
509
|
+
...(accountUsage.resetsAt === undefined
|
|
510
|
+
? {}
|
|
511
|
+
: { resetsAt: accountUsage.resetsAt }),
|
|
512
|
+
});
|
|
513
|
+
} else {
|
|
514
|
+
this.emit({ type: "error", message });
|
|
515
|
+
}
|
|
516
|
+
}
|
|
348
517
|
}
|
|
349
|
-
this.emit({ type: "turn_complete", usage: turnUsage });
|
|
350
518
|
resolve();
|
|
351
519
|
};
|
|
352
520
|
child.on("exit", (code) => finish(code));
|
|
353
521
|
child.on("error", (err) => finish(null, err.message));
|
|
354
522
|
});
|
|
523
|
+
await periodUsage;
|
|
524
|
+
if (!this.closed) this.emit({ type: "turn_complete", usage: turnUsage });
|
|
355
525
|
}
|
|
356
526
|
|
|
357
527
|
async interrupt(): Promise<void> {
|
package/lib/agents/grok.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
spawnEnvironmentProcess,
|
|
45
45
|
type AgentEnvironmentProcess,
|
|
46
46
|
} from "./proc";
|
|
47
|
+
import { rateLimitErrorUsage } from "./rate-limit";
|
|
47
48
|
import { register } from "./registry";
|
|
48
49
|
import { GROK_MODELS } from "../../src/engine-models";
|
|
49
50
|
import type {
|
|
@@ -58,10 +59,9 @@ import type {
|
|
|
58
59
|
const GROK_BIN = "/home/node/.grok/bin/grok";
|
|
59
60
|
export const GROK_LOADER_LOGIN_ENV = "0";
|
|
60
61
|
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
const GROK_DEFAULT_MODEL = "grok-4.5";
|
|
62
|
+
// Static fallback catalog verified against `grok models`; keep the shared list
|
|
63
|
+
// in host-agent/src/engine-models.ts (GROK_MODELS) current with the pinned CLI.
|
|
64
|
+
const GROK_DEFAULT_MODEL = "grok-4.7";
|
|
65
65
|
// Grok has no per-invocation reasoning-effort flag.
|
|
66
66
|
const GROK_EFFORTS: string[] = [];
|
|
67
67
|
|
|
@@ -313,10 +313,23 @@ export class GrokSession implements AgentSession {
|
|
|
313
313
|
this.emit({ type: "error", message: `grok spawn failed: ${spawnErr}` });
|
|
314
314
|
} else if (code !== 0) {
|
|
315
315
|
const tail = stderr.trim().slice(-500);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
316
|
+
const message = `grok exited ${code ?? "null"}${tail ? `: ${tail}` : ""}`;
|
|
317
|
+
// Same path as Claude/Codex/OpenCode: a usage-cap / 429 error emits
|
|
318
|
+
// account_usage (source error-text) so the orchestrator can rotate.
|
|
319
|
+
const accountUsage = rateLimitErrorUsage(message);
|
|
320
|
+
if (accountUsage) {
|
|
321
|
+
this.emit({ type: "account_usage", usage: accountUsage });
|
|
322
|
+
this.emit({
|
|
323
|
+
type: "error",
|
|
324
|
+
message,
|
|
325
|
+
retryable: "rate_limit",
|
|
326
|
+
...(accountUsage.resetsAt === undefined
|
|
327
|
+
? {}
|
|
328
|
+
: { resetsAt: accountUsage.resetsAt }),
|
|
329
|
+
});
|
|
330
|
+
} else {
|
|
331
|
+
this.emit({ type: "error", message });
|
|
332
|
+
}
|
|
320
333
|
}
|
|
321
334
|
// Finalize the streamed message (no-op text if the turn produced none).
|
|
322
335
|
if (sawText) this.emit({ type: "message_complete", text: acc });
|
package/lib/agents/registry.ts
CHANGED
|
@@ -144,25 +144,25 @@ export function capabilities(): AgentKindCapability[] {
|
|
|
144
144
|
: (adapter.available?.() ?? true) || advertisementFallback(adapter.kind),
|
|
145
145
|
)
|
|
146
146
|
.map((adapter) => {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
147
|
+
const out: AgentKindCapability = {
|
|
148
|
+
kind: adapter.kind,
|
|
149
|
+
label: adapter.label,
|
|
150
|
+
supportedModels: adapter.supportedModels(),
|
|
151
|
+
supportedEfforts: adapter.supportedEfforts(),
|
|
152
|
+
};
|
|
153
|
+
if (adapter.defaultModel !== undefined) {
|
|
154
|
+
out.defaultModel = adapter.defaultModel;
|
|
155
|
+
}
|
|
156
|
+
if (adapter.defaultEffort !== undefined) {
|
|
157
|
+
out.defaultEffort = adapter.defaultEffort;
|
|
158
|
+
}
|
|
159
|
+
if (adapter.executionProfiles && adapter.executionProfiles.length > 0) {
|
|
160
|
+
out.executionProfiles = adapter.executionProfiles.map((profile) => ({
|
|
161
|
+
...profile,
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
});
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
package/lib/agents/types.ts
CHANGED
|
@@ -124,7 +124,7 @@ export interface AgentAccountUsage {
|
|
|
124
124
|
resetsAt?: number;
|
|
125
125
|
usingOverage?: boolean;
|
|
126
126
|
windows: EngineUsageWindow[];
|
|
127
|
-
source: "claude-rate-limit-event" | "codex-rate-limits" | "error-text";
|
|
127
|
+
source: "claude-rate-limit-event" | "codex-rate-limits" | "error-text" | "cursor-period-usage";
|
|
128
128
|
observedAt: number;
|
|
129
129
|
/** Which provider capacity this reading can prove usable again. Claude emits
|
|
130
130
|
* one event per named window; Codex snapshots cover every window. Internal
|
package/lib/agents/usage.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared token/cost extraction for
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Shared token/cost extraction for a stream-json `result` envelope (ADR-071
|
|
3
|
+
* metering). Claude Code reports snake_case `usage` plus `total_cost_usd` and
|
|
4
|
+
* per-model `modelUsage`. cursor-agent 2026.10.x reports camelCase counts and
|
|
5
|
+
* no cost: `{inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens}`.
|
|
6
|
+
* Snake_case wins when a field is present, so Claude's path is unchanged.
|
|
6
7
|
*
|
|
7
8
|
* Codex (app-server protocol) and Kimi/Grok (plain text streams) report no
|
|
8
|
-
* token/cost data
|
|
9
|
+
* token/cost data through this extractor.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import type { AgentUsage } from "./types";
|
|
@@ -18,6 +19,12 @@ function num(v: unknown): number | undefined {
|
|
|
18
19
|
return typeof v === "number" && Number.isFinite(v) ? v : undefined;
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/** Claude's name when it is a number, otherwise Cursor 2026.10's. */
|
|
23
|
+
function firstNum(claude: unknown, cursor: unknown): number | undefined {
|
|
24
|
+
const primary = num(claude);
|
|
25
|
+
return primary !== undefined ? primary : num(cursor);
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
/** Pull AgentUsage from a `result` line, or undefined when none is reported. */
|
|
22
29
|
export function extractResultUsage(
|
|
23
30
|
json: Record<string, unknown>,
|
|
@@ -26,10 +33,10 @@ export function extractResultUsage(
|
|
|
26
33
|
const models = isObj(json.modelUsage) ? Object.keys(json.modelUsage) : [];
|
|
27
34
|
const usage: AgentUsage = {
|
|
28
35
|
model: models.length ? models.join(", ") : undefined,
|
|
29
|
-
inputTokens:
|
|
30
|
-
outputTokens:
|
|
31
|
-
cacheReadTokens:
|
|
32
|
-
cacheCreateTokens:
|
|
36
|
+
inputTokens: firstNum(u.input_tokens, u.inputTokens),
|
|
37
|
+
outputTokens: firstNum(u.output_tokens, u.outputTokens),
|
|
38
|
+
cacheReadTokens: firstNum(u.cache_read_input_tokens, u.cacheReadTokens),
|
|
39
|
+
cacheCreateTokens: firstNum(u.cache_creation_input_tokens, u.cacheWriteTokens),
|
|
33
40
|
costUsd: num(json.total_cost_usd),
|
|
34
41
|
};
|
|
35
42
|
// Nothing usable reported → omit rather than send an empty object.
|
package/lib/orchestrator.ts
CHANGED
|
@@ -5409,12 +5409,26 @@ export class Orchestrator {
|
|
|
5409
5409
|
// ADR-076: a turn that completed means the current account is healthy —
|
|
5410
5410
|
// reset its rotation budget so a LATER rate-limit gets a fresh failover.
|
|
5411
5411
|
channel.rotations.delete(agentId);
|
|
5412
|
+
// Name the leased AI account on the turn usage report so the cloud
|
|
5413
|
+
// ledger can roll up observed spend per credential (host 0.9.167+).
|
|
5414
|
+
// Only a cloud-leased account identity is reportable; runner-default
|
|
5415
|
+
// and Managed Engine identities stay unnamed (same rule as account_usage).
|
|
5416
|
+
const leasedIdentity = channel.leasedIdentityByAgent.get(agentId);
|
|
5417
|
+
const usage =
|
|
5418
|
+
event.usage === undefined
|
|
5419
|
+
? undefined
|
|
5420
|
+
: {
|
|
5421
|
+
...event.usage,
|
|
5422
|
+
...(leasedIdentity?.subject === "account"
|
|
5423
|
+
? { credentialId: leasedIdentity.credentialId }
|
|
5424
|
+
: {}),
|
|
5425
|
+
};
|
|
5412
5426
|
this.emitHost({
|
|
5413
5427
|
kind: "agent.turn_complete",
|
|
5414
5428
|
taskId: channel.taskId,
|
|
5415
5429
|
agentId,
|
|
5416
5430
|
aborted,
|
|
5417
|
-
usage
|
|
5431
|
+
usage,
|
|
5418
5432
|
});
|
|
5419
5433
|
if (remainingTurns === 0) {
|
|
5420
5434
|
channel.lastPrompt.delete(agentId);
|
|
@@ -5487,7 +5501,12 @@ export class Orchestrator {
|
|
|
5487
5501
|
/** An engine error can race a live launch edit after a partial turn. Keep
|
|
5488
5502
|
* the obsolete adapter owned until the stale recycler closes it, but close
|
|
5489
5503
|
* the cloud turn boundary immediately so restarting/ready cannot leave the
|
|
5490
|
-
* crew UI stuck in a working state.
|
|
5504
|
+
* crew UI stuck in a working state.
|
|
5505
|
+
*
|
|
5506
|
+
* The error is still reported, as the exit notice every other engine error
|
|
5507
|
+
* sends: whatever the agent was doing is lost, and without it nobody (the
|
|
5508
|
+
* human, or a Secretary waiting on the agent) was told why. Sent before the
|
|
5509
|
+
* reconcile below, so the cloud sees it ahead of the respawn's `restarting`. */
|
|
5491
5510
|
private reconcileErroredRosterChange(
|
|
5492
5511
|
channel: Channel,
|
|
5493
5512
|
agentId: string,
|
|
@@ -5501,6 +5520,12 @@ export class Orchestrator {
|
|
|
5501
5520
|
if (change) this.publishRosterChangeAbort(channel, agentId, change);
|
|
5502
5521
|
else this.emitRetiredTurnAbort(channel, agentId);
|
|
5503
5522
|
}
|
|
5523
|
+
this.emitHost({
|
|
5524
|
+
kind: "agent.exit",
|
|
5525
|
+
taskId: channel.taskId,
|
|
5526
|
+
agentId,
|
|
5527
|
+
reason,
|
|
5528
|
+
});
|
|
5504
5529
|
void this.reconcileAfterRosterRefresh(channel).catch((err: unknown) => {
|
|
5505
5530
|
console.warn(
|
|
5506
5531
|
`[orchestrator] ${channel.taskId}/${agentId}: roster-change error (${reason}) reconcile failed: ${
|
package/lib/standard-image.ts
CHANGED
|
@@ -67,7 +67,7 @@ export const ASDF_DATA_VOLUME = "uai-asdf-data";
|
|
|
67
67
|
export const STANDARD_DEFAULT_NODE_VERSION = "22.11.0";
|
|
68
68
|
|
|
69
69
|
/** Exact Codex CLI release exposed to task runtimes through the shared volume. */
|
|
70
|
-
export const STANDARD_CODEX_CLI_VERSION = "0.
|
|
70
|
+
export const STANDARD_CODEX_CLI_VERSION = "0.164.0";
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* Runtimes the standard image's asdf plugins can satisfy, with a sensible set
|
package/package.json
CHANGED
package/src/engine-models.ts
CHANGED
|
@@ -5,20 +5,21 @@
|
|
|
5
5
|
* here has no guide entry. Pure data: no runtime imports.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
// Claude Code's aliases, which follow its own model table (2.1.
|
|
8
|
+
// Claude Code's aliases, which follow its own model table (2.1.310:
|
|
9
9
|
// fable = Claude Fable 5.1, opus = Opus 5.5, sonnet = Sonnet 5.5,
|
|
10
10
|
// haiku = Haiku 5.5). Update alongside the CLI.
|
|
11
11
|
export const CLAUDE_MODELS: readonly string[] = ["fable", "opus", "sonnet", "haiku"];
|
|
12
12
|
|
|
13
|
-
// Codex model slugs accepted via `-c model=<slug>`.
|
|
14
|
-
//
|
|
15
|
-
// interactive `codex` model picker (and `codex -m <slug>` / config.toml).
|
|
16
|
-
// Sourced from that picker; UPDATE WHEN MODELS CHANGE. Order = display order
|
|
17
|
-
// in the cloud picker. Legacy models remain reachable via config.toml.
|
|
13
|
+
// Codex model slugs accepted via `-c model=<slug>`. Keep the newest version of
|
|
14
|
+
// each tier first; older models remain available at the end.
|
|
18
15
|
export const CODEX_MODELS: readonly string[] = [
|
|
19
16
|
"gpt-6-astra",
|
|
20
|
-
"gpt-
|
|
17
|
+
"gpt-6.1-sol",
|
|
18
|
+
// OpenAI has not released a GPT-6 Terra; 5.6 remains the balanced tier.
|
|
21
19
|
"gpt-5.6-terra",
|
|
20
|
+
"gpt-6-luna",
|
|
21
|
+
"gpt-6-sol",
|
|
22
|
+
"gpt-5.6-sol",
|
|
22
23
|
"gpt-5.6-luna",
|
|
23
24
|
"gpt-5.5",
|
|
24
25
|
"gpt-5.4",
|
|
@@ -28,4 +29,11 @@ export const CODEX_MODELS: readonly string[] = [
|
|
|
28
29
|
"gpt-5.2",
|
|
29
30
|
];
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
// Grok model slugs accepted via `-m <slug>`. Keep the current models
|
|
33
|
+
// first and older working models at the end.
|
|
34
|
+
export const GROK_MODELS: readonly string[] = [
|
|
35
|
+
"grok-4.7",
|
|
36
|
+
"grok-4.7-build-fast",
|
|
37
|
+
"grok-4.6",
|
|
38
|
+
"grok-4.5",
|
|
39
|
+
];
|
package/src/protocol.ts
CHANGED
|
@@ -212,7 +212,7 @@ export interface EngineUsageFrame {
|
|
|
212
212
|
usingOverage?: boolean;
|
|
213
213
|
windows: EngineUsageWindow[];
|
|
214
214
|
/** What the report was read from. */
|
|
215
|
-
source: "claude-rate-limit-event" | "codex-rate-limits" | "error-text";
|
|
215
|
+
source: "claude-rate-limit-event" | "codex-rate-limits" | "error-text" | "cursor-period-usage";
|
|
216
216
|
/** When the engine said it, epoch ms (host clock). The cloud keeps the
|
|
217
217
|
* newest per account, so a late report never overwrites a newer one. */
|
|
218
218
|
observedAt: number;
|
|
@@ -5059,7 +5059,9 @@ export type HostEvent =
|
|
|
5059
5059
|
agentId: string;
|
|
5060
5060
|
aborted?: boolean;
|
|
5061
5061
|
/** Token/cost accounting for the turn, when the engine reports it
|
|
5062
|
-
* (ADR-071 usage metering — powers per-task cost + managed AI billing).
|
|
5062
|
+
* (ADR-071 usage metering — powers per-task cost + managed AI billing).
|
|
5063
|
+
* `credentialId` is the leased AI account the turn ran on (host
|
|
5064
|
+
* 0.9.167+); older runners omit it and the cloud stores null. */
|
|
5063
5065
|
usage?: {
|
|
5064
5066
|
model?: string;
|
|
5065
5067
|
inputTokens?: number;
|
|
@@ -5067,6 +5069,7 @@ export type HostEvent =
|
|
|
5067
5069
|
cacheReadTokens?: number;
|
|
5068
5070
|
cacheCreateTokens?: number;
|
|
5069
5071
|
costUsd?: number;
|
|
5072
|
+
credentialId?: string;
|
|
5070
5073
|
};
|
|
5071
5074
|
}
|
|
5072
5075
|
/**
|