remote-codex 0.11.31 → 0.11.33
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/README.md +12 -4
- package/apps/relay-server/dist/index.js +2956 -164
- package/apps/supervisor-api/dist/index.js +497 -54
- package/apps/supervisor-web/dist/assets/index-CdjTdnJt.css +1 -0
- package/apps/supervisor-web/dist/assets/index-DIi8wTEY.js +21 -0
- package/apps/supervisor-web/dist/assets/thread-ui-B9eC2H4u.js +3677 -0
- package/apps/supervisor-web/dist/index.html +3 -3
- package/bin/remote-codex.mjs +6 -3
- package/package.json +1 -1
- package/packages/agent-runtime/src/types.ts +13 -0
- package/packages/claude/src/runtimeAdapter.test.ts +121 -1
- package/packages/claude/src/runtimeAdapter.ts +131 -0
- package/packages/codex/src/appServerManager.ts +16 -0
- package/packages/codex/src/runtimeAdapter.test.ts +44 -0
- package/packages/codex/src/runtimeAdapter.ts +66 -0
- package/packages/db/migrations/0029_thread_turn_delivery.sql +19 -0
- package/packages/db/src/repositories.ts +96 -1
- package/packages/db/src/schema.ts +20 -0
- package/packages/shared/src/index.ts +199 -6
- package/scripts/run-web-service.mjs +2 -2
- package/scripts/service-manager.mjs +2 -2
- package/apps/supervisor-web/dist/assets/index-BkwJP0KB.js +0 -6
- package/apps/supervisor-web/dist/assets/index-CJFMmjP5.css +0 -1
- package/apps/supervisor-web/dist/assets/thread-ui-BOqbjoiB.js +0 -3677
|
@@ -10,16 +10,16 @@
|
|
|
10
10
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
|
11
11
|
<link rel="manifest" href="/site.webmanifest" />
|
|
12
12
|
<title>Remote Codex</title>
|
|
13
|
-
<script type="module" crossorigin src="/assets/index-
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-DIi8wTEY.js"></script>
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/react-vendor-Dfg_6BLf.js">
|
|
15
15
|
<link rel="modulepreload" crossorigin href="/assets/ui-vendor-CuR8GHb0.js">
|
|
16
16
|
<link rel="modulepreload" crossorigin href="/assets/graph-vendor-DVQUpZ8C.js">
|
|
17
17
|
<link rel="modulepreload" crossorigin href="/assets/terminal-vendor-C5bTa-Ka.js">
|
|
18
18
|
<link rel="modulepreload" crossorigin href="/assets/markdown-vendor-RZk8L7-L.js">
|
|
19
|
-
<link rel="modulepreload" crossorigin href="/assets/thread-ui-
|
|
19
|
+
<link rel="modulepreload" crossorigin href="/assets/thread-ui-B9eC2H4u.js">
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/graph-vendor-C5ap-Sga.css">
|
|
21
21
|
<link rel="stylesheet" crossorigin href="/assets/terminal-vendor-Beg8tuEN.css">
|
|
22
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
22
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CdjTdnJt.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body class="bg-stone-950">
|
|
25
25
|
<div id="root"></div>
|
package/bin/remote-codex.mjs
CHANGED
|
@@ -851,9 +851,9 @@ What it starts:
|
|
|
851
851
|
API http://SERVICE_API_HOST:SERVICE_API_PORT
|
|
852
852
|
|
|
853
853
|
Environment:
|
|
854
|
-
SERVICE_HOST Web listen host. Default
|
|
854
|
+
SERVICE_HOST Web listen host. Default 0.0.0.0.
|
|
855
855
|
SERVICE_PORT Web listen port. Default ${defaultServicePort}.
|
|
856
|
-
SERVICE_API_HOST API listen host. Default
|
|
856
|
+
SERVICE_API_HOST API listen host. Default 0.0.0.0.
|
|
857
857
|
SERVICE_API_PORT API listen port. Default ${defaultApiPort}.
|
|
858
858
|
REMOTE_CODEX_SERVICE_DIR Service state/log directory.
|
|
859
859
|
LOG_LEVEL API log level. Default warn for service mode.
|
|
@@ -871,7 +871,10 @@ Supervisor configuration forwarded to the API:
|
|
|
871
871
|
OPENCODE_COMMAND OpenCode executable. Default opencode.
|
|
872
872
|
|
|
873
873
|
Example:
|
|
874
|
-
|
|
874
|
+
remote-codex start
|
|
875
|
+
|
|
876
|
+
Restrict the service to this machine:
|
|
877
|
+
SERVICE_HOST=127.0.0.1 SERVICE_API_HOST=127.0.0.1 remote-codex start
|
|
875
878
|
|
|
876
879
|
Expose over Tailscale after start:
|
|
877
880
|
tailscale serve --bg http://127.0.0.1:${defaultServicePort}
|
package/package.json
CHANGED
|
@@ -582,4 +582,17 @@ export interface AgentRuntime extends EventEmitter {
|
|
|
582
582
|
getGoal?(providerSessionId: string): Promise<AgentGoal | null>;
|
|
583
583
|
setGoal?(input: SetAgentGoalInput): Promise<AgentGoal>;
|
|
584
584
|
clearGoal?(providerSessionId: string): Promise<boolean>;
|
|
585
|
+
getSubscriptionUsage?(): Promise<{
|
|
586
|
+
provider: 'codex' | 'claude';
|
|
587
|
+
authKind: 'subscription' | 'apiKey' | 'unknown';
|
|
588
|
+
observedAt: string;
|
|
589
|
+
stale: boolean;
|
|
590
|
+
windows: Array<{
|
|
591
|
+
id: string;
|
|
592
|
+
durationMinutes: number | null;
|
|
593
|
+
label: string;
|
|
594
|
+
usedPercent: number;
|
|
595
|
+
resetsAt: string | null;
|
|
596
|
+
}>;
|
|
597
|
+
} | null>;
|
|
585
598
|
}
|
|
@@ -15,6 +15,7 @@ interface Query extends AsyncIterable<SDKMessage> {
|
|
|
15
15
|
interrupt(): Promise<void>;
|
|
16
16
|
supportedModels(): Promise<any[]>;
|
|
17
17
|
mcpServerStatus(): Promise<any[]>;
|
|
18
|
+
usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET(): Promise<any>;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
function wait(ms = 0) {
|
|
@@ -29,7 +30,7 @@ class FakeQuery implements Query {
|
|
|
29
30
|
|
|
30
31
|
constructor(
|
|
31
32
|
private readonly messages: SDKMessage[],
|
|
32
|
-
private readonly options: { holdOpen?: boolean } = {},
|
|
33
|
+
private readonly options: { holdOpen?: boolean; usage?: Record<string, any> } = {},
|
|
33
34
|
) {}
|
|
34
35
|
|
|
35
36
|
[Symbol.asyncIterator]() {
|
|
@@ -104,6 +105,12 @@ class FakeQuery implements Query {
|
|
|
104
105
|
async getContextUsage(): Promise<any> {
|
|
105
106
|
return {};
|
|
106
107
|
}
|
|
108
|
+
async usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET(): Promise<any> {
|
|
109
|
+
if (!this.options.usage) {
|
|
110
|
+
throw new Error('Structured usage unavailable in this fixture.');
|
|
111
|
+
}
|
|
112
|
+
return this.options.usage;
|
|
113
|
+
}
|
|
107
114
|
async readFile(): Promise<any> {
|
|
108
115
|
return {};
|
|
109
116
|
}
|
|
@@ -208,6 +215,119 @@ function makeAdapter(
|
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
describe('ClaudeRuntimeAdapter', () => {
|
|
218
|
+
it('actively reads Claude subscription windows from the structured usage API', async () => {
|
|
219
|
+
const adapter = makeAdapter(() => new FakeQuery(
|
|
220
|
+
[systemInit(), result()],
|
|
221
|
+
{
|
|
222
|
+
usage: {
|
|
223
|
+
subscription_type: 'max',
|
|
224
|
+
rate_limits_available: true,
|
|
225
|
+
rate_limits: {
|
|
226
|
+
five_hour: {
|
|
227
|
+
utilization: 23,
|
|
228
|
+
resets_at: '2027-01-15T08:00:00.000Z',
|
|
229
|
+
},
|
|
230
|
+
seven_day: {
|
|
231
|
+
utilization: 84,
|
|
232
|
+
resets_at: '2027-01-20T08:00:00.000Z',
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
));
|
|
238
|
+
|
|
239
|
+
await adapter.start();
|
|
240
|
+
await adapter.startSession({
|
|
241
|
+
cwd: '/tmp/workspace',
|
|
242
|
+
model: 'sonnet',
|
|
243
|
+
approvalMode: 'guarded',
|
|
244
|
+
sandboxMode: 'workspace-write',
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
|
|
248
|
+
provider: 'claude',
|
|
249
|
+
authKind: 'subscription',
|
|
250
|
+
windows: [
|
|
251
|
+
{
|
|
252
|
+
id: 'five_hour',
|
|
253
|
+
label: '5h',
|
|
254
|
+
usedPercent: 23,
|
|
255
|
+
resetsAt: '2027-01-15T08:00:00.000Z',
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: 'seven_day',
|
|
259
|
+
label: '7d',
|
|
260
|
+
usedPercent: 84,
|
|
261
|
+
resetsAt: '2027-01-20T08:00:00.000Z',
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('captures Claude subscription rate-limit windows from SDK events', async () => {
|
|
268
|
+
const adapter = makeAdapter(() => [
|
|
269
|
+
systemInit(),
|
|
270
|
+
{
|
|
271
|
+
type: 'rate_limit_event',
|
|
272
|
+
rate_limit_info: {
|
|
273
|
+
status: 'allowed',
|
|
274
|
+
rateLimitType: 'five_hour',
|
|
275
|
+
utilization: 0.23,
|
|
276
|
+
resetsAt: 1_800_000_000,
|
|
277
|
+
},
|
|
278
|
+
uuid: '00000000-0000-4000-8000-000000000003',
|
|
279
|
+
session_id: 'claude-session-1',
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
type: 'rate_limit_event',
|
|
283
|
+
rate_limit_info: {
|
|
284
|
+
status: 'allowed_warning',
|
|
285
|
+
rateLimitType: 'seven_day',
|
|
286
|
+
utilization: 0.84,
|
|
287
|
+
resetsAt: 1_800_086_400,
|
|
288
|
+
},
|
|
289
|
+
uuid: '00000000-0000-4000-8000-000000000004',
|
|
290
|
+
session_id: 'claude-session-1',
|
|
291
|
+
},
|
|
292
|
+
result(),
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
await adapter.start();
|
|
296
|
+
await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
|
|
297
|
+
provider: 'claude',
|
|
298
|
+
authKind: 'unknown',
|
|
299
|
+
windows: [],
|
|
300
|
+
});
|
|
301
|
+
await adapter.startSession({
|
|
302
|
+
cwd: '/tmp/workspace',
|
|
303
|
+
model: 'sonnet',
|
|
304
|
+
approvalMode: 'guarded',
|
|
305
|
+
sandboxMode: 'workspace-write',
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
|
|
309
|
+
provider: 'claude',
|
|
310
|
+
authKind: 'subscription',
|
|
311
|
+
stale: false,
|
|
312
|
+
windows: [
|
|
313
|
+
{
|
|
314
|
+
id: 'five_hour',
|
|
315
|
+
durationMinutes: 300,
|
|
316
|
+
label: '5h',
|
|
317
|
+
usedPercent: 23,
|
|
318
|
+
resetsAt: new Date(1_800_000_000 * 1000).toISOString(),
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: 'seven_day',
|
|
322
|
+
durationMinutes: 10_080,
|
|
323
|
+
label: '7d',
|
|
324
|
+
usedPercent: 84,
|
|
325
|
+
resetsAt: new Date(1_800_086_400 * 1000).toISOString(),
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
211
331
|
it('passes the configured Claude executable to the SDK', async () => {
|
|
212
332
|
const sdkOptions: Record<string, unknown>[] = [];
|
|
213
333
|
const adapter = new ClaudeRuntimeAdapter({
|
|
@@ -99,6 +99,19 @@ interface Query extends AsyncIterable<SDKMessage> {
|
|
|
99
99
|
interrupt(): Promise<void>;
|
|
100
100
|
supportedModels(): Promise<ModelInfo[]>;
|
|
101
101
|
mcpServerStatus(): Promise<McpServerStatus[]>;
|
|
102
|
+
usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET?(): Promise<SDKUsageResponse>;
|
|
103
|
+
}
|
|
104
|
+
interface SDKUsageWindow {
|
|
105
|
+
utilization: number | null;
|
|
106
|
+
resets_at: string | null;
|
|
107
|
+
}
|
|
108
|
+
interface SDKUsageResponse {
|
|
109
|
+
subscription_type: string | null;
|
|
110
|
+
rate_limits_available: boolean;
|
|
111
|
+
rate_limits: {
|
|
112
|
+
five_hour?: SDKUsageWindow | null;
|
|
113
|
+
seven_day?: SDKUsageWindow | null;
|
|
114
|
+
} | null;
|
|
102
115
|
}
|
|
103
116
|
interface SDKMessage {
|
|
104
117
|
type: string;
|
|
@@ -119,6 +132,19 @@ interface SDKMessage {
|
|
|
119
132
|
modelUsage?: unknown;
|
|
120
133
|
errors?: string[];
|
|
121
134
|
stop_reason?: string;
|
|
135
|
+
rate_limit_info?: SDKRateLimitInfo;
|
|
136
|
+
}
|
|
137
|
+
interface SDKRateLimitInfo {
|
|
138
|
+
status: 'allowed' | 'allowed_warning' | 'rejected';
|
|
139
|
+
resetsAt?: number;
|
|
140
|
+
rateLimitType?:
|
|
141
|
+
| 'five_hour'
|
|
142
|
+
| 'seven_day'
|
|
143
|
+
| 'seven_day_opus'
|
|
144
|
+
| 'seven_day_sonnet'
|
|
145
|
+
| 'seven_day_overage_included'
|
|
146
|
+
| 'overage';
|
|
147
|
+
utilization?: number;
|
|
122
148
|
}
|
|
123
149
|
interface SDKUserMessage {
|
|
124
150
|
type: 'user';
|
|
@@ -1222,6 +1248,12 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1222
1248
|
private readonly sessionModels = new Map<string, string | null>();
|
|
1223
1249
|
private readonly sessionApprovalModes = new Map<string, StartAgentSessionInput['approvalMode']>();
|
|
1224
1250
|
private readonly liveUserPrompts = new Map<string, Map<string, string>>();
|
|
1251
|
+
private readonly subscriptionUsageWindows = new Map<
|
|
1252
|
+
'five_hour' | 'seven_day',
|
|
1253
|
+
{ usedPercent: number; resetsAt: string | null }
|
|
1254
|
+
>();
|
|
1255
|
+
private subscriptionAuthKind: 'subscription' | 'apiKey' | 'unknown' = 'unknown';
|
|
1256
|
+
private subscriptionUsageObservedAt: string | null = null;
|
|
1225
1257
|
private readonly clientApp: string;
|
|
1226
1258
|
private sdkLoadError: string | null = null;
|
|
1227
1259
|
|
|
@@ -1242,6 +1274,93 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1242
1274
|
return { ...this.status };
|
|
1243
1275
|
}
|
|
1244
1276
|
|
|
1277
|
+
async getSubscriptionUsage() {
|
|
1278
|
+
const observedAt = this.subscriptionUsageObservedAt ?? new Date().toISOString();
|
|
1279
|
+
const windows = [...this.subscriptionUsageWindows.entries()].map(([id, window]) => ({
|
|
1280
|
+
id,
|
|
1281
|
+
durationMinutes: id === 'five_hour' ? 300 : 10_080,
|
|
1282
|
+
label: id === 'five_hour' ? '5h' : '7d',
|
|
1283
|
+
usedPercent: window.usedPercent,
|
|
1284
|
+
resetsAt: window.resetsAt,
|
|
1285
|
+
}));
|
|
1286
|
+
return {
|
|
1287
|
+
provider: 'claude' as const,
|
|
1288
|
+
authKind: this.subscriptionAuthKind,
|
|
1289
|
+
observedAt,
|
|
1290
|
+
stale: false,
|
|
1291
|
+
windows,
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
private captureRateLimit(message: SDKMessage) {
|
|
1296
|
+
if (message.type !== 'rate_limit_event') {
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
const info = message.rate_limit_info;
|
|
1300
|
+
if (
|
|
1301
|
+
!info
|
|
1302
|
+
|| (info.rateLimitType !== 'five_hour' && info.rateLimitType !== 'seven_day')
|
|
1303
|
+
|| typeof info.utilization !== 'number'
|
|
1304
|
+
|| !Number.isFinite(info.utilization)
|
|
1305
|
+
) {
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
this.subscriptionUsageWindows.set(info.rateLimitType, {
|
|
1309
|
+
usedPercent: Math.max(0, Math.min(100, info.utilization * 100)),
|
|
1310
|
+
resetsAt: typeof info.resetsAt === 'number'
|
|
1311
|
+
? new Date(info.resetsAt * 1000).toISOString()
|
|
1312
|
+
: null,
|
|
1313
|
+
});
|
|
1314
|
+
this.subscriptionAuthKind = 'subscription';
|
|
1315
|
+
this.subscriptionUsageObservedAt = new Date().toISOString();
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
private async captureStructuredSubscriptionUsage(query: Query) {
|
|
1319
|
+
const getUsage = query.usage_EXPERIMENTAL_MAY_CHANGE_DO_NOT_RELY_ON_THIS_API_YET;
|
|
1320
|
+
if (!getUsage) {
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
try {
|
|
1325
|
+
const usage = await getUsage.call(query);
|
|
1326
|
+
const nextWindows = new Map<
|
|
1327
|
+
'five_hour' | 'seven_day',
|
|
1328
|
+
{ usedPercent: number; resetsAt: string | null }
|
|
1329
|
+
>();
|
|
1330
|
+
const addWindow = (
|
|
1331
|
+
id: 'five_hour' | 'seven_day',
|
|
1332
|
+
window: SDKUsageWindow | null | undefined,
|
|
1333
|
+
) => {
|
|
1334
|
+
if (
|
|
1335
|
+
typeof window?.utilization !== 'number'
|
|
1336
|
+
|| !Number.isFinite(window.utilization)
|
|
1337
|
+
) {
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
nextWindows.set(id, {
|
|
1341
|
+
usedPercent: Math.max(0, Math.min(100, window.utilization)),
|
|
1342
|
+
resetsAt: typeof window.resets_at === 'string' ? window.resets_at : null,
|
|
1343
|
+
});
|
|
1344
|
+
};
|
|
1345
|
+
addWindow('five_hour', usage.rate_limits?.five_hour);
|
|
1346
|
+
addWindow('seven_day', usage.rate_limits?.seven_day);
|
|
1347
|
+
|
|
1348
|
+
this.subscriptionUsageWindows.clear();
|
|
1349
|
+
for (const [id, window] of nextWindows) {
|
|
1350
|
+
this.subscriptionUsageWindows.set(id, window);
|
|
1351
|
+
}
|
|
1352
|
+
this.subscriptionAuthKind = usage.subscription_type
|
|
1353
|
+
? 'subscription'
|
|
1354
|
+
: usage.rate_limits_available
|
|
1355
|
+
? 'subscription'
|
|
1356
|
+
: 'apiKey';
|
|
1357
|
+
this.subscriptionUsageObservedAt = new Date().toISOString();
|
|
1358
|
+
} catch {
|
|
1359
|
+
// This SDK method is experimental. Rate-limit events remain the
|
|
1360
|
+
// compatibility fallback for Claude versions that do not expose it.
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1245
1364
|
private updateToolboxItemsFromSystemInit(message: SDKMessage) {
|
|
1246
1365
|
this.managementSchema.toolboxItems = buildClaudeToolboxItems(
|
|
1247
1366
|
normalizeClaudeSlashCommands(message.slash_commands),
|
|
@@ -1405,9 +1524,15 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1405
1524
|
let providerSessionId: string | null = null;
|
|
1406
1525
|
let model: string | null = input.model;
|
|
1407
1526
|
const rawMessages: SDKMessage[] = [];
|
|
1527
|
+
let capturedStructuredUsage = false;
|
|
1408
1528
|
try {
|
|
1409
1529
|
for await (const message of query) {
|
|
1410
1530
|
rawMessages.push(message);
|
|
1531
|
+
if (!capturedStructuredUsage) {
|
|
1532
|
+
capturedStructuredUsage = true;
|
|
1533
|
+
await this.captureStructuredSubscriptionUsage(query);
|
|
1534
|
+
}
|
|
1535
|
+
this.captureRateLimit(message);
|
|
1411
1536
|
if (message.type === 'system' && message.subtype === 'init') {
|
|
1412
1537
|
this.updateToolboxItemsFromSystemInit(message);
|
|
1413
1538
|
const sessionId = message.session_id;
|
|
@@ -1696,9 +1821,15 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
1696
1821
|
const rawMessages: SDKMessage[] = [];
|
|
1697
1822
|
let terminalStatus: AgentTurn['status'] | null = null;
|
|
1698
1823
|
let terminalError: string | null = null;
|
|
1824
|
+
let capturedStructuredUsage = false;
|
|
1699
1825
|
try {
|
|
1700
1826
|
for await (const message of state.query) {
|
|
1701
1827
|
rawMessages.push(message);
|
|
1828
|
+
if (!capturedStructuredUsage) {
|
|
1829
|
+
capturedStructuredUsage = true;
|
|
1830
|
+
await this.captureStructuredSubscriptionUsage(state.query);
|
|
1831
|
+
}
|
|
1832
|
+
this.captureRateLimit(message);
|
|
1702
1833
|
this.consumeMessage(state, message);
|
|
1703
1834
|
const status = queryResultStatus(message);
|
|
1704
1835
|
if (status) {
|
|
@@ -320,6 +320,22 @@ export class CodexAppServerManager extends EventEmitter {
|
|
|
320
320
|
return response.data.map(mapModel);
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
async readAccount() {
|
|
324
|
+
await this.ensureReady();
|
|
325
|
+
return this.client!.request<{
|
|
326
|
+
account: { type: string } | null;
|
|
327
|
+
requiresOpenaiAuth: boolean;
|
|
328
|
+
}>('account/read', { refreshToken: false });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async readAccountRateLimits() {
|
|
332
|
+
await this.ensureReady();
|
|
333
|
+
return this.client!.request<{
|
|
334
|
+
rateLimits: unknown;
|
|
335
|
+
rateLimitsByLimitId?: Record<string, unknown> | null;
|
|
336
|
+
}>('account/rateLimits/read', null);
|
|
337
|
+
}
|
|
338
|
+
|
|
323
339
|
async listThreads(): Promise<CodexThreadRecord[]> {
|
|
324
340
|
await this.ensureReady();
|
|
325
341
|
const response = await this.client!.request<{ data: any[] }>('thread/list', {
|
|
@@ -27,6 +27,22 @@ class FakeCodexManager extends EventEmitter {
|
|
|
27
27
|
items: [],
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
async readAccount() {
|
|
32
|
+
return { account: { type: 'chatgpt' }, requiresOpenaiAuth: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async readAccountRateLimits() {
|
|
36
|
+
return {
|
|
37
|
+
rateLimits: {},
|
|
38
|
+
rateLimitsByLimitId: {
|
|
39
|
+
codex: {
|
|
40
|
+
primary: { usedPercent: 40, windowDurationMins: 300, resetsAt: 1_800_000_000 },
|
|
41
|
+
secondary: { usedPercent: 75, windowDurationMins: 10_080, resetsAt: 1_800_604_800 },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
describe('CodexRuntimeAdapter', () => {
|
|
@@ -73,4 +89,32 @@ describe('CodexRuntimeAdapter', () => {
|
|
|
73
89
|
{ type: 'text', text: '.', text_elements: [] },
|
|
74
90
|
]);
|
|
75
91
|
});
|
|
92
|
+
|
|
93
|
+
it('maps ChatGPT rate-limit windows without assuming fixed durations', async () => {
|
|
94
|
+
const adapter = new CodexRuntimeAdapter(new FakeCodexManager() as never);
|
|
95
|
+
|
|
96
|
+
await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
|
|
97
|
+
provider: 'codex',
|
|
98
|
+
authKind: 'subscription',
|
|
99
|
+
stale: false,
|
|
100
|
+
windows: [
|
|
101
|
+
{ id: 'primary', label: '5h', durationMinutes: 300, usedPercent: 40 },
|
|
102
|
+
{ id: 'secondary', label: '7d', durationMinutes: 10_080, usedPercent: 75 },
|
|
103
|
+
],
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('hides subscription windows for API-key authentication', async () => {
|
|
108
|
+
const manager = new FakeCodexManager();
|
|
109
|
+
manager.readAccount = async () => ({
|
|
110
|
+
account: { type: 'apiKey' },
|
|
111
|
+
requiresOpenaiAuth: true,
|
|
112
|
+
});
|
|
113
|
+
const adapter = new CodexRuntimeAdapter(manager as never);
|
|
114
|
+
|
|
115
|
+
await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
|
|
116
|
+
authKind: 'apiKey',
|
|
117
|
+
windows: [],
|
|
118
|
+
});
|
|
119
|
+
});
|
|
76
120
|
});
|
|
@@ -484,6 +484,20 @@ function mapCodexNotification(event: CodexServerEvent): AgentRuntimeEvent | null
|
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
function formatRateLimitWindowLabel(
|
|
488
|
+
durationMinutes: number | null,
|
|
489
|
+
fallback: string,
|
|
490
|
+
) {
|
|
491
|
+
if (durationMinutes === null) return fallback;
|
|
492
|
+
if (durationMinutes % (60 * 24) === 0) {
|
|
493
|
+
return `${durationMinutes / (60 * 24)}d`;
|
|
494
|
+
}
|
|
495
|
+
if (durationMinutes % 60 === 0) {
|
|
496
|
+
return `${durationMinutes / 60}h`;
|
|
497
|
+
}
|
|
498
|
+
return `${durationMinutes}m`;
|
|
499
|
+
}
|
|
500
|
+
|
|
487
501
|
function mapCodexRuntimeError(error: unknown): never {
|
|
488
502
|
if (error instanceof AgentRuntimeError) {
|
|
489
503
|
throw error;
|
|
@@ -617,6 +631,58 @@ export class CodexRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
617
631
|
return mapStatus(this.manager.getStatus());
|
|
618
632
|
}
|
|
619
633
|
|
|
634
|
+
async getSubscriptionUsage() {
|
|
635
|
+
const account = await codexRuntimeCall(() => this.manager.readAccount());
|
|
636
|
+
if (account.account?.type === 'apiKey') {
|
|
637
|
+
return {
|
|
638
|
+
provider: 'codex' as const,
|
|
639
|
+
authKind: 'apiKey' as const,
|
|
640
|
+
observedAt: new Date().toISOString(),
|
|
641
|
+
stale: false,
|
|
642
|
+
windows: [],
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
if (account.account?.type !== 'chatgpt') {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
const response = await codexRuntimeCall(() =>
|
|
649
|
+
this.manager.readAccountRateLimits(),
|
|
650
|
+
);
|
|
651
|
+
const buckets = response.rateLimitsByLimitId;
|
|
652
|
+
const snapshot =
|
|
653
|
+
(buckets && (buckets.codex ?? Object.values(buckets)[0])) ??
|
|
654
|
+
response.rateLimits;
|
|
655
|
+
const record = snapshot && typeof snapshot === 'object'
|
|
656
|
+
? (snapshot as Record<string, unknown>)
|
|
657
|
+
: {};
|
|
658
|
+
const windows = ['primary', 'secondary'].flatMap((id) => {
|
|
659
|
+
const value = record[id];
|
|
660
|
+
if (!value || typeof value !== 'object') return [];
|
|
661
|
+
const window = value as Record<string, unknown>;
|
|
662
|
+
const usedPercent = Number(window.usedPercent);
|
|
663
|
+
if (!Number.isFinite(usedPercent)) return [];
|
|
664
|
+
const duration = Number(window.windowDurationMins);
|
|
665
|
+
const durationMinutes = Number.isFinite(duration) ? duration : null;
|
|
666
|
+
const resetsAt = Number(window.resetsAt);
|
|
667
|
+
return [{
|
|
668
|
+
id,
|
|
669
|
+
durationMinutes,
|
|
670
|
+
label: formatRateLimitWindowLabel(durationMinutes, id),
|
|
671
|
+
usedPercent: Math.max(0, Math.min(100, usedPercent)),
|
|
672
|
+
resetsAt: Number.isFinite(resetsAt)
|
|
673
|
+
? new Date(resetsAt * 1000).toISOString()
|
|
674
|
+
: null,
|
|
675
|
+
}];
|
|
676
|
+
});
|
|
677
|
+
return {
|
|
678
|
+
provider: 'codex' as const,
|
|
679
|
+
authKind: 'subscription' as const,
|
|
680
|
+
observedAt: new Date().toISOString(),
|
|
681
|
+
stale: false,
|
|
682
|
+
windows,
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
620
686
|
start() {
|
|
621
687
|
return codexRuntimeCall(() => this.manager.start());
|
|
622
688
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
ALTER TABLE threads ADD COLUMN active_turn_collaboration_mode TEXT;
|
|
2
|
+
|
|
3
|
+
ALTER TABLE thread_pending_steers
|
|
4
|
+
ADD COLUMN delivery TEXT NOT NULL DEFAULT 'steer';
|
|
5
|
+
|
|
6
|
+
ALTER TABLE thread_pending_steers
|
|
7
|
+
ADD COLUMN turn_config_json TEXT;
|
|
8
|
+
|
|
9
|
+
CREATE TABLE IF NOT EXISTS thread_prompt_requests (
|
|
10
|
+
id TEXT PRIMARY KEY,
|
|
11
|
+
thread_id TEXT NOT NULL,
|
|
12
|
+
client_request_id TEXT NOT NULL,
|
|
13
|
+
status TEXT NOT NULL,
|
|
14
|
+
created_at TEXT NOT NULL,
|
|
15
|
+
updated_at TEXT NOT NULL
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE UNIQUE INDEX IF NOT EXISTS thread_prompt_requests_thread_client_request_idx
|
|
19
|
+
ON thread_prompt_requests(thread_id, client_request_id);
|