@bitkyc08/opencodex 2.44.0 → 2.45.0

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.
Files changed (38) hide show
  1. package/gui/dist/assets/index-CCfD72yq.js +115 -0
  2. package/gui/dist/assets/index-J96sug5C.css +1 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/openai-responses.ts +14 -0
  6. package/src/chat/outbound.ts +286 -35
  7. package/src/claude/compatibility.ts +192 -0
  8. package/src/claude/model-info.ts +14 -2
  9. package/src/cli/account-extended.ts +24 -1
  10. package/src/cli/init.ts +70 -13
  11. package/src/codex/catalog/provider-fetch.ts +38 -9
  12. package/src/codex/catalog/sync.ts +34 -2
  13. package/src/codex/catalog.ts +1 -1
  14. package/src/config/initialize.ts +132 -0
  15. package/src/config/rebase-provenance.ts +26 -0
  16. package/src/config.ts +50 -1
  17. package/src/generated/compatibility-version.json +41 -29
  18. package/src/lib/windows-secret-acl.ts +8 -4
  19. package/src/providers/quota.ts +26 -15
  20. package/src/responses/state.ts +48 -3
  21. package/src/server/chat-completions.ts +32 -36
  22. package/src/server/chat-native-sse.ts +23 -3
  23. package/src/server/chat-native.ts +15 -8
  24. package/src/server/claude-messages.ts +27 -0
  25. package/src/server/index.ts +5 -1
  26. package/src/server/management/agent-settings-routes.ts +101 -14
  27. package/src/server/management/logs-usage-routes.ts +9 -2
  28. package/src/server/request-log-cursor.ts +84 -0
  29. package/src/server/request-log.ts +46 -3
  30. package/src/server/responses/agent-task-recovery.ts +50 -14
  31. package/src/server/responses/codex-ws-exchange.ts +79 -0
  32. package/src/server/responses/compact.ts +39 -33
  33. package/src/server/responses/core.ts +43 -27
  34. package/src/storage/cleanup.ts +49 -35
  35. package/src/types/config.ts +4 -0
  36. package/src/usage/log.ts +22 -0
  37. package/gui/dist/assets/index-B7_K1Hsj.js +0 -115
  38. package/gui/dist/assets/index-ltx3L-WS.css +0 -1
@@ -169,7 +169,10 @@ async function snapshotOnDiskMatches(path: string, payload: string, payloadBytes
169
169
  return false;
170
170
  }
171
171
  }
172
- const spillCounters = { writes: 0, writeFailures: 0, readFailures: 0 };
172
+ const spillCounters = {
173
+ writes: 0, writeFailures: 0, readFailures: 0,
174
+ aclRetryReturnedTimeouts: 0, aclTimeoutMemoRefusals: 0,
175
+ };
173
176
 
174
177
  export type ResponseSpillWriteFailureCode =
175
178
  | "EACLRETRYEXHAUSTED"
@@ -184,9 +187,14 @@ export type ResponseSpillWriteFailureCode =
184
187
 
185
188
  export type ResponseSpillWriteStatus = "initial" | "healthy" | "degraded";
186
189
 
190
+ export type ResponseSpillWriteFailureOrigin =
191
+ | "retry_returned_timeout"
192
+ | "timeout_memo_refusal";
193
+
187
194
  interface ResponseSpillWriteHealth {
188
195
  consecutiveFailures: number;
189
196
  lastFailureCode: ResponseSpillWriteFailureCode | null;
197
+ lastFailureOrigin: ResponseSpillWriteFailureOrigin | null;
190
198
  lastFailureAt: number | null;
191
199
  lastSuccessAt: number | null;
192
200
  }
@@ -194,6 +202,7 @@ interface ResponseSpillWriteHealth {
194
202
  const spillWriteHealth: ResponseSpillWriteHealth = {
195
203
  consecutiveFailures: 0,
196
204
  lastFailureCode: null,
205
+ lastFailureOrigin: null,
197
206
  lastFailureAt: null,
198
207
  lastSuccessAt: null,
199
208
  };
@@ -226,6 +235,20 @@ function classifySpillWriteFailure(error: unknown): ResponseSpillWriteFailureCod
226
235
  return "EUNKNOWN";
227
236
  }
228
237
 
238
+ /** The spill writer preserves ACL errors in cause; only a fixed memo marker is diagnostic. */
239
+ function spillAclMemoRefusalOrigin(error: unknown): "timeout_memo_refusal" | null {
240
+ let cursor = error;
241
+ for (let depth = 0; depth < 4 && cursor && typeof cursor === "object"; depth += 1) {
242
+ const record = cursor as { code?: unknown; aclFailureOrigin?: unknown; cause?: unknown };
243
+ if ((record.code === "ETIMEDOUT" || record.code === "EACLRETRYEXHAUSTED")
244
+ && record.aclFailureOrigin === "timeout_memo_refusal") {
245
+ return "timeout_memo_refusal";
246
+ }
247
+ cursor = record.cause;
248
+ }
249
+ return null;
250
+ }
251
+
229
252
  function noteSpillWriteSuccess(): void {
230
253
  spillCounters.writes += 1;
231
254
  spillWriteHealth.consecutiveFailures = 0;
@@ -235,11 +258,20 @@ function noteSpillWriteSuccess(): void {
235
258
  function noteSpillWriteFailure(
236
259
  error: unknown,
237
260
  override?: ResponseSpillWriteFailureCode,
261
+ retryOrigin: ResponseSpillWriteFailureOrigin | null = null,
238
262
  ): void {
263
+ const code = override ?? classifySpillWriteFailure(error);
264
+ const origin = code === "ETIMEDOUT" || code === "EACLRETRYEXHAUSTED"
265
+ ? spillAclMemoRefusalOrigin(error) ?? retryOrigin
266
+ : null;
239
267
  spillCounters.writeFailures += 1;
240
268
  spillWriteHealth.consecutiveFailures += 1;
241
- spillWriteHealth.lastFailureCode = override ?? classifySpillWriteFailure(error);
269
+ spillWriteHealth.lastFailureCode = code;
270
+ spillWriteHealth.lastFailureOrigin = origin;
242
271
  spillWriteHealth.lastFailureAt = now();
272
+ // Count terminal publications, not ACL calls or a transient first attempt.
273
+ if (origin === "retry_returned_timeout") spillCounters.aclRetryReturnedTimeouts += 1;
274
+ else if (origin === "timeout_memo_refusal") spillCounters.aclTimeoutMemoRefusals += 1;
243
275
  }
244
276
  /**
245
277
  * Admission-boundary observability (test-visible). directSpills: oversized
@@ -418,6 +450,7 @@ async function runPendingResponseSpill(job: PendingResponseSpill): Promise<void>
418
450
  const candidate = job.candidate;
419
451
  let ref: ResponseSpillRef | null = null;
420
452
  let exhaustedAclRetry = false;
453
+ let aclRetryFailureOrigin: ResponseSpillWriteFailureOrigin | null = null;
421
454
  try {
422
455
  const state = spillPayloadForResident(candidate);
423
456
  try {
@@ -437,6 +470,9 @@ async function runPendingResponseSpill(job: PendingResponseSpill): Promise<void>
437
470
  });
438
471
  } catch (retryError) {
439
472
  exhaustedAclRetry = isAclTimeout(retryError);
473
+ // A returned timeout can also mean an exhausted budget before the next OS command.
474
+ aclRetryFailureOrigin = spillAclMemoRefusalOrigin(retryError)
475
+ ?? (exhaustedAclRetry ? "retry_returned_timeout" : null);
440
476
  throw retryError;
441
477
  }
442
478
  }
@@ -460,7 +496,7 @@ async function runPendingResponseSpill(job: PendingResponseSpill): Promise<void>
460
496
  } catch (error) {
461
497
  if (ref) deleteResponseSpill(ref);
462
498
  if (states.get(job.id) === candidate && !job.cancelled) {
463
- noteSpillWriteFailure(error, exhaustedAclRetry ? "EACLRETRYEXHAUSTED" : undefined);
499
+ noteSpillWriteFailure(error, exhaustedAclRetry ? "EACLRETRYEXHAUSTED" : undefined, aclRetryFailureOrigin);
464
500
  replaceWithSpillFailure(job.id, candidate);
465
501
  deferSupersededSpill(job.supersededSpill);
466
502
  }
@@ -2188,6 +2224,9 @@ export interface ResponseStateMetrics {
2188
2224
  spillWriteStatus: ResponseSpillWriteStatus;
2189
2225
  spillWriteConsecutiveFailures: number;
2190
2226
  spillLastWriteFailureCode: ResponseSpillWriteFailureCode | null;
2227
+ spillLastWriteFailureOrigin: ResponseSpillWriteFailureOrigin | null;
2228
+ spillAclRetryReturnedTimeouts: number;
2229
+ spillAclTimeoutMemoRefusals: number;
2191
2230
  spillLastWriteFailureAt: number | null;
2192
2231
  spillLastWriteSuccessAt: number | null;
2193
2232
  spillReadFailures: number;
@@ -2240,6 +2279,9 @@ export function responseStateMetrics(): ResponseStateMetrics {
2240
2279
  : "initial",
2241
2280
  spillWriteConsecutiveFailures: spillWriteHealth.consecutiveFailures,
2242
2281
  spillLastWriteFailureCode: spillWriteHealth.lastFailureCode,
2282
+ spillLastWriteFailureOrigin: spillWriteHealth.lastFailureOrigin,
2283
+ spillAclRetryReturnedTimeouts: spillCounters.aclRetryReturnedTimeouts,
2284
+ spillAclTimeoutMemoRefusals: spillCounters.aclTimeoutMemoRefusals,
2243
2285
  spillLastWriteFailureAt: spillWriteHealth.lastFailureAt,
2244
2286
  spillLastWriteSuccessAt: spillWriteHealth.lastSuccessAt,
2245
2287
  spillReadFailures: spillCounters.readFailures,
@@ -2360,8 +2402,11 @@ export function clearResponseStateMemoryForTests(): void {
2360
2402
  spillCounters.writes = 0;
2361
2403
  spillCounters.writeFailures = 0;
2362
2404
  spillCounters.readFailures = 0;
2405
+ spillCounters.aclRetryReturnedTimeouts = 0;
2406
+ spillCounters.aclTimeoutMemoRefusals = 0;
2363
2407
  spillWriteHealth.consecutiveFailures = 0;
2364
2408
  spillWriteHealth.lastFailureCode = null;
2409
+ spillWriteHealth.lastFailureOrigin = null;
2365
2410
  spillWriteHealth.lastFailureAt = null;
2366
2411
  spillWriteHealth.lastSuccessAt = null;
2367
2412
  replayScopeMismatchDrops = 0;
@@ -46,6 +46,7 @@ import {
46
46
  type TranslatorBudget,
47
47
  } from "../lib/translator-budget";
48
48
  import { handleNativeChatCompletions, isNativeChatRouteEligible } from "./chat-native";
49
+ import { jsonCompletionSse } from "./chat-native-sse";
49
50
  import { parseRequestEffortRowId } from "./effort-row";
50
51
  import { parseSyntheticRowId } from "./fast-row";
51
52
  import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers";
@@ -79,6 +80,14 @@ export async function handleChatCompletions(
79
80
  );
80
81
  } catch (error) {
81
82
  translatorBudget.dispose();
83
+ if (isTranslatorBudgetExceededError(error)) {
84
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 502, { closeReason: "non_stream" });
85
+ return chatCompletionsErrorResponse(502, "upstream translation buffer exceeded the safe limit", "upstream_error", "translation_buffer_limit");
86
+ }
87
+ if (isChatCompletionsStreamError(error)) {
88
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, error.status, { closeReason: "non_stream" });
89
+ return chatCompletionsErrorResponse(error.status, error.message, error.type, error.code);
90
+ }
82
91
  throw error;
83
92
  }
84
93
  }
@@ -277,7 +286,7 @@ async function handleChatCompletionsWithBudget(
277
286
  });
278
287
 
279
288
  let nativeLogged = false;
280
- const finalizeNativeLog = (status: number, meta: { terminalStatus?: RequestLogEntry["terminalStatus"]; closeReason: "terminal" | "client_cancel" }) => {
289
+ const finalizeNativeLog = (status: number, meta: { terminalStatus?: RequestLogEntry["terminalStatus"]; closeReason: "terminal" | "client_cancel" | "non_stream" }) => {
281
290
  if (!logIds || nativeLogged) return;
282
291
  nativeLogged = true;
283
292
  addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
@@ -378,11 +387,14 @@ async function handleChatCompletionsWithBudget(
378
387
  : rewritten;
379
388
  }
380
389
 
381
- const response = logIds
390
+ const contentType = upstream.headers.get("content-type") ?? "";
391
+ // JSON is not complete for the client until its Chat projection succeeds.
392
+ // Logging the upstream JSON body here would persist 200 before a later
393
+ // conversion/serialization error, double-counting both the request and usage.
394
+ const response = logIds && contentType.includes("text/event-stream")
382
395
  ? responseWithDeferredRequestLog(upstream, logIds.requestId, logIds.start, logCtx)
383
396
  : upstream;
384
397
 
385
- const contentType = response.headers.get("content-type") ?? "";
386
398
  if (contentType.includes("text/event-stream") && response.body) {
387
399
  const chatSse = responsesSseToChatCompletionsSse(response.body, requestedModel, { translatorBudget });
388
400
  if (stream) {
@@ -416,11 +428,15 @@ async function handleChatCompletionsWithBudget(
416
428
  }
417
429
 
418
430
  // Defensive: JSON despite stream:true.
431
+ const finishJson = (result: Response): Response => {
432
+ finalizeNativeLog(result.status, { closeReason: "non_stream" });
433
+ return result;
434
+ };
419
435
  let json: unknown;
420
436
  try {
421
437
  json = await response.json();
422
438
  } catch {
423
- return chatCompletionsErrorResponse(502, "internal replay returned a non-JSON response", "server_error");
439
+ return finishJson(chatCompletionsErrorResponse(502, "internal replay returned a non-JSON response", "server_error"));
424
440
  }
425
441
  const status = (json as Rec)?.status;
426
442
  if (status === "failed") {
@@ -438,44 +454,24 @@ async function handleChatCompletionsWithBudget(
438
454
  classified.code = "model_not_found";
439
455
  classified.type = "invalid_request_error";
440
456
  }
441
- return chatCompletionsErrorResponse(
457
+ return finishJson(chatCompletionsErrorResponse(
442
458
  classified.code === "translation_buffer_limit"
443
459
  ? 502
444
460
  : isCyberPolicyCode(classified.code) ? 400 : 502,
445
461
  message,
446
462
  classified.type,
447
463
  classified.code,
448
- );
464
+ ));
449
465
  }
450
- const completion = responsesJsonToChatCompletion(json, requestedModel);
451
- if (!stream) {
452
- return new Response(JSON.stringify(completion), {
453
- status: 200,
454
- headers: { "Content-Type": "application/json" },
455
- });
456
- }
457
-
458
- // Streaming client + JSON upstream: synthesize a minimal Chat Completions stream.
459
- const encoder = new TextEncoder();
460
- const id = typeof completion.id === "string" ? completion.id : `chatcmpl-${Date.now()}`;
461
- const created = typeof completion.created === "number" ? completion.created : Math.floor(Date.now() / 1000);
462
- const message = isRec((completion.choices as Rec[] | undefined)?.[0])
463
- ? ((completion.choices as Rec[])[0] as Rec).message as Rec | undefined
464
- : undefined;
465
- const content = message && typeof message.content === "string" ? message.content : "";
466
- const frames = [
467
- `data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model: requestedModel, choices: [{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null }] })}\n\n`,
468
- ...(content
469
- ? [`data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model: requestedModel, choices: [{ index: 0, delta: { content }, finish_reason: null }] })}\n\n`]
470
- : []),
471
- `data: ${JSON.stringify({ id, object: "chat.completion.chunk", created, model: requestedModel, choices: [{ index: 0, delta: {}, finish_reason: "stop" }], usage: completion.usage })}\n\n`,
472
- "data: [DONE]\n\n",
473
- ];
474
- return new Response(encoder.encode(frames.join("")), {
466
+ const completion = responsesJsonToChatCompletion(json, requestedModel, translatorBudget);
467
+ const body = stream
468
+ ? jsonCompletionSse(completion, requestedModel, translatorBudget)
469
+ : JSON.stringify(completion);
470
+ if (!stream) translatorBudget.chargeRetained(Buffer.byteLength(body) * 2, { kind: "live_transient" });
471
+ return finishJson(new Response(body, {
475
472
  status: 200,
476
- headers: {
477
- "Content-Type": "text/event-stream; charset=utf-8",
478
- "Cache-Control": "no-cache",
479
- },
480
- });
473
+ headers: stream
474
+ ? { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache", Connection: "keep-alive" }
475
+ : { "Content-Type": "application/json" },
476
+ }));
481
477
  }
@@ -61,7 +61,7 @@ function normalizedChunk(value: Rec, requestedModel: string): Rec {
61
61
  };
62
62
  }
63
63
 
64
- export function jsonCompletionSse(value: Rec, requestedModel: string): string {
64
+ export function jsonCompletionSse(value: Rec, requestedModel: string, budget?: TranslatorBudget): string {
65
65
  const id = typeof value.id === "string" ? value.id : `chatcmpl-${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
66
66
  const created = typeof value.created === "number" ? value.created : Math.floor(Date.now() / 1000);
67
67
  const model = requestedModel;
@@ -77,11 +77,12 @@ export function jsonCompletionSse(value: Rec, requestedModel: string): string {
77
77
  }];
78
78
  const delta: Rec = {};
79
79
  if (typeof message.content === "string" && message.content.length > 0) delta.content = message.content;
80
+ if (typeof message.refusal === "string") delta.refusal = message.refusal;
80
81
  if (typeof message.reasoning_content === "string" && message.reasoning_content.length > 0) {
81
82
  delta.reasoning_content = message.reasoning_content;
82
83
  }
83
84
  if (Array.isArray(message.tool_calls) && message.tool_calls.length > 0) {
84
- delta.tool_calls = message.tool_calls.map((tool, index) => isRec(tool) ? { index, ...tool } : tool);
85
+ delta.tool_calls = message.tool_calls.filter(isRec).map((tool, index) => ({ ...tool, index }));
85
86
  }
86
87
  if (Object.keys(delta).length > 0) {
87
88
  frames.push({ id, object: "chat.completion.chunk", created, model, choices: [{ index: 0, delta, finish_reason: null }] });
@@ -94,7 +95,26 @@ export function jsonCompletionSse(value: Rec, requestedModel: string): string {
94
95
  choices: [{ index: 0, delta: {}, finish_reason: typeof choice.finish_reason === "string" ? choice.finish_reason : "stop" }],
95
96
  ...(value.usage !== undefined ? { usage: value.usage } : {}),
96
97
  });
97
- return `${frames.map(frame => `data: ${JSON.stringify(frame)}\n\n`).join("")}data: [DONE]\n\n`;
98
+ // Keep the frame strings charged while the joined body is allocated. The final
99
+ // string and Response's UTF-8 body coexist until response ownership ends.
100
+ const scope = { kind: "live_transient" as const };
101
+ let frameBytes = 0;
102
+ const serialized: string[] = [];
103
+ try {
104
+ for (const frame of frames) {
105
+ const text = `data: ${JSON.stringify(frame)}\n\n`;
106
+ const bytes = Buffer.byteLength(text);
107
+ budget?.chargeRetained(bytes, scope);
108
+ frameBytes += bytes;
109
+ serialized.push(text);
110
+ }
111
+ const done = "data: [DONE]\n\n";
112
+ const outputBytes = frameBytes + Buffer.byteLength(done);
113
+ budget?.chargeRetained(outputBytes * 2, scope);
114
+ return serialized.join("") + done;
115
+ } finally {
116
+ budget?.releaseRetained(frameBytes, scope);
117
+ }
98
118
  }
99
119
 
100
120
  interface NativeChatSseOptions {
@@ -502,15 +502,22 @@ export async function handleNativeChatCompletions(options: HandleNativeChatOptio
502
502
  attempt.usage = usage;
503
503
  }
504
504
  if (logIds) recordFirstOutput(logCtx, logIds.start);
505
- finishLog(200);
506
- if (requestedStream) {
507
- return new Response(jsonCompletionSse(completion, requestedModel), {
505
+ try {
506
+ const serialized = requestedStream
507
+ ? jsonCompletionSse(completion, requestedModel, translatorBudget)
508
+ : JSON.stringify(completion);
509
+ if (!requestedStream) translatorBudget.chargeRetained(Buffer.byteLength(serialized) * 2, { kind: "live_transient" });
510
+ finishLog(200);
511
+ return new Response(serialized, {
508
512
  status: 200,
509
- headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache" },
513
+ headers: requestedStream
514
+ ? { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache" }
515
+ : { "Content-Type": "application/json" },
510
516
  });
517
+ } catch (error) {
518
+ if (isTranslatorBudgetExceededError(error)) {
519
+ return fail(502, "upstream translation buffer exceeded the safe limit", "upstream_error", "translation_buffer_limit");
520
+ }
521
+ throw error;
511
522
  }
512
- return new Response(JSON.stringify(completion), {
513
- status: 200,
514
- headers: { "Content-Type": "application/json" },
515
- });
516
523
  }
@@ -16,6 +16,7 @@ import { resolveAlias, claudeCodeNativeAlias } from "../claude/alias";
16
16
  import { recordDesktopRequest } from "../claude/desktop-health";
17
17
  import { stripOneMillionMarker } from "../claude/context-windows";
18
18
  import { captureClaudeInbound } from "../claude/inbound-debug";
19
+ import { analyzeClaudeCompatibility, isClaudeCompatibilityMode } from "../claude/compatibility";
19
20
  import { isTransientUpstreamStatus } from "../lib/upstream-retry";
20
21
  import { resolveClientRetryAfter } from "../lib/retry-after";
21
22
  import {
@@ -719,6 +720,32 @@ async function handleClaudeMessagesWithBudget(
719
720
  if (!effortRow && !fastRow && isRec(anthropicBody) && wantsNativePassthrough(req, config, requestPolicy, anthropicBody.model)) {
720
721
  return await anthropicNativePassthrough(req, config, logCtx, logIds, anthropicBody, "/v1/messages");
721
722
  }
723
+ // Capture source semantics before effort rewriting or translation drops fields.
724
+ // This policy is uniform across translated targets, including later fallback attempts.
725
+ const compatibilityMode: unknown = config.claudeCode?.compatibility;
726
+ if (compatibilityMode !== undefined) {
727
+ if (!isClaudeCompatibilityMode(compatibilityMode)) {
728
+ logCtx.errorCode = "claude_compatibility_configuration";
729
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 503, { closeReason: "non_stream" });
730
+ return anthropicErrorResponse(503, "Invalid claudeCode.compatibility setting", "api_error");
731
+ }
732
+ const compatibility = analyzeClaudeCompatibility(anthropicBody, {
733
+ mode: compatibilityMode,
734
+ anthropicBeta: req.headers.get("anthropic-beta") ?? undefined,
735
+ });
736
+ if (compatibility.decision === "reject") {
737
+ logCtx.errorCode = "claude_compatibility_unsupported";
738
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 400, { closeReason: "non_stream" });
739
+ return anthropicErrorResponse(400, compatibility.reason!, "invalid_request_error");
740
+ }
741
+ if (compatibility.decision === "shadow") {
742
+ logCtx.claudeCompatibility = {
743
+ decision: "shadow",
744
+ featureCodes: compatibility.featureCodes,
745
+ reason: compatibility.reason,
746
+ };
747
+ }
748
+ }
722
749
  if (isRec(anthropicBody) && effortOverride) {
723
750
  anthropicBody.output_config = {
724
751
  ...(isRec(anthropicBody.output_config) ? anthropicBody.output_config : {}),
@@ -1528,6 +1528,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1528
1528
  ? nativeFastEligible(model.id)
1529
1529
  : catalogRowFastEligible(model)
1530
1530
  : undefined,
1531
+ { modelPickerOrder: config.modelPickerOrder, featured: config.subagentModels },
1531
1532
  );
1532
1533
  return jsonResponse({ data }, 200, req, policy);
1533
1534
  }
@@ -1562,6 +1563,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1562
1563
  accountNativeSlugs,
1563
1564
  accountNativeSlugsBySelector,
1564
1565
  config.keepNativeChatGptOnV1 === true,
1566
+ config.modelPickerOrder,
1565
1567
  );
1566
1568
  return jsonResponse({
1567
1569
  models: applyNativeVisibility(
@@ -1763,7 +1765,9 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
1763
1765
  return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1764
1766
  let response: Response;
1765
1767
  try {
1766
- response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease, admission);
1768
+ response = await handleResponsesCompact(req, config, logCtx, turnAdmissionLease, admission, {
1769
+ onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
1770
+ });
1767
1771
  } catch {
1768
1772
  response = formatErrorResponse(500, "server_error", "Unexpected compact request failure");
1769
1773
  }
@@ -1,7 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { readFileSync } from "node:fs";
3
3
  import type { CatalogModel } from "../../codex/catalog";
4
- import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
4
+ import { catalogModelSlug, filterCatalogVisibleModels, invalidateCodexModelsCache, nativeContextLimits, nativeModelRows, uniqueCatalogModelsForPublicList } from "../../codex/catalog";
5
+ import { captureConfigTopLevelRollback, parsedConfigRebaseDeletionKeys, projectConfigRebaseProvenance } from "../../config/rebase-provenance";
5
6
  import {
6
7
  DEFAULT_SUBAGENT_MODELS,
7
8
  codexAutoStartEnabled,
@@ -622,10 +623,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
622
623
  return jsonResponse({ ok: true, effortCap: config.effortCap ?? null, subagentEffortCap: config.subagentEffortCap ?? null });
623
624
  }
624
625
 
625
- // Subagent model picker: which ≤5 routed models Codex's spawn_agent advertises (it shows the
626
- // first 5 routed catalog entries). PUT reorders the injected catalog so the chosen ones lead.
626
+ // Featured roster and saved picker order are separate settings. Native Codex advertises
627
+ // the first five eligible visible rows by display priority; OCX guidance uses natural ranks.
627
628
  if (url.pathname === "/api/subagent-models" && req.method === "GET") {
628
- const models = await fetchAllModels(config);
629
+ const models = await (deps.fetchAllModels ?? fetchAllModels)(config);
629
630
  const disabled = new Set(config.disabledModels ?? []);
630
631
  // Native gpt (passthrough) are also valid subagent picks — they're picker-visible models in the
631
632
  // catalog, just buried by priority. List them first so the user can feature them over routed.
@@ -657,19 +658,105 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
657
658
  // in-memory catalog than the one on disk.
658
659
  const { collectCodexAppServerCatalogState } = await import("../../codex/app-server-processes");
659
660
  const catalogState = collectCodexAppServerCatalogState();
660
- return jsonResponse({ chosen, available, catalogState });
661
+ return jsonResponse({
662
+ chosen, available, catalogState,
663
+ pickerAvailable: [...new Set(filterCatalogVisibleModels(models, config).map(catalogModelSlug).filter(slug => slug.includes("/")))],
664
+ pickerOrder: config.modelPickerOrder ?? [],
665
+ pickerOrderMode: config.modelPickerOrderMode ?? null,
666
+ });
661
667
  }
662
668
  if (url.pathname === "/api/subagent-models" && req.method === "PUT") {
663
- let body: { models?: unknown };
664
- try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
665
- const chosen = Array.isArray(body.models) ? body.models.filter((m): m is string => typeof m === "string").slice(0, 5) : [];
666
- config.subagentModels = chosen;
667
- const { saveConfigPreservingClaudeCode: save } = await import("../../config");
668
- save(config);
669
+ let rawBody: unknown;
670
+ try { rawBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
671
+ if (!isPlainRecord(rawBody)) return jsonResponse({ error: "JSON body must be an object" }, 400);
672
+ const body = rawBody as { models?: unknown; pickerOrder?: unknown; pickerOrderMode?: unknown };
673
+ const updatesRoster = body.models !== undefined;
674
+ const updatesPicker = body.pickerOrder !== undefined;
675
+ if (!updatesRoster && !updatesPicker) return jsonResponse({ error: "models or pickerOrder is required" }, 400);
676
+ let chosen: string[] | undefined;
677
+ if (updatesRoster) {
678
+ if (!Array.isArray(body.models) || body.models.some(model => typeof model !== "string")) {
679
+ return jsonResponse({ error: "models must be an array of strings" }, 400);
680
+ }
681
+ // Keep the original valid roster contract: no discovery validation, trimming or deduping.
682
+ chosen = body.models.slice(0, 5);
683
+ }
684
+ const mode = body.pickerOrderMode;
685
+ if (mode !== undefined && (!updatesPicker || (mode !== null
686
+ && mode !== "alphabetical" && mode !== "provider" && mode !== "most-used"))) {
687
+ return jsonResponse({ error: "pickerOrderMode requires pickerOrder and must be alphabetical, provider, most-used, or null" }, 400);
688
+ }
689
+ let pickerOrder: string[] | undefined;
690
+ if (updatesPicker) {
691
+ if (body.pickerOrder !== null && (!Array.isArray(body.pickerOrder)
692
+ || body.pickerOrder.some(model => typeof model !== "string" || model.trim() === ""))) {
693
+ return jsonResponse({ error: "pickerOrder must be an array of non-empty routed model ids, or null" }, 400);
694
+ }
695
+ pickerOrder = body.pickerOrder === null ? [] : (body.pickerOrder as string[]).map(model => model.trim());
696
+ if (new Set(pickerOrder).size !== pickerOrder.length) {
697
+ return jsonResponse({ error: "pickerOrder must not contain duplicate ids" }, 400);
698
+ }
699
+ if (pickerOrder.length > 0) {
700
+ const models = await (deps.fetchAllModels ?? fetchAllModels)(config);
701
+ // Evaluate visibility AFTER discovery: a concurrent visibility write may have completed.
702
+ const visible = new Set(filterCatalogVisibleModels(models, config).map(catalogModelSlug).filter(slug => slug.includes("/")));
703
+ if (pickerOrder.some(model => !visible.has(model))) {
704
+ return jsonResponse({ error: "pickerOrder must contain each visible routed model at most once" }, 400);
705
+ }
706
+ }
707
+ }
708
+
709
+ // Everything above can await. From this snapshot through persistence there is no yield.
710
+ // Stage deletion intent before adopting the touched fields through the canonical
711
+ // live deletion owner. A failed save restores both fields and pending intent.
712
+ if (updatesPicker && config.configRebaseProvenance !== undefined
713
+ && parsedConfigRebaseDeletionKeys(config) === null) {
714
+ // A newer provenance format must not silently discard this clear's intent on rebase.
715
+ return jsonResponse({ error: "unsupported config deletion provenance" }, 409);
716
+ }
717
+ const draft = { ...projectConfigRebaseProvenance(config) };
718
+ if (chosen !== undefined) draft.subagentModels = chosen;
719
+ if (pickerOrder !== undefined) {
720
+ if (pickerOrder.length === 0) {
721
+ deleteConfigTopLevelKey(draft, "modelPickerOrder");
722
+ deleteConfigTopLevelKey(draft, "modelPickerOrderMode");
723
+ } else {
724
+ draft.modelPickerOrder = pickerOrder;
725
+ if (mode === "alphabetical" || mode === "provider" || mode === "most-used") draft.modelPickerOrderMode = mode;
726
+ else deleteConfigTopLevelKey(draft, "modelPickerOrderMode");
727
+ }
728
+ }
729
+ const projected = projectConfigRebaseProvenance(draft);
730
+ const touched = [
731
+ ...(updatesRoster ? ["subagentModels" as const] : []),
732
+ ...(updatesPicker ? ["modelPickerOrder" as const, "modelPickerOrderMode" as const] : []),
733
+ "configRebaseProvenance" as const,
734
+ ];
735
+ const rollback = captureConfigTopLevelRollback(config, touched);
736
+ try {
737
+ for (const key of touched) {
738
+ if (Object.hasOwn(projected, key)) Object.defineProperty(config, key, {
739
+ value: projected[key], writable: true, enumerable: true, configurable: true,
740
+ });
741
+ else deleteConfigTopLevelKey(config, key);
742
+ }
743
+ (deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
744
+ } catch (error) {
745
+ rollback();
746
+ throw error;
747
+ }
748
+ // Capture the result before convergence yields to another settings mutation.
749
+ const saved = {
750
+ applied: [...(config.subagentModels ?? [])],
751
+ pickerOrder: [...(config.modelPickerOrder ?? [])],
752
+ pickerOrderMode: config.modelPickerOrderMode ?? null,
753
+ };
669
754
  const catalogRefresh = await convergeCodexCatalog();
670
- await syncClaudeAgentDefsBestEffort();
671
- await autoApplyDesktopBestEffort();
672
- return jsonResponse({ ok: true, applied: chosen, catalogRefresh });
755
+ if (updatesRoster) {
756
+ await syncClaudeAgentDefsBestEffort();
757
+ await autoApplyDesktopBestEffort();
758
+ }
759
+ return jsonResponse({ ok: true, ...saved, catalogRefresh });
673
760
  }
674
761
 
675
762
  // Priority-ordered subagent model fallback chain for quota-aware spawn routing.
@@ -66,6 +66,7 @@ import {
66
66
  import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
67
67
  import { drainAndShutdown } from "../lifecycle";
68
68
  import { filterRequestLogs, filteredRequestLogCount, getRequestLogEntries, type RequestLogEntry } from "../request-log";
69
+ import { decodeRequestLogCursor, selectRequestLogPoll } from "../request-log-cursor";
69
70
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../../usage/cost";
70
71
  import { userCostOverlayVersion } from "../../usage/user-cost-overlays";
71
72
  import type { PersistedUsageAttempt } from "../../usage/log";
@@ -106,14 +107,20 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
106
107
  const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;
107
108
 
108
109
  if (url.pathname === "/api/logs" && req.method === "GET") {
110
+ const rawCursor = url.searchParams.get("cursor");
111
+ const cursor = rawCursor === null ? null : decodeRequestLogCursor(rawCursor);
112
+ if (rawCursor !== null && cursor === null) {
113
+ return jsonResponse({ error: { code: "invalid_cursor", message: "invalid cursor" } }, 400);
114
+ }
109
115
  const all = getRequestLogEntries();
110
116
  const total = filteredRequestLogCount(all, url.searchParams);
111
- const logs = filterRequestLogs(all, url.searchParams);
117
+ const logs = filterRequestLogs(all, url.searchParams).map(requestLogDto);
118
+ const poll = selectRequestLogPoll(logs, url.searchParams, cursor);
112
119
  return jsonResponse({
113
120
  timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
114
121
  generatedAt: Date.now(),
115
122
  total,
116
- logs: logs.map(requestLogDto),
123
+ ...poll,
117
124
  });
118
125
  }
119
126