remote-codex 0.11.35 → 0.11.37

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.
@@ -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-C14vD5Hp.js"></script>
13
+ <script type="module" crossorigin src="/assets/index-pkf-_mec.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-CK1xSLOW.js">
19
+ <link rel="modulepreload" crossorigin href="/assets/thread-ui-xWvswa2v.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-DYIjFImB.css">
22
+ <link rel="stylesheet" crossorigin href="/assets/index-P0J5du_C.css">
23
23
  </head>
24
24
  <body class="bg-stone-950">
25
25
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "remote-codex",
3
- "version": "0.11.35",
3
+ "version": "0.11.37",
4
4
  "description": "Local web supervisor for Codex workspaces and threads.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -32,7 +32,7 @@ class FakeCodexManager extends EventEmitter {
32
32
  return { account: { type: 'chatgpt' }, requiresOpenaiAuth: true };
33
33
  }
34
34
 
35
- async readAccountRateLimits() {
35
+ async readAccountRateLimits(): Promise<any> {
36
36
  return {
37
37
  rateLimits: {},
38
38
  rateLimitsByLimitId: {
@@ -46,6 +46,63 @@ class FakeCodexManager extends EventEmitter {
46
46
  }
47
47
 
48
48
  describe('CodexRuntimeAdapter', () => {
49
+ it('does not terminate a turn while Codex is retrying an upstream request', () => {
50
+ const manager = new FakeCodexManager();
51
+ const adapter = new CodexRuntimeAdapter(manager as never);
52
+ const events: unknown[] = [];
53
+ adapter.on('event', (event) => events.push(event));
54
+
55
+ manager.emit('notification', {
56
+ method: 'error',
57
+ params: {
58
+ threadId: 'thread-1',
59
+ turnId: 'turn-1',
60
+ willRetry: true,
61
+ error: {
62
+ message: 'Reconnecting... 1/5',
63
+ additionalDetails: 'unexpected status 404 Not Found',
64
+ },
65
+ },
66
+ });
67
+
68
+ expect(events).toEqual([]);
69
+ });
70
+
71
+ it('preserves detailed upstream errors when the final retry fails', () => {
72
+ const manager = new FakeCodexManager();
73
+ const adapter = new CodexRuntimeAdapter(manager as never);
74
+ const events: unknown[] = [];
75
+ adapter.on('event', (event) => events.push(event));
76
+
77
+ manager.emit('notification', {
78
+ method: 'error',
79
+ params: {
80
+ threadId: 'thread-1',
81
+ turnId: 'turn-1',
82
+ willRetry: false,
83
+ error: {
84
+ message: 'Upstream request failed.',
85
+ additionalDetails: JSON.stringify({
86
+ error: {
87
+ message: 'Model "gpt-5.6-sol" is not supported by any configured account in this group',
88
+ type: 'model_not_found',
89
+ },
90
+ }),
91
+ },
92
+ },
93
+ });
94
+
95
+ expect(events).toEqual([
96
+ expect.objectContaining({
97
+ type: 'turn.failed',
98
+ providerSessionId: 'thread-1',
99
+ providerTurnId: 'turn-1',
100
+ error: 'Model "gpt-5.6-sol" is not supported by any configured account in this group',
101
+ willRetry: false,
102
+ }),
103
+ ]);
104
+ });
105
+
49
106
  it('converts prompt photo tokens into structured local image input', async () => {
50
107
  const manager = new FakeCodexManager();
51
108
  const adapter = new CodexRuntimeAdapter(manager as never);
@@ -104,6 +161,48 @@ describe('CodexRuntimeAdapter', () => {
104
161
  });
105
162
  });
106
163
 
164
+ it('uses the populated backward-compatible snapshot when a named bucket is empty', async () => {
165
+ const manager = new FakeCodexManager();
166
+ manager.readAccountRateLimits = async () => ({
167
+ rateLimits: {
168
+ primary: {
169
+ usedPercent: 63,
170
+ windowDurationMins: 10_080,
171
+ resetsAt: 1_800_604_800,
172
+ },
173
+ },
174
+ rateLimitsByLimitId: { codex: { primary: null, secondary: null } },
175
+ });
176
+ const adapter = new CodexRuntimeAdapter(manager as never);
177
+
178
+ await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
179
+ provider: 'codex',
180
+ authKind: 'subscription',
181
+ windows: [
182
+ { id: 'primary', label: '7d', durationMinutes: 10_080, usedPercent: 63 },
183
+ ],
184
+ });
185
+ });
186
+
187
+ it('normalizes legacy snake-case rate-limit window fields', async () => {
188
+ const manager = new FakeCodexManager();
189
+ manager.readAccountRateLimits = async () => ({
190
+ rateLimits: {
191
+ primary: {
192
+ used_percent: 12,
193
+ window_minutes: 300,
194
+ resets_at: 1_800_000_000,
195
+ },
196
+ },
197
+ rateLimitsByLimitId: null,
198
+ });
199
+ const adapter = new CodexRuntimeAdapter(manager as never);
200
+
201
+ await expect(adapter.getSubscriptionUsage()).resolves.toMatchObject({
202
+ windows: [{ label: '5h', durationMinutes: 300, usedPercent: 12 }],
203
+ });
204
+ });
205
+
107
206
  it('hides subscription windows for API-key authentication', async () => {
108
207
  const manager = new FakeCodexManager();
109
208
  manager.readAccount = async () => ({
@@ -45,6 +45,7 @@ import {
45
45
  CodexThreadRecord,
46
46
  CodexThreadStatus,
47
47
  CodexThreadGoalRecord,
48
+ CodexTurnError,
48
49
  CodexTurnRecord,
49
50
  CodexTurnItem,
50
51
  CodexServerEvent,
@@ -208,7 +209,52 @@ function mapModel(model: Awaited<ReturnType<CodexAppServerManager['listModels']>
208
209
  }
209
210
 
210
211
  function mapTurn(turn: CodexTurnRecord): AgentTurn {
211
- return codexTurnToAgentTurn(turn);
212
+ const mapped = codexTurnToAgentTurn(turn);
213
+ return turn.error
214
+ ? {
215
+ ...mapped,
216
+ error: { message: codexTurnErrorMessage(turn.error) },
217
+ }
218
+ : mapped;
219
+ }
220
+
221
+ function nestedUpstreamErrorMessage(value: string | null | undefined) {
222
+ if (!value) {
223
+ return null;
224
+ }
225
+
226
+ try {
227
+ const parsed = JSON.parse(value) as unknown;
228
+ if (typeof parsed !== 'object' || parsed === null) {
229
+ return null;
230
+ }
231
+ const error = 'error' in parsed ? parsed.error : parsed;
232
+ if (typeof error !== 'object' || error === null || !('message' in error)) {
233
+ return null;
234
+ }
235
+ return typeof error.message === 'string' && error.message.trim()
236
+ ? error.message.trim()
237
+ : null;
238
+ } catch {
239
+ return null;
240
+ }
241
+ }
242
+
243
+ function codexTurnErrorMessage(error: CodexTurnError) {
244
+ const message = error.message?.trim() || 'Turn failed unexpectedly.';
245
+ const details = error.additionalDetails?.trim();
246
+ if (!details) {
247
+ return message;
248
+ }
249
+
250
+ const upstreamMessage = nestedUpstreamErrorMessage(details);
251
+ if (upstreamMessage && !message.includes(upstreamMessage)) {
252
+ return upstreamMessage;
253
+ }
254
+ if (message.includes(details)) {
255
+ return message;
256
+ }
257
+ return `${message}\n\n${details}`;
212
258
  }
213
259
 
214
260
  function mapGoal(goal: CodexThreadGoalRecord): AgentGoal {
@@ -467,15 +513,18 @@ function mapCodexNotification(event: CodexServerEvent): AgentRuntimeEvent | null
467
513
  const params = event.params as {
468
514
  threadId: string;
469
515
  turnId: string;
470
- error: { message?: string };
516
+ error: CodexTurnError;
471
517
  willRetry: boolean;
472
518
  };
519
+ if (params.willRetry) {
520
+ return null;
521
+ }
473
522
  return {
474
523
  type: 'turn.failed',
475
524
  provider: 'codex',
476
525
  providerSessionId: params.threadId,
477
526
  providerTurnId: params.turnId,
478
- error: params.error.message ?? 'Turn failed unexpectedly.',
527
+ error: codexTurnErrorMessage(params.error),
479
528
  willRetry: params.willRetry,
480
529
  };
481
530
  }
@@ -498,6 +547,41 @@ function formatRateLimitWindowLabel(
498
547
  return `${durationMinutes}m`;
499
548
  }
500
549
 
550
+ function recordOrNull(value: unknown): Record<string, unknown> | null {
551
+ return value && typeof value === 'object' && !Array.isArray(value)
552
+ ? (value as Record<string, unknown>)
553
+ : null;
554
+ }
555
+
556
+ function rateLimitWindowsFromSnapshot(snapshot: unknown) {
557
+ const record = recordOrNull(snapshot);
558
+ if (!record) return [];
559
+
560
+ return ['primary', 'secondary'].flatMap((id) => {
561
+ const window = recordOrNull(record[id]);
562
+ if (!window) return [];
563
+ const usedPercent = Number(window.usedPercent ?? window.used_percent);
564
+ if (!Number.isFinite(usedPercent)) return [];
565
+ const duration = Number(
566
+ window.windowDurationMins ??
567
+ window.window_duration_mins ??
568
+ window.windowMinutes ??
569
+ window.window_minutes,
570
+ );
571
+ const durationMinutes = Number.isFinite(duration) ? duration : null;
572
+ const resetsAt = Number(window.resetsAt ?? window.resets_at);
573
+ return [{
574
+ id,
575
+ durationMinutes,
576
+ label: formatRateLimitWindowLabel(durationMinutes, id),
577
+ usedPercent: Math.max(0, Math.min(100, usedPercent)),
578
+ resetsAt: Number.isFinite(resetsAt)
579
+ ? new Date(resetsAt * 1000).toISOString()
580
+ : null,
581
+ }];
582
+ });
583
+ }
584
+
501
585
  function mapCodexRuntimeError(error: unknown): never {
502
586
  if (error instanceof AgentRuntimeError) {
503
587
  throw error;
@@ -649,31 +733,14 @@ export class CodexRuntimeAdapter extends EventEmitter implements AgentRuntime {
649
733
  this.manager.readAccountRateLimits(),
650
734
  );
651
735
  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
- });
736
+ const candidates = [
737
+ response.rateLimits,
738
+ buckets?.codex,
739
+ ...Object.values(buckets ?? {}),
740
+ ];
741
+ const windows = candidates
742
+ .map(rateLimitWindowsFromSnapshot)
743
+ .sort((left, right) => right.length - left.length)[0] ?? [];
677
744
  return {
678
745
  provider: 'codex' as const,
679
746
  authKind: 'subscription' as const,
@@ -135,7 +135,7 @@ export interface CodexTurnItem {
135
135
  export interface CodexTurnRecord {
136
136
  id: string;
137
137
  status: CodexTurnStatus;
138
- error: { message?: string } | null;
138
+ error: CodexTurnError | null;
139
139
  items: CodexTurnItem[];
140
140
  }
141
141
 
@@ -401,6 +401,12 @@ export interface CodexErrorEvent {
401
401
  willRetry: boolean;
402
402
  }
403
403
 
404
+ export interface CodexTurnError {
405
+ message: string;
406
+ additionalDetails?: string | null;
407
+ codexErrorInfo?: unknown;
408
+ }
409
+
404
410
  export type CodexServerEvent =
405
411
  | { method: 'thread/started'; params: { thread: CodexThreadRecord } }
406
412
  | { method: 'thread/status/changed'; params: { threadId: string; status: CodexThreadStatus } }
@@ -416,5 +422,5 @@ export type CodexServerEvent =
416
422
  | { method: 'turn/plan/updated'; params: { threadId: string; turnId: string; explanation: string | null; plan: Array<{ step: string; status: string }> } }
417
423
  | { method: 'turn/completed'; params: { threadId: string; turn: CodexTurnRecord } }
418
424
  | { method: 'item/agentMessage/delta'; params: CodexOutputDeltaEvent }
419
- | { method: 'error'; params: { error: { message?: string }; willRetry: boolean; threadId: string; turnId: string } }
425
+ | { method: 'error'; params: { error: CodexTurnError; willRetry: boolean; threadId: string; turnId: string } }
420
426
  | { method: string; params: Record<string, unknown> };
@@ -558,6 +558,22 @@ export function deleteThreadPendingSteerRecordById(db: DatabaseClient, id: strin
558
558
  db.delete(threadPendingSteers).where(eq(threadPendingSteers.id, id)).run();
559
559
  }
560
560
 
561
+ export function updateThreadPendingSteerRecordDelivery(
562
+ db: DatabaseClient,
563
+ id: string,
564
+ delivery: 'steer' | 'continuation',
565
+ turnId: string,
566
+ ) {
567
+ db.update(threadPendingSteers)
568
+ .set({
569
+ delivery,
570
+ turnId,
571
+ updatedAt: new Date().toISOString(),
572
+ })
573
+ .where(eq(threadPendingSteers.id, id))
574
+ .run();
575
+ }
576
+
561
577
  export function deleteThreadPendingSteerRecordsByThreadId(
562
578
  db: DatabaseClient,
563
579
  threadId: string,