@parall/codex-agent 1.51.0 → 1.52.1
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/dist/dispatch.d.ts.map +1 -1
- package/dist/dispatch.js +5 -0
- package/dist/event-mapping.d.ts +7 -0
- package/dist/event-mapping.d.ts.map +1 -1
- package/dist/event-mapping.js +140 -0
- package/package.json +4 -4
- package/src/dispatch.ts +5 -0
- package/src/event-mapping.ts +140 -1
package/dist/dispatch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,YAAY,EACZ,QAAQ,EACR,iBAAiB,EACjB,aAAa,EACb,YAAY,EACb,MAAM,oBAAoB,CAAC;AAa5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,2BAA2B,CAAC;AAGnC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAGhE,KAAK,4BAA4B,GAAG,IAAI,CACtC,gBAAgB,EACd,gBAAgB,GAChB,UAAU,GACV,WAAW,GACX,OAAO,GACP,iBAAiB,GACjB,SAAS,GACT,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,mBAAmB,CAAC;IACpC,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;;;;;;;;;;;;;OAiBG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gFAAgF;IAChF,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,8EAA8E;IAC9E,4BAA4B,CAAC,EAAE,MAAM,CAAC;CACvC,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,qBAAa,qBAAsB,YAAW,eAAe;IA8C/C,OAAO,CAAC,QAAQ,CAAC,IAAI;IA7CjC,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,IAAI,CAA+C;IAC3D,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,YAAY,CAA8B;IAClD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA6B;IAC/D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAqB;IAC1D;;;;;;;;OAQG;IACH,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA8B;IAC/D,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAkC;IACxE,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,8BAA8B,CAAK;IAE3C;;;;;;;OAOG;IACH,OAAO,CAAC,aAAa;gBAYQ,IAAI,EAAE,4BAA4B;IAS/D,6EAA6E;IAC7E,kBAAkB,CAAC,GAAG,EAAE,eAAe,GAAG,MAAM,IAAI;IAKpD;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAU1B,YAAY,CAAC,MAAM,EAAE;QACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,qBAAqB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KACvC,GAAG,IAAI;IAQF,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqB/E,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAyBvC,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAI1C,QAAQ,CAAC,EACd,KAAK,EACL,YAAY,EACZ,UAAU,EACV,OAAO,GACR,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
|
package/dist/dispatch.js
CHANGED
|
@@ -436,6 +436,11 @@ export class CodexAppServerAdapter {
|
|
|
436
436
|
yield { ...runtimeEvent, project: false, groupKey };
|
|
437
437
|
continue;
|
|
438
438
|
}
|
|
439
|
+
if (runtimeEvent.type === 'turn_outcome') {
|
|
440
|
+
// Turn-boundary summary — no groupKey, passed through as-is.
|
|
441
|
+
yield runtimeEvent;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
439
444
|
yield { ...runtimeEvent, groupKey };
|
|
440
445
|
}
|
|
441
446
|
}
|
package/dist/event-mapping.d.ts
CHANGED
|
@@ -19,6 +19,13 @@ import type { RuntimeEvent } from '@parall/agent-core';
|
|
|
19
19
|
*/
|
|
20
20
|
export declare class EventMapper {
|
|
21
21
|
private readonly toolCallStart;
|
|
22
|
+
/**
|
|
23
|
+
* Latest thread/tokenUsage/updated snapshot for this turn. On the pinned
|
|
24
|
+
* app-server (0.144.x) `turn/completed.turn` carries NO usage field —
|
|
25
|
+
* token accounting arrives through this separate notification
|
|
26
|
+
* (`tokenUsage.last`), so the mapper buffers it for the turn boundary.
|
|
27
|
+
*/
|
|
28
|
+
private lastTokenUsage;
|
|
22
29
|
/**
|
|
23
30
|
* Map a single server notification to zero or more RuntimeEvents.
|
|
24
31
|
* Return `null` for notifications that have no chat surface.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-mapping.d.ts","sourceRoot":"","sources":["../src/event-mapping.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"event-mapping.d.ts","sourceRoot":"","sources":["../src/event-mapping.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAA+B,MAAM,oBAAoB,CAAC;AAEpF;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D;;;;;OAKG;IACH,OAAO,CAAC,cAAc,CAAsC;IAE5D;;;OAGG;IACH,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,YAAY,EAAE;IA8DpD,OAAO,CAAC,OAAO;IA4Jf;;;;;OAKG;IACH,OAAO,CAAC,eAAe;CAaxB"}
|
package/dist/event-mapping.js
CHANGED
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
*/
|
|
19
19
|
export class EventMapper {
|
|
20
20
|
toolCallStart = new Map();
|
|
21
|
+
/**
|
|
22
|
+
* Latest thread/tokenUsage/updated snapshot for this turn. On the pinned
|
|
23
|
+
* app-server (0.144.x) `turn/completed.turn` carries NO usage field —
|
|
24
|
+
* token accounting arrives through this separate notification
|
|
25
|
+
* (`tokenUsage.last`), so the mapper buffers it for the turn boundary.
|
|
26
|
+
*/
|
|
27
|
+
lastTokenUsage;
|
|
21
28
|
/**
|
|
22
29
|
* Map a single server notification to zero or more RuntimeEvents.
|
|
23
30
|
* Return `null` for notifications that have no chat surface.
|
|
@@ -45,6 +52,15 @@ export class EventMapper {
|
|
|
45
52
|
events.push(...this.mapItem(item, 'completed'));
|
|
46
53
|
break;
|
|
47
54
|
}
|
|
55
|
+
case 'thread/tokenUsage/updated': {
|
|
56
|
+
// Buffered for the turn boundary — see the field comment.
|
|
57
|
+
const tokenUsage = p.tokenUsage;
|
|
58
|
+
const last = tokenUsage?.last;
|
|
59
|
+
if (last && typeof last === 'object') {
|
|
60
|
+
this.lastTokenUsage = last;
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
48
64
|
case 'turn/completed': {
|
|
49
65
|
const turn = p.turn;
|
|
50
66
|
const status = turn ? asString(turn.status) : undefined;
|
|
@@ -53,6 +69,12 @@ export class EventMapper {
|
|
|
53
69
|
const message = asString(error?.message) ?? 'Codex turn failed';
|
|
54
70
|
events.push({ type: 'error', message });
|
|
55
71
|
}
|
|
72
|
+
// Turn-boundary classification (agent-turn-outcome-design.md §4.2):
|
|
73
|
+
// structured codexErrorInfo first, message families second, generic
|
|
74
|
+
// api_error otherwise. Usage rides along on both outcomes. Pushed
|
|
75
|
+
// after the error event so the gateway folds them in wire order.
|
|
76
|
+
events.push(buildCodexTurnOutcome(status, error, turn?.usage ?? this.lastTokenUsage));
|
|
77
|
+
this.lastTokenUsage = undefined;
|
|
56
78
|
break;
|
|
57
79
|
}
|
|
58
80
|
case 'error': {
|
|
@@ -236,6 +258,124 @@ function asString(value) {
|
|
|
236
258
|
const trimmed = value.trim();
|
|
237
259
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
238
260
|
}
|
|
261
|
+
const CODEX_LIMIT_TEXT = /usage limit|rate limit|quota exceeded|plan limit/i;
|
|
262
|
+
const CODEX_AUTH_TEXT = /unauthorized|not logged in|invalid api key|authentication/i;
|
|
263
|
+
const CODEX_CONTEXT_TEXT = /context (window|length)|prompt is too long|request too large/i;
|
|
264
|
+
/**
|
|
265
|
+
* Extract the structured discriminator from a failed turn's error. On the
|
|
266
|
+
* pinned app-server (0.144.x) this is `error.codexErrorInfo` — a Rust enum
|
|
267
|
+
* serialized either as a bare string ("usageLimitExceeded") or as a
|
|
268
|
+
* single-key object ({"httpError": {"status": 429}}). Returns the variant
|
|
269
|
+
* name plus any HTTP status found inside the variant payload. `error.code`
|
|
270
|
+
* is kept as a secondary read for other/newer protocol shapes.
|
|
271
|
+
*/
|
|
272
|
+
function codexErrorDiscriminator(error) {
|
|
273
|
+
const info = error?.codexErrorInfo;
|
|
274
|
+
if (typeof info === 'string')
|
|
275
|
+
return { variant: info };
|
|
276
|
+
if (info && typeof info === 'object') {
|
|
277
|
+
const keys = Object.keys(info);
|
|
278
|
+
if (keys.length > 0) {
|
|
279
|
+
const variant = keys[0];
|
|
280
|
+
const payload = info[variant];
|
|
281
|
+
let status;
|
|
282
|
+
if (payload && typeof payload === 'object') {
|
|
283
|
+
// Pinned protocol carries camelCase `httpStatusCode`; `status` is
|
|
284
|
+
// kept as a compatibility fallback only (review round 3 P1).
|
|
285
|
+
const rec = payload;
|
|
286
|
+
const s = rec.httpStatusCode ?? rec.status;
|
|
287
|
+
if (typeof s === 'number' && Number.isFinite(s))
|
|
288
|
+
status = s;
|
|
289
|
+
}
|
|
290
|
+
return { variant, status };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return {};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Classify a codex turn/completed frame into a TurnOutcomeEvent. The
|
|
297
|
+
* structured `codexErrorInfo` variant is the primary discriminator
|
|
298
|
+
* (usageLimitExceeded / contextWindowExceeded / unauthorized / HTTP-status
|
|
299
|
+
* variants), `error.code` and message families only refine, so unknown
|
|
300
|
+
* wording degrades to `api_error` — never to a wrong deferral. codex does
|
|
301
|
+
* not report a reset time, so usage_limit carries no retryAt and the server
|
|
302
|
+
* applies its default deferral window.
|
|
303
|
+
*/
|
|
304
|
+
function buildCodexTurnOutcome(status, error, usageRaw) {
|
|
305
|
+
const usage = extractCodexUsage(usageRaw);
|
|
306
|
+
if (status !== 'failed') {
|
|
307
|
+
return { type: 'turn_outcome', outcome: 'ok', ...(usage ? { usage } : {}) };
|
|
308
|
+
}
|
|
309
|
+
const message = asString(error?.message) ?? '';
|
|
310
|
+
const { variant, status: httpStatus } = codexErrorDiscriminator(error);
|
|
311
|
+
const code = typeof error?.code === 'number' ? error.code : (asString(error?.code) ?? undefined);
|
|
312
|
+
const codeStr = httpStatus !== undefined ? String(httpStatus) : code === undefined ? '' : String(code);
|
|
313
|
+
const variantLower = (variant ?? '').toLowerCase();
|
|
314
|
+
// Structured discriminators (variant + HTTP status) rule first; message text
|
|
315
|
+
// only refines when neither can classify. Mixing text into the same OR as
|
|
316
|
+
// the variant let a `usageLimitExceeded` frame whose message happened to
|
|
317
|
+
// quote "authentication"/"rate limit" misclassify as auth and lose the
|
|
318
|
+
// deferred redrive — the exact read-no-reply regression this PR prevents.
|
|
319
|
+
let outcome = 'api_error';
|
|
320
|
+
if (variantLower.includes('unauthorized') || codeStr === '401' || codeStr === '403') {
|
|
321
|
+
outcome = 'auth';
|
|
322
|
+
}
|
|
323
|
+
else if (variantLower.includes('contextwindow') || codeStr === '413') {
|
|
324
|
+
outcome = 'context_overflow';
|
|
325
|
+
}
|
|
326
|
+
else if (variantLower.includes('usagelimit') ||
|
|
327
|
+
variantLower.includes('ratelimit') ||
|
|
328
|
+
codeStr === '429') {
|
|
329
|
+
outcome = 'usage_limit';
|
|
330
|
+
}
|
|
331
|
+
else if (CODEX_AUTH_TEXT.test(message)) {
|
|
332
|
+
outcome = 'auth';
|
|
333
|
+
}
|
|
334
|
+
else if (CODEX_CONTEXT_TEXT.test(message)) {
|
|
335
|
+
outcome = 'context_overflow';
|
|
336
|
+
}
|
|
337
|
+
else if (CODEX_LIMIT_TEXT.test(message)) {
|
|
338
|
+
outcome = 'usage_limit';
|
|
339
|
+
}
|
|
340
|
+
const raw = {};
|
|
341
|
+
if (variant)
|
|
342
|
+
raw.codex_error_info = variant;
|
|
343
|
+
if (httpStatus !== undefined)
|
|
344
|
+
raw.status = httpStatus;
|
|
345
|
+
if (code !== undefined)
|
|
346
|
+
raw.code = code;
|
|
347
|
+
return {
|
|
348
|
+
type: 'turn_outcome',
|
|
349
|
+
outcome,
|
|
350
|
+
...(message ? { detail: message.slice(0, 500) } : {}),
|
|
351
|
+
...(usage ? { usage } : {}),
|
|
352
|
+
...(Object.keys(raw).length > 0 ? { raw } : {}),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/** codex app-server usage fields are camelCase; tolerate snake_case too. */
|
|
356
|
+
function extractCodexUsage(raw) {
|
|
357
|
+
if (!raw || typeof raw !== 'object')
|
|
358
|
+
return undefined;
|
|
359
|
+
const r = raw;
|
|
360
|
+
const num = (a, b) => {
|
|
361
|
+
if (typeof a === 'number' && Number.isFinite(a))
|
|
362
|
+
return a;
|
|
363
|
+
if (typeof b === 'number' && Number.isFinite(b))
|
|
364
|
+
return b;
|
|
365
|
+
return undefined;
|
|
366
|
+
};
|
|
367
|
+
const usage = {};
|
|
368
|
+
const input = num(r.inputTokens, r.input_tokens);
|
|
369
|
+
if (input !== undefined)
|
|
370
|
+
usage.inputTokens = input;
|
|
371
|
+
const output = num(r.outputTokens, r.output_tokens);
|
|
372
|
+
if (output !== undefined)
|
|
373
|
+
usage.outputTokens = output;
|
|
374
|
+
const cached = num(r.cachedInputTokens, r.cached_input_tokens);
|
|
375
|
+
if (cached !== undefined)
|
|
376
|
+
usage.cacheReadTokens = cached;
|
|
377
|
+
return Object.keys(usage).length > 0 ? usage : undefined;
|
|
378
|
+
}
|
|
239
379
|
function joinReasoningText(item) {
|
|
240
380
|
const summary = Array.isArray(item.summary)
|
|
241
381
|
? item.summary.filter((x) => typeof x === 'string')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/codex-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.52.1",
|
|
4
4
|
"description": "Codex CLI bridge runtime for self-hosted Parall agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
"src"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@parall/agent-core": "1.
|
|
29
|
-
"@parall/cli": "1.
|
|
30
|
-
"@parall/sdk": "1.
|
|
28
|
+
"@parall/agent-core": "1.52.1",
|
|
29
|
+
"@parall/cli": "1.52.1",
|
|
30
|
+
"@parall/sdk": "1.52.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@types/node": "^22.0.0",
|
package/src/dispatch.ts
CHANGED
|
@@ -531,6 +531,11 @@ export class CodexAppServerAdapter implements DispatchAdapter {
|
|
|
531
531
|
yield { ...runtimeEvent, project: false, groupKey };
|
|
532
532
|
continue;
|
|
533
533
|
}
|
|
534
|
+
if (runtimeEvent.type === 'turn_outcome') {
|
|
535
|
+
// Turn-boundary summary — no groupKey, passed through as-is.
|
|
536
|
+
yield runtimeEvent;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
534
539
|
yield { ...runtimeEvent, groupKey };
|
|
535
540
|
}
|
|
536
541
|
} finally {
|
package/src/event-mapping.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RuntimeEvent } from '@parall/agent-core';
|
|
1
|
+
import type { RuntimeEvent, TurnOutcomeEvent, TurnUsage } from '@parall/agent-core';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Translation layer between `codex app-server` JSON-RPC notifications and
|
|
@@ -20,6 +20,13 @@ import type { RuntimeEvent } from '@parall/agent-core';
|
|
|
20
20
|
*/
|
|
21
21
|
export class EventMapper {
|
|
22
22
|
private readonly toolCallStart = new Map<string, number>();
|
|
23
|
+
/**
|
|
24
|
+
* Latest thread/tokenUsage/updated snapshot for this turn. On the pinned
|
|
25
|
+
* app-server (0.144.x) `turn/completed.turn` carries NO usage field —
|
|
26
|
+
* token accounting arrives through this separate notification
|
|
27
|
+
* (`tokenUsage.last`), so the mapper buffers it for the turn boundary.
|
|
28
|
+
*/
|
|
29
|
+
private lastTokenUsage: Record<string, unknown> | undefined;
|
|
23
30
|
|
|
24
31
|
/**
|
|
25
32
|
* Map a single server notification to zero or more RuntimeEvents.
|
|
@@ -50,6 +57,16 @@ export class EventMapper {
|
|
|
50
57
|
break;
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
case 'thread/tokenUsage/updated': {
|
|
61
|
+
// Buffered for the turn boundary — see the field comment.
|
|
62
|
+
const tokenUsage = p.tokenUsage as Record<string, unknown> | undefined;
|
|
63
|
+
const last = tokenUsage?.last;
|
|
64
|
+
if (last && typeof last === 'object') {
|
|
65
|
+
this.lastTokenUsage = last as Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
case 'turn/completed': {
|
|
54
71
|
const turn = p.turn as Record<string, unknown> | undefined;
|
|
55
72
|
const status = turn ? asString(turn.status) : undefined;
|
|
@@ -58,6 +75,12 @@ export class EventMapper {
|
|
|
58
75
|
const message = asString(error?.message) ?? 'Codex turn failed';
|
|
59
76
|
events.push({ type: 'error', message });
|
|
60
77
|
}
|
|
78
|
+
// Turn-boundary classification (agent-turn-outcome-design.md §4.2):
|
|
79
|
+
// structured codexErrorInfo first, message families second, generic
|
|
80
|
+
// api_error otherwise. Usage rides along on both outcomes. Pushed
|
|
81
|
+
// after the error event so the gateway folds them in wire order.
|
|
82
|
+
events.push(buildCodexTurnOutcome(status, error, turn?.usage ?? this.lastTokenUsage));
|
|
83
|
+
this.lastTokenUsage = undefined;
|
|
61
84
|
break;
|
|
62
85
|
}
|
|
63
86
|
|
|
@@ -254,6 +277,122 @@ function asString(value: unknown): string | undefined {
|
|
|
254
277
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
255
278
|
}
|
|
256
279
|
|
|
280
|
+
const CODEX_LIMIT_TEXT = /usage limit|rate limit|quota exceeded|plan limit/i;
|
|
281
|
+
const CODEX_AUTH_TEXT = /unauthorized|not logged in|invalid api key|authentication/i;
|
|
282
|
+
const CODEX_CONTEXT_TEXT = /context (window|length)|prompt is too long|request too large/i;
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Extract the structured discriminator from a failed turn's error. On the
|
|
286
|
+
* pinned app-server (0.144.x) this is `error.codexErrorInfo` — a Rust enum
|
|
287
|
+
* serialized either as a bare string ("usageLimitExceeded") or as a
|
|
288
|
+
* single-key object ({"httpError": {"status": 429}}). Returns the variant
|
|
289
|
+
* name plus any HTTP status found inside the variant payload. `error.code`
|
|
290
|
+
* is kept as a secondary read for other/newer protocol shapes.
|
|
291
|
+
*/
|
|
292
|
+
function codexErrorDiscriminator(error: Record<string, unknown> | undefined): {
|
|
293
|
+
variant?: string;
|
|
294
|
+
status?: number;
|
|
295
|
+
} {
|
|
296
|
+
const info = error?.codexErrorInfo;
|
|
297
|
+
if (typeof info === 'string') return { variant: info };
|
|
298
|
+
if (info && typeof info === 'object') {
|
|
299
|
+
const keys = Object.keys(info);
|
|
300
|
+
if (keys.length > 0) {
|
|
301
|
+
const variant = keys[0];
|
|
302
|
+
const payload = (info as Record<string, unknown>)[variant];
|
|
303
|
+
let status: number | undefined;
|
|
304
|
+
if (payload && typeof payload === 'object') {
|
|
305
|
+
// Pinned protocol carries camelCase `httpStatusCode`; `status` is
|
|
306
|
+
// kept as a compatibility fallback only (review round 3 P1).
|
|
307
|
+
const rec = payload as Record<string, unknown>;
|
|
308
|
+
const s = rec.httpStatusCode ?? rec.status;
|
|
309
|
+
if (typeof s === 'number' && Number.isFinite(s)) status = s;
|
|
310
|
+
}
|
|
311
|
+
return { variant, status };
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return {};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Classify a codex turn/completed frame into a TurnOutcomeEvent. The
|
|
319
|
+
* structured `codexErrorInfo` variant is the primary discriminator
|
|
320
|
+
* (usageLimitExceeded / contextWindowExceeded / unauthorized / HTTP-status
|
|
321
|
+
* variants), `error.code` and message families only refine, so unknown
|
|
322
|
+
* wording degrades to `api_error` — never to a wrong deferral. codex does
|
|
323
|
+
* not report a reset time, so usage_limit carries no retryAt and the server
|
|
324
|
+
* applies its default deferral window.
|
|
325
|
+
*/
|
|
326
|
+
function buildCodexTurnOutcome(
|
|
327
|
+
status: string | undefined,
|
|
328
|
+
error: Record<string, unknown> | undefined,
|
|
329
|
+
usageRaw: unknown,
|
|
330
|
+
): TurnOutcomeEvent {
|
|
331
|
+
const usage = extractCodexUsage(usageRaw);
|
|
332
|
+
if (status !== 'failed') {
|
|
333
|
+
return { type: 'turn_outcome', outcome: 'ok', ...(usage ? { usage } : {}) };
|
|
334
|
+
}
|
|
335
|
+
const message = asString(error?.message) ?? '';
|
|
336
|
+
const { variant, status: httpStatus } = codexErrorDiscriminator(error);
|
|
337
|
+
const code = typeof error?.code === 'number' ? error.code : (asString(error?.code) ?? undefined);
|
|
338
|
+
const codeStr =
|
|
339
|
+
httpStatus !== undefined ? String(httpStatus) : code === undefined ? '' : String(code);
|
|
340
|
+
const variantLower = (variant ?? '').toLowerCase();
|
|
341
|
+
// Structured discriminators (variant + HTTP status) rule first; message text
|
|
342
|
+
// only refines when neither can classify. Mixing text into the same OR as
|
|
343
|
+
// the variant let a `usageLimitExceeded` frame whose message happened to
|
|
344
|
+
// quote "authentication"/"rate limit" misclassify as auth and lose the
|
|
345
|
+
// deferred redrive — the exact read-no-reply regression this PR prevents.
|
|
346
|
+
let outcome: TurnOutcomeEvent['outcome'] = 'api_error';
|
|
347
|
+
if (variantLower.includes('unauthorized') || codeStr === '401' || codeStr === '403') {
|
|
348
|
+
outcome = 'auth';
|
|
349
|
+
} else if (variantLower.includes('contextwindow') || codeStr === '413') {
|
|
350
|
+
outcome = 'context_overflow';
|
|
351
|
+
} else if (
|
|
352
|
+
variantLower.includes('usagelimit') ||
|
|
353
|
+
variantLower.includes('ratelimit') ||
|
|
354
|
+
codeStr === '429'
|
|
355
|
+
) {
|
|
356
|
+
outcome = 'usage_limit';
|
|
357
|
+
} else if (CODEX_AUTH_TEXT.test(message)) {
|
|
358
|
+
outcome = 'auth';
|
|
359
|
+
} else if (CODEX_CONTEXT_TEXT.test(message)) {
|
|
360
|
+
outcome = 'context_overflow';
|
|
361
|
+
} else if (CODEX_LIMIT_TEXT.test(message)) {
|
|
362
|
+
outcome = 'usage_limit';
|
|
363
|
+
}
|
|
364
|
+
const raw: Record<string, string | number | boolean | null> = {};
|
|
365
|
+
if (variant) raw.codex_error_info = variant;
|
|
366
|
+
if (httpStatus !== undefined) raw.status = httpStatus;
|
|
367
|
+
if (code !== undefined) raw.code = code;
|
|
368
|
+
return {
|
|
369
|
+
type: 'turn_outcome',
|
|
370
|
+
outcome,
|
|
371
|
+
...(message ? { detail: message.slice(0, 500) } : {}),
|
|
372
|
+
...(usage ? { usage } : {}),
|
|
373
|
+
...(Object.keys(raw).length > 0 ? { raw } : {}),
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** codex app-server usage fields are camelCase; tolerate snake_case too. */
|
|
378
|
+
function extractCodexUsage(raw: unknown): TurnUsage | undefined {
|
|
379
|
+
if (!raw || typeof raw !== 'object') return undefined;
|
|
380
|
+
const r = raw as Record<string, unknown>;
|
|
381
|
+
const num = (a: unknown, b: unknown): number | undefined => {
|
|
382
|
+
if (typeof a === 'number' && Number.isFinite(a)) return a;
|
|
383
|
+
if (typeof b === 'number' && Number.isFinite(b)) return b;
|
|
384
|
+
return undefined;
|
|
385
|
+
};
|
|
386
|
+
const usage: TurnUsage = {};
|
|
387
|
+
const input = num(r.inputTokens, r.input_tokens);
|
|
388
|
+
if (input !== undefined) usage.inputTokens = input;
|
|
389
|
+
const output = num(r.outputTokens, r.output_tokens);
|
|
390
|
+
if (output !== undefined) usage.outputTokens = output;
|
|
391
|
+
const cached = num(r.cachedInputTokens, r.cached_input_tokens);
|
|
392
|
+
if (cached !== undefined) usage.cacheReadTokens = cached;
|
|
393
|
+
return Object.keys(usage).length > 0 ? usage : undefined;
|
|
394
|
+
}
|
|
395
|
+
|
|
257
396
|
function joinReasoningText(item: Record<string, unknown>): string {
|
|
258
397
|
const summary = Array.isArray(item.summary)
|
|
259
398
|
? item.summary.filter((x) => typeof x === 'string')
|