@bitkyc08/opencodex 2.8.0 → 2.9.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 (175) hide show
  1. package/README.md +24 -0
  2. package/bin/ocx.mjs +32 -4
  3. package/gui/dist/assets/index-CHwf3tTD.css +1 -0
  4. package/gui/dist/assets/index-u5eFOv2y.js +67 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic-image-normalize.ts +114 -20
  8. package/src/adapters/anthropic.ts +126 -10
  9. package/src/adapters/azure.ts +3 -3
  10. package/src/adapters/base.ts +7 -3
  11. package/src/adapters/cursor/discovery.ts +14 -4
  12. package/src/adapters/cursor/effort-map.ts +18 -6
  13. package/src/adapters/cursor/framing.ts +102 -27
  14. package/src/adapters/cursor/kv-store.ts +30 -3
  15. package/src/adapters/cursor/live-models.ts +22 -2
  16. package/src/adapters/cursor/live-transport.ts +245 -49
  17. package/src/adapters/cursor/mcp-manager.ts +105 -8
  18. package/src/adapters/cursor/native-exec-mcp.ts +5 -3
  19. package/src/adapters/cursor/native-exec-shell.ts +296 -14
  20. package/src/adapters/cursor/native-exec.ts +381 -33
  21. package/src/adapters/cursor/protobuf-events.ts +28 -1
  22. package/src/adapters/cursor/protobuf-request.ts +71 -39
  23. package/src/adapters/cursor/request-builder.ts +2 -2
  24. package/src/adapters/cursor/transport.ts +2 -0
  25. package/src/adapters/cursor.ts +13 -2
  26. package/src/adapters/google-antigravity-replay.ts +184 -17
  27. package/src/adapters/google.ts +58 -8
  28. package/src/adapters/kiro-thinking.ts +23 -9
  29. package/src/adapters/kiro-tools.ts +49 -18
  30. package/src/adapters/kiro.ts +377 -133
  31. package/src/adapters/mimo-free.ts +36 -4
  32. package/src/adapters/openai-chat.ts +143 -17
  33. package/src/adapters/openai-responses.ts +130 -14
  34. package/src/adapters/run-turn-queue.ts +7 -1
  35. package/src/bridge.ts +466 -69
  36. package/src/chat/outbound.ts +144 -38
  37. package/src/claude/inbound-debug.ts +53 -8
  38. package/src/claude/outbound.ts +224 -38
  39. package/src/cli/agent-driven.ts +34 -1
  40. package/src/cli/catalog-prewarm.ts +5 -2
  41. package/src/cli/claude-desktop.ts +2 -2
  42. package/src/cli/doctor.ts +12 -0
  43. package/src/cli/export-command.ts +187 -0
  44. package/src/cli/help.ts +11 -0
  45. package/src/cli/index.ts +13 -3
  46. package/src/cli/init.ts +129 -102
  47. package/src/cli/opencode.ts +36 -151
  48. package/src/cli/star-prompt.ts +13 -4
  49. package/src/cli/status-oauth.ts +12 -2
  50. package/src/clients/config-export.ts +377 -0
  51. package/src/codex/account-runtime-state.ts +19 -1
  52. package/src/codex/account-store.ts +162 -82
  53. package/src/codex/auth-api.ts +467 -159
  54. package/src/codex/auth-context.ts +15 -2
  55. package/src/codex/catalog/aggregation.ts +15 -0
  56. package/src/codex/catalog/effort.ts +16 -6
  57. package/src/codex/catalog/metadata.ts +6 -0
  58. package/src/codex/catalog/parsing.ts +3 -1
  59. package/src/codex/catalog/provider-fetch.ts +29 -0
  60. package/src/codex/catalog/sync.ts +64 -7
  61. package/src/codex/catalog.ts +2 -2
  62. package/src/codex/inject.ts +5 -5
  63. package/src/codex/main-account-cache.ts +8 -1
  64. package/src/codex/model-cache.ts +81 -2
  65. package/src/codex/pool-rotation.ts +39 -0
  66. package/src/codex/project-config-warnings.ts +12 -1
  67. package/src/codex/quota.ts +35 -3
  68. package/src/codex/routing.ts +46 -1
  69. package/src/codex/shim.ts +10 -4
  70. package/src/codex/subagent-model-fallback.ts +12 -0
  71. package/src/codex/websocket-registry.ts +27 -0
  72. package/src/combos/failover.ts +31 -1
  73. package/src/combos/request.ts +9 -0
  74. package/src/combos/resolve.ts +60 -4
  75. package/src/combos/types.ts +12 -0
  76. package/src/config.ts +510 -55
  77. package/src/github/star-state.ts +13 -1
  78. package/src/images/fulfill.ts +39 -1
  79. package/src/images/loop.ts +52 -12
  80. package/src/lib/admission.ts +83 -0
  81. package/src/lib/app-owned-memory-stores.ts +173 -0
  82. package/src/lib/app-owned-memory.ts +265 -0
  83. package/src/lib/bun-stream-caps.ts +31 -7
  84. package/src/lib/config-ownership.ts +33 -0
  85. package/src/lib/crash-guard.ts +65 -5
  86. package/src/lib/debug-log-buffer.ts +47 -6
  87. package/src/lib/destination-policy.ts +12 -1
  88. package/src/lib/errors.ts +3 -0
  89. package/src/lib/gcp-adc.ts +40 -2
  90. package/src/lib/injection-debug-log.ts +26 -2
  91. package/src/lib/provider-outbound.ts +3 -0
  92. package/src/lib/sidecar-tracker.ts +5 -2
  93. package/src/lib/sse-decoder.ts +257 -37
  94. package/src/lib/state-store-registrations.ts +109 -0
  95. package/src/lib/state-store-sweeper.ts +184 -0
  96. package/src/lib/translator-budget.ts +356 -0
  97. package/src/lib/windows-secret-acl.ts +33 -12
  98. package/src/lib/winsw.ts +14 -1
  99. package/src/oauth/anthropic-routing.ts +31 -7
  100. package/src/oauth/google-antigravity.ts +2 -1
  101. package/src/oauth/health.ts +30 -12
  102. package/src/oauth/index.ts +127 -23
  103. package/src/oauth/kiro-credentials.ts +72 -1
  104. package/src/oauth/kiro.ts +23 -4
  105. package/src/oauth/store.ts +165 -18
  106. package/src/oauth/token-guardian.ts +43 -4
  107. package/src/oauth/types.ts +2 -1
  108. package/src/providers/base-url-choices.ts +10 -0
  109. package/src/providers/derive.ts +12 -0
  110. package/src/providers/free-directory.ts +4 -1
  111. package/src/providers/key-failover.ts +12 -0
  112. package/src/providers/openai-sidecar.ts +4 -1
  113. package/src/providers/quota.ts +68 -7
  114. package/src/providers/registry.ts +279 -3
  115. package/src/responses/parser.ts +5 -1
  116. package/src/responses/spill-store.ts +394 -0
  117. package/src/responses/state.ts +520 -102
  118. package/src/router.ts +18 -1
  119. package/src/server/adapter-resolve.ts +20 -3
  120. package/src/server/auth-cors.ts +121 -28
  121. package/src/server/chat-completions.ts +57 -12
  122. package/src/server/claude-messages.ts +85 -13
  123. package/src/server/index.ts +242 -100
  124. package/src/server/lifecycle.ts +155 -25
  125. package/src/server/management/agent-settings-routes.ts +79 -36
  126. package/src/server/management/api-key-usage.ts +167 -0
  127. package/src/server/management/body.ts +35 -0
  128. package/src/server/management/combo-routes.ts +5 -1
  129. package/src/server/management/config-routes.ts +42 -12
  130. package/src/server/management/logs-usage-routes.ts +41 -21
  131. package/src/server/management/model-routes.ts +188 -54
  132. package/src/server/management/oauth-account-routes.ts +115 -26
  133. package/src/server/management/provider-routes.ts +56 -6
  134. package/src/server/management/shared.ts +16 -3
  135. package/src/server/management/sidebar-routes.ts +50 -1
  136. package/src/server/management/system-restart.ts +13 -6
  137. package/src/server/management/system-routes.ts +15 -3
  138. package/src/server/management/usage-summary-cache.ts +86 -0
  139. package/src/server/management-api.ts +39 -5
  140. package/src/server/management-auth.ts +65 -14
  141. package/src/server/port-reclaim.ts +58 -12
  142. package/src/server/ports.ts +2 -0
  143. package/src/server/proxy-liveness.ts +60 -14
  144. package/src/server/relay-eager.ts +20 -4
  145. package/src/server/relay.ts +548 -154
  146. package/src/server/request-decompress.ts +51 -4
  147. package/src/server/request-log.ts +134 -15
  148. package/src/server/responses/collaboration.ts +15 -4
  149. package/src/server/responses/compact.ts +3 -0
  150. package/src/server/responses/core.ts +241 -66
  151. package/src/server/responses-image-gen-repair.ts +19 -5
  152. package/src/server/responses-item-id-repair.ts +23 -5
  153. package/src/server/sse-payload-rewrite.ts +71 -12
  154. package/src/server/startup-health-cache.ts +14 -1
  155. package/src/server/system-env.ts +8 -1
  156. package/src/server/windows-tcp-drop.ts +15 -5
  157. package/src/server/ws-bridge.ts +25 -0
  158. package/src/service.ts +179 -13
  159. package/src/storage/policy-job.ts +93 -23
  160. package/src/storage/policy-worker.ts +6 -0
  161. package/src/storage/restore-job.ts +62 -16
  162. package/src/storage/restore-worker.ts +6 -0
  163. package/src/storage/storage-mutation-coordinator.ts +36 -6
  164. package/src/storage/worker-lifecycle.ts +181 -47
  165. package/src/tray/windows.ts +97 -25
  166. package/src/types.ts +39 -6
  167. package/src/update/index.ts +24 -5
  168. package/src/update/job.ts +598 -73
  169. package/src/usage/log.ts +115 -17
  170. package/src/usage/summary.ts +67 -2
  171. package/src/vision/index.ts +112 -22
  172. package/src/web-search/loop.ts +38 -6
  173. package/src/web-search/progress-stream.ts +14 -3
  174. package/gui/dist/assets/index-BDjpkcRN.js +0 -67
  175. package/gui/dist/assets/index-BHsKRFh9.css +0 -1
@@ -1,4 +1,5 @@
1
1
  import { gunzipSync, inflateRawSync, inflateSync, zstdDecompressSync } from "node:zlib";
2
+ import type { TranslatorBudget } from "../lib/translator-budget";
2
3
 
3
4
  /**
4
5
  * Request-body decompression for the /v1/responses data plane.
@@ -37,6 +38,13 @@ function assertBodySizeWithinLimit(body: Uint8Array, maxBytes: number): Uint8Arr
37
38
  return body;
38
39
  }
39
40
 
41
+ function declaredBodyLength(req: Request): number | null {
42
+ const raw = req.headers.get("content-length");
43
+ if (raw === null || raw.trim() === "") return null;
44
+ const length = Number(raw);
45
+ return Number.isFinite(length) && length >= 0 ? length : null;
46
+ }
47
+
40
48
  function inflateDeflateBody(compressed: Uint8Array<ArrayBuffer>, opts: { maxOutputLength: number }): Uint8Array {
41
49
  // HTTP "deflate" appears both zlib-wrapped and raw in the wild (Bun.deflateSync emits raw,
42
50
  // which the previous Bun.inflateSync accepted). Try zlib-wrapped first, fall back to raw —
@@ -77,9 +85,48 @@ export function decodeRequestBody(
77
85
  return assertBodySizeWithinLimit(decoded, maxBytes);
78
86
  }
79
87
 
80
- /** Parse a JSON request body, transparently decoding compressed payloads. */
81
- export async function readJsonRequestBody(req: Request): Promise<unknown> {
88
+ /** Parse a bounded JSON request body, transparently decoding compressed payloads. */
89
+ export async function readBoundedJsonRequestBody(
90
+ req: Request,
91
+ maxBytes: number,
92
+ budget?: TranslatorBudget,
93
+ ): Promise<unknown> {
82
94
  const encoding = req.headers.get("content-encoding");
83
- const decoded = decodeRequestBody(new Uint8Array(await req.arrayBuffer()), encoding);
84
- return JSON.parse(new TextDecoder().decode(decoded));
95
+ const declaredLength = declaredBodyLength(req);
96
+ // Reject an honest oversized declaration before req.arrayBuffer() can allocate it.
97
+ // Missing, malformed, or dishonest declarations remain covered by decodeRequestBody's
98
+ // post-read cap below.
99
+ if (declaredLength !== null && declaredLength > maxBytes) {
100
+ throw new DecompressedBodyTooLargeError(declaredLength, maxBytes);
101
+ }
102
+ const releaseReservation = budget && declaredLength !== null && declaredLength > 0
103
+ ? budget.observeAcceptedRequestCopy(declaredLength)
104
+ : undefined;
105
+ let raw: Uint8Array;
106
+ try {
107
+ raw = new Uint8Array(await req.arrayBuffer());
108
+ } finally {
109
+ releaseReservation?.();
110
+ }
111
+ const releaseRaw = budget?.observeAcceptedRequestCopy(raw.byteLength);
112
+ let releaseDecoded: (() => void) | undefined;
113
+ let releaseText: (() => void) | undefined;
114
+ try {
115
+ const decoded = decodeRequestBody(raw, encoding, maxBytes);
116
+ releaseDecoded = decoded === raw ? undefined : budget?.observeAcceptedRequestCopy(decoded.byteLength);
117
+ const text = new TextDecoder().decode(decoded);
118
+ releaseText = budget?.observeAcceptedRequestCopy(new TextEncoder().encode(text).byteLength);
119
+ const parsed = JSON.parse(text);
120
+ budget?.observeAcceptedRequestCopy(new TextEncoder().encode(JSON.stringify(parsed)).byteLength);
121
+ return parsed;
122
+ } finally {
123
+ releaseText?.();
124
+ releaseDecoded?.();
125
+ releaseRaw?.();
126
+ }
127
+ }
128
+
129
+ /** Parse a JSON data-plane body using the shared 256 MiB admission cap. */
130
+ export function readJsonRequestBody(req: Request, budget?: TranslatorBudget): Promise<unknown> {
131
+ return readBoundedJsonRequestBody(req, MAX_DECOMPRESSED_BODY_BYTES, budget);
85
132
  }
@@ -12,6 +12,8 @@ import type { AdapterRequest } from "../adapters/base";
12
12
  import { redactSecretString } from "../lib/redact";
13
13
  import {
14
14
  appendUsageEntry,
15
+ isKnownAdmissionKind,
16
+ isKnownInboundProtocol,
15
17
  isKnownUsageSurface,
16
18
  readRecentUsageEntries,
17
19
  usageForFinalLog,
@@ -30,6 +32,7 @@ import {
30
32
  type UsageDebugBodyKind,
31
33
  } from "../usage/debug";
32
34
  import { matchesLogConversationId } from "./request-log-conversation";
35
+ import { enforceAppOwnedMemoryBudget, type RetainedStoreSnapshot } from "../lib/app-owned-memory";
33
36
 
34
37
  export interface RequestLogContext {
35
38
  model: string;
@@ -39,6 +42,16 @@ export interface RequestLogContext {
39
42
  /** Best-effort chat/session correlation for Logs grouping (#330). Opaque; omit when unknown. */
40
43
  conversationId?: string;
41
44
  surface?: "claude" | "claude-desktop" | "grok";
45
+ /** The matched configured key's id. Set ONLY for admissionKind "configured" —
46
+ * never a sentinel, so a hand-edited entry whose id happens to be "loopback"
47
+ * cannot absorb unrelated traffic. */
48
+ apiKeyId?: string;
49
+ /** Which kind of admission opened this request. Carries no secret. */
50
+ admissionKind?: "configured" | "environment" | "loopback";
51
+ /** Which inbound wire was used. Orthogonal to `surface`, which names the client
52
+ * product: widening that enum would merge Responses and Chat Completions,
53
+ * since both leave it undefined. */
54
+ inboundProtocol?: "responses" | "chat" | "messages";
42
55
  requestedModel?: string;
43
56
  /** Internal structural combo identity; omitted from RequestLogEntry/JSONL. */
44
57
  comboId?: string;
@@ -92,6 +105,16 @@ export interface RequestLogEntry {
92
105
  /** TTFT: ms from request start to the first non-empty model output delta; unset for non-streaming/tool-only. */
93
106
  firstOutputMs?: number;
94
107
  surface?: "claude" | "claude-desktop" | "grok";
108
+ /** The matched configured key's id. Set ONLY for admissionKind "configured" —
109
+ * never a sentinel, so a hand-edited entry whose id happens to be "loopback"
110
+ * cannot absorb unrelated traffic. */
111
+ apiKeyId?: string;
112
+ /** Which kind of admission opened this request. Carries no secret. */
113
+ admissionKind?: "configured" | "environment" | "loopback";
114
+ /** Which inbound wire was used. Orthogonal to `surface`, which names the client
115
+ * product: widening that enum would merge Responses and Chat Completions,
116
+ * since both leave it undefined. */
117
+ inboundProtocol?: "responses" | "chat" | "messages";
95
118
  /** Best-effort chat/session correlation for Logs grouping (#330). */
96
119
  conversationId?: string;
97
120
  requestedModel?: string;
@@ -126,11 +149,49 @@ export interface RequestLogEntry {
126
149
  }
127
150
 
128
151
  const requestLog: RequestLogEntry[] = [];
129
- const MAX_LOG_SIZE = 200;
152
+ const MAX_LOG_SIZE = 2000;
153
+ const requestLogEntryBytes = new WeakMap<RequestLogEntry, number>();
154
+ let requestLogBytes = 0;
130
155
  let requestLogSeq = 0;
131
156
  /** True after hydrateRequestLogsFromDisk ran once in this process. */
132
157
  let requestLogsHydratedFromDisk = false;
133
158
 
159
+ function retainedRequestLogBytes(entry: RequestLogEntry): number {
160
+ return Buffer.byteLength(JSON.stringify(entry), "utf8");
161
+ }
162
+
163
+ function removeOldestRequestLogEntry(): number {
164
+ const entry = requestLog.shift();
165
+ if (!entry) return 0;
166
+ const bytes = requestLogEntryBytes.get(entry) ?? retainedRequestLogBytes(entry);
167
+ requestLogEntryBytes.delete(entry);
168
+ requestLogBytes = Math.max(0, requestLogBytes - bytes);
169
+ return bytes;
170
+ }
171
+
172
+ function retainRequestLogEntry(entry: RequestLogEntry): void {
173
+ const bytes = retainedRequestLogBytes(entry);
174
+ requestLog.push(entry);
175
+ requestLogEntryBytes.set(entry, bytes);
176
+ requestLogBytes += bytes;
177
+ while (requestLog.length > MAX_LOG_SIZE) removeOldestRequestLogEntry();
178
+ enforceAppOwnedMemoryBudget();
179
+ }
180
+
181
+ export function requestLogRetainedStoreSnapshot(): RetainedStoreSnapshot {
182
+ return {
183
+ count: requestLog.length,
184
+ bytes: requestLogBytes,
185
+ evictableBytes: requestLogBytes,
186
+ pinnedBytes: 0,
187
+ oldestAt: requestLog[0]?.timestamp ?? null,
188
+ };
189
+ }
190
+
191
+ export function evictOldestRequestLogForBudget(): number {
192
+ return removeOldestRequestLogEntry();
193
+ }
194
+
134
195
  function asTerminalStatus(value: string | undefined): ResponsesTerminalStatus | undefined {
135
196
  if (value === "completed" || value === "failed" || value === "incomplete") return value;
136
197
  return undefined;
@@ -208,7 +269,7 @@ export function hydrateRequestLogsFromDisk(
208
269
  const slice = persisted.length > MAX_LOG_SIZE
209
270
  ? persisted.slice(persisted.length - MAX_LOG_SIZE)
210
271
  : persisted;
211
- for (const entry of slice) requestLog.push(requestLogEntryFromPersistedUsage(entry));
272
+ for (const entry of slice) retainRequestLogEntry(requestLogEntryFromPersistedUsage(entry));
212
273
  return slice.length;
213
274
  } catch (err) {
214
275
  requestLogsHydratedFromDisk = true;
@@ -220,8 +281,7 @@ export function hydrateRequestLogsFromDisk(
220
281
  }
221
282
 
222
283
  export function addRequestLog(entry: RequestLogEntry) {
223
- requestLog.push(entry);
224
- if (requestLog.length > MAX_LOG_SIZE) requestLog.shift();
284
+ retainRequestLogEntry(entry);
225
285
  try {
226
286
  // Failure diagnostics survive the 200-entry ring buffer by riding the persisted
227
287
  // usage entry (devlog/_plan/260716_claudecode_hardening/030). Success rows stay
@@ -240,6 +300,12 @@ export function addRequestLog(entry: RequestLogEntry) {
240
300
  provider: entry.provider,
241
301
  model: entry.model,
242
302
  ...(isKnownUsageSurface(entry.surface) ? { surface: entry.surface } : {}),
303
+ // This function REBUILDS the persisted row field by field rather than
304
+ // spreading it, so a field missing here reaches /api/logs and never
305
+ // reaches usage.jsonl — which is where the per-key rollup reads from.
306
+ ...(entry.apiKeyId ? { apiKeyId: entry.apiKeyId } : {}),
307
+ ...(isKnownAdmissionKind(entry.admissionKind) ? { admissionKind: entry.admissionKind } : {}),
308
+ ...(isKnownInboundProtocol(entry.inboundProtocol) ? { inboundProtocol: entry.inboundProtocol } : {}),
243
309
  ...(entry.conversationId ? { conversationId: entry.conversationId } : {}),
244
310
  ...(entry.resolvedModel ? { resolvedModel: entry.resolvedModel } : {}),
245
311
  ...(entry.requestedModel ? { requestedModel: entry.requestedModel } : {}),
@@ -506,14 +572,26 @@ export function inspectResponseLogJson(logCtx: RequestLogContext, text: string):
506
572
 
507
573
  export function inspectResponseLogSsePayload(logCtx: RequestLogContext, payload: string | null): void {
508
574
  if (!payload || payload.trim() === "[DONE]") return;
509
- const debugEnabled = isUsageDebugEnabled();
510
- const sseAlreadyMarked = logCtx.usageDebugBodyKind === "sse";
575
+ let parsed: unknown | undefined;
511
576
  try {
512
- applyResponseLogMetadata(logCtx, JSON.parse(payload));
577
+ parsed = JSON.parse(payload);
513
578
  } catch {
514
579
  /* SSE block payload may not be JSON; metadata inspection is best-effort */
515
580
  }
516
- captureUpstreamError(logCtx, payload);
581
+ inspectResponseLogSsePayloadParsed(logCtx, payload, parsed);
582
+ }
583
+
584
+ /** Inspect an SSE payload using the caller's single best-effort JSON parse. */
585
+ export function inspectResponseLogSsePayloadParsed(
586
+ logCtx: RequestLogContext,
587
+ payload: string | null,
588
+ parsed: unknown | undefined,
589
+ ): void {
590
+ if (!payload || payload.trim() === "[DONE]") return;
591
+ const debugEnabled = isUsageDebugEnabled();
592
+ const sseAlreadyMarked = logCtx.usageDebugBodyKind === "sse";
593
+ if (parsed !== undefined) applyResponseLogMetadata(logCtx, parsed);
594
+ captureUpstreamErrorParsed(logCtx, payload, parsed);
517
595
  if (debugEnabled) {
518
596
  if (!sseAlreadyMarked) {
519
597
  logCtx.usageDebugBodyKind = "sse";
@@ -535,8 +613,22 @@ export function inspectResponseLogSsePayload(logCtx: RequestLogContext, payload:
535
613
  */
536
614
  function captureUpstreamError(logCtx: RequestLogContext, text: string | null): void {
537
615
  if (!text) return;
616
+ let parsed: unknown | undefined;
538
617
  try {
539
- const json = JSON.parse(text) as {
618
+ parsed = JSON.parse(text);
619
+ } catch {
620
+ /* retain the raw malformed payload for the bounded fallback below */
621
+ }
622
+ captureUpstreamErrorParsed(logCtx, text, parsed);
623
+ }
624
+
625
+ function captureUpstreamErrorParsed(
626
+ logCtx: RequestLogContext,
627
+ text: string,
628
+ parsed: unknown | undefined,
629
+ ): void {
630
+ if (parsed !== undefined && parsed !== null) {
631
+ const json = parsed as {
540
632
  type?: unknown;
541
633
  error?: { message?: unknown };
542
634
  last_error?: { message?: unknown };
@@ -568,12 +660,12 @@ function captureUpstreamError(logCtx: RequestLogContext, text: string | null): v
568
660
  if (typeof reason === "string" && reason.trim()) {
569
661
  logCtx.upstreamError = redactSecretString(incompleteReasonLabel(reason.trim())).slice(0, 500);
570
662
  }
571
- } catch {
572
- if (logCtx.upstreamError) return;
573
- const trimmed = text.trim();
574
- if (trimmed) {
575
- logCtx.upstreamError = redactSecretString(trimmed).slice(0, 500);
576
- }
663
+ return;
664
+ }
665
+ if (logCtx.upstreamError) return;
666
+ const trimmed = text.trim();
667
+ if (trimmed) {
668
+ logCtx.upstreamError = redactSecretString(trimmed).slice(0, 500);
577
669
  }
578
670
  }
579
671
 
@@ -695,6 +787,9 @@ export function addFinalRequestLog(
695
787
  model: isCombo ? logCtx.requestedModel! : logCtx.model,
696
788
  provider: isCombo ? "combo" : logCtx.provider,
697
789
  ...(logCtx.surface ? { surface: logCtx.surface } : {}),
790
+ ...(logCtx.apiKeyId ? { apiKeyId: logCtx.apiKeyId } : {}),
791
+ ...(logCtx.admissionKind ? { admissionKind: logCtx.admissionKind } : {}),
792
+ ...(logCtx.inboundProtocol ? { inboundProtocol: logCtx.inboundProtocol } : {}),
698
793
  ...(logCtx.conversationId ? { conversationId: logCtx.conversationId } : {}),
699
794
  ...(logCtx.requestedModel ? { requestedModel: logCtx.requestedModel } : {}),
700
795
  ...(logCtx.requestedEffort ? { requestedEffort: logCtx.requestedEffort } : {}),
@@ -760,9 +855,32 @@ export function filterRequestLogs(logs: RequestLogEntry[], params: URLSearchPara
760
855
  const tail = Number.parseInt(tailRaw, 10);
761
856
  if (Number.isFinite(tail) && tail > 0) filtered = filtered.slice(-Math.min(tail, MAX_LOG_SIZE));
762
857
  }
858
+ const offsetRaw = params.get("offset")?.trim();
859
+ const limitRaw = params.get("limit")?.trim();
860
+ if (limitRaw) {
861
+ const limit = Number.parseInt(limitRaw, 10);
862
+ const offset = offsetRaw ? Number.parseInt(offsetRaw, 10) : 0;
863
+ if (Number.isFinite(limit) && limit > 0) {
864
+ const capped = Math.min(limit, MAX_LOG_SIZE);
865
+ const startOffset = Number.isFinite(offset) && offset > 0 ? offset : 0;
866
+ const end = filtered.length - startOffset;
867
+ if (end <= 0) filtered = [];
868
+ else {
869
+ const begin = Math.max(0, end - capped);
870
+ filtered = filtered.slice(begin, end);
871
+ }
872
+ }
873
+ }
763
874
  return filtered;
764
875
  }
765
876
 
877
+ export function filteredRequestLogCount(logs: RequestLogEntry[], params: URLSearchParams): number {
878
+ const withoutPagination = new URLSearchParams(params);
879
+ withoutPagination.delete("limit");
880
+ withoutPagination.delete("offset");
881
+ return filterRequestLogs(logs, withoutPagination).length;
882
+ }
883
+
766
884
  interface FinalizedUsageResult {
767
885
  usage?: OcxUsage;
768
886
  status: UsageStatus;
@@ -922,6 +1040,7 @@ export function getRequestLogEntries(): RequestLogEntry[] { return requestLog; }
922
1040
  /** Test-only process-state reset for isolated integration harnesses. */
923
1041
  export function clearRequestLogsForTests(): void {
924
1042
  requestLog.length = 0;
1043
+ requestLogBytes = 0;
925
1044
  requestLogSeq = 0;
926
1045
  requestLogsHydratedFromDisk = false;
927
1046
  }
@@ -97,9 +97,10 @@ import {
97
97
  } from "../relay";
98
98
  import { hasResponsesItemIdRepair, relaySseWithResponsesItemIdRepair } from "../responses-item-id-repair";
99
99
  import type { EffectiveSubagentRoster, SpawnAgentSurface } from "../../codex/catalog";
100
+ import type { TranslatorBudget } from "../../lib/translator-budget";
100
101
 
101
102
 
102
- export function buildToolBridgeMaps(parsed: OcxParsedRequest): {
103
+ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: TranslatorBudget): {
103
104
  toolNsMap: Map<string, { namespace: string; name: string }>;
104
105
  freeformToolNames: Set<string>;
105
106
  toolSearchToolNames: Set<string>;
@@ -108,9 +109,19 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest): {
108
109
  const freeformToolNames = new Set<string>();
109
110
  const toolSearchToolNames = new Set<string>();
110
111
  for (const t of parsed.context.tools ?? []) {
111
- if (t.namespace) toolNsMap.set(namespacedToolName(t.namespace, t.name), { namespace: t.namespace, name: t.name });
112
- if (t.freeform) freeformToolNames.add(t.name);
113
- if (t.toolSearch) toolSearchToolNames.add(t.name);
112
+ if (t.namespace) {
113
+ const wireName = namespacedToolName(t.namespace, t.name);
114
+ budget?.chargeRetained(new TextEncoder().encode(JSON.stringify([wireName, t.namespace, t.name])).byteLength, { kind: "retained_collectors" });
115
+ toolNsMap.set(wireName, { namespace: t.namespace, name: t.name });
116
+ }
117
+ if (t.freeform) {
118
+ budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" });
119
+ freeformToolNames.add(t.name);
120
+ }
121
+ if (t.toolSearch) {
122
+ budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" });
123
+ toolSearchToolNames.add(t.name);
124
+ }
114
125
  }
115
126
  return { toolNsMap, freeformToolNames, toolSearchToolNames };
116
127
  }
@@ -267,6 +267,9 @@ export async function handleResponsesCompact(
267
267
  modelId: selectedModelId,
268
268
  probeLeaseId: codexProbeLeaseId(authCtx),
269
269
  probeQuotaScope: codexProbeQuotaScope(authCtx),
270
+ writerGeneration: authCtx.kind === "pool" || authCtx.kind === "main-pool"
271
+ ? authCtx.writerGeneration
272
+ : undefined,
270
273
  });
271
274
  };
272
275
  let upstream: Response;