@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
@@ -79,7 +79,8 @@ async function spawnGh(args: string[], timeoutMs: number): Promise<{ status: num
79
79
  }
80
80
  }
81
81
 
82
- const defaultDeps: StarDeps = { runGh: spawnGh, nowMs: () => Date.now() };
82
+ const productionDeps: StarDeps = { runGh: spawnGh, nowMs: () => Date.now() };
83
+ let defaultDeps = productionDeps;
83
84
 
84
85
  let cached: { timestamp: number; state: StarState } | null = null;
85
86
  /** Coalesces concurrent probes so parallel sidebar polls share one `gh` run. */
@@ -148,6 +149,17 @@ export function invalidateStarStatusCache(): void {
148
149
  inflight = null;
149
150
  }
150
151
 
152
+ /**
153
+ * Route tests must not launch the user's `gh` executable: its installation,
154
+ * authentication helper, and Windows shim are all outside the route contract.
155
+ * Production keeps the real runner; tests install an explicit deterministic
156
+ * dependency and must reset it afterwards.
157
+ */
158
+ export function setStarDepsForTests(deps: StarDeps | null): void {
159
+ defaultDeps = deps ?? productionDeps;
160
+ invalidateStarStatusCache();
161
+ }
162
+
151
163
  /**
152
164
  * Star the repository through the user's `gh` login. Returns the resulting
153
165
  * state so the caller does not need a second round trip; an unauthenticated
@@ -8,11 +8,32 @@ import {
8
8
  pruneArtifacts,
9
9
  type ImageBudget,
10
10
  } from "./artifacts";
11
+ import { MAX_PENDING_IMAGE_FULFILLMENTS } from "../lib/translator-budget";
12
+
13
+ const fulfillmentEncoder = new TextEncoder();
14
+ let pendingFulfillments = 0;
15
+ let fulfillmentCurrentBytes = 0;
16
+ let fulfillmentHighWaterBytes = 0;
17
+
18
+ function addFulfillmentBytes(bytes: number): void {
19
+ fulfillmentCurrentBytes += bytes;
20
+ fulfillmentHighWaterBytes = Math.max(fulfillmentHighWaterBytes, fulfillmentCurrentBytes);
21
+ }
22
+
23
+ function releaseFulfillmentBytes(bytes: number): void {
24
+ fulfillmentCurrentBytes = Math.max(0, fulfillmentCurrentBytes - bytes);
25
+ }
26
+
27
+ export function imageFulfillmentTailSnapshot(): { currentBytes: number; highWaterBytes: number; active: number } {
28
+ return { currentBytes: fulfillmentCurrentBytes, highWaterBytes: fulfillmentHighWaterBytes, active: pendingFulfillments };
29
+ }
11
30
 
12
31
  /** Serialize write→prune→retain across concurrent fulfillments sharing artifacts/. */
13
32
  let retentionTail: Promise<void> = Promise.resolve();
14
33
 
15
34
  async function retainAfterBatch(paths: string[], keepCount?: number): Promise<string[]> {
35
+ const retainedBytes = paths.reduce((total, path) => total + fulfillmentEncoder.encode(path).byteLength, 0);
36
+ addFulfillmentBytes(retainedBytes);
16
37
  const run = retentionTail.then(() => {
17
38
  pruneArtifacts(keepCount);
18
39
  return paths.filter((p) => existsSync(p));
@@ -21,7 +42,11 @@ async function retainAfterBatch(paths: string[], keepCount?: number): Promise<st
21
42
  () => undefined,
22
43
  () => undefined,
23
44
  );
24
- return run;
45
+ try {
46
+ return await run;
47
+ } finally {
48
+ releaseFulfillmentBytes(retainedBytes);
49
+ }
25
50
  }
26
51
 
27
52
  /**
@@ -35,6 +60,15 @@ export async function fulfillImageCall(
35
60
  budget: ImageBudget,
36
61
  signal?: AbortSignal,
37
62
  ): Promise<ImageCallResult> {
63
+ if (pendingFulfillments >= MAX_PENDING_IMAGE_FULFILLMENTS) {
64
+ return { ok: false, model: plan.model, prompt: "", files: [], count: 0, error: "image_fulfillment_busy" };
65
+ }
66
+ pendingFulfillments += 1;
67
+ const callBytes = fulfillmentEncoder.encode(call.id).byteLength
68
+ + fulfillmentEncoder.encode(call.name).byteLength
69
+ + fulfillmentEncoder.encode(call.arguments).byteLength;
70
+ addFulfillmentBytes(callBytes);
71
+ try {
38
72
  let args: unknown;
39
73
  try {
40
74
  args = JSON.parse(call.arguments || "{}");
@@ -108,4 +142,8 @@ export async function fulfillImageCall(
108
142
  // backslashes are not treated as escapes by renderers.
109
143
  markdown: `![image](${pathToFileURL(primary).href})`,
110
144
  };
145
+ } finally {
146
+ releaseFulfillmentBytes(callBytes);
147
+ pendingFulfillments -= 1;
148
+ }
111
149
  }
@@ -10,7 +10,7 @@
10
10
  * Removed vs web-search: no sidecar backend selection, no forced-answer nudge, no failed-query
11
11
  * dedup, no describeImages/structuredOutput, no recordSidecarOutcome.
12
12
  */
13
- import type { AdapterRequest, ProviderAdapter } from "../adapters/base";
13
+ import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "../adapters/base";
14
14
  import { existsSync } from "node:fs";
15
15
  import { pathToFileURL } from "node:url";
16
16
  import { createAdapterEventQueue } from "../adapters/run-turn-queue";
@@ -20,6 +20,11 @@ import { bridgeToResponsesSSE } from "../bridge";
20
20
  import { clearableDeadline, idleDeadline } from "../lib/abort";
21
21
  import { readBoundedResponseBody } from "../lib/bounded-body";
22
22
  import { fetchWithResetRetry } from "../lib/upstream-retry";
23
+ import {
24
+ isTranslatorBudgetExceededError,
25
+ TRANSLATOR_MAX_TURN_BYTES,
26
+ TranslatorBudgetExceededError,
27
+ } from "../lib/translator-budget";
23
28
  import { parseStreamWithProgress, RoutedModelInactivityError, WebSearchStreamProtocolError } from "../web-search/progress-stream";
24
29
  import { fulfillImageCall } from "./fulfill";
25
30
  import { parseVideoCallArgs, pollVideoWithHeartbeats, buildVideoResult, createVideoBudget } from "./fulfill-video";
@@ -200,6 +205,7 @@ class LoopError extends Error {
200
205
  export interface ImageBridgeDeps {
201
206
  parsed: OcxParsedRequest;
202
207
  adapter: ProviderAdapter;
208
+ incomingMeta: IncomingMeta;
203
209
  plan?: ImageBridgePlan;
204
210
  videoPlan?: VideoBridgePlan;
205
211
  /** Per-video generation timeout (ms) including polling. */
@@ -240,6 +246,7 @@ export interface ImageBridgeDeps {
240
246
  * inject the answer as a tool_result, and loop (bounded by `maxRounds`).
241
247
  */
242
248
  export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Response> {
249
+ const translatorBudget = deps.incomingMeta.translatorBudget;
243
250
  const { parsed, plan, videoPlan, videoTimeoutMs, abortSignal } = deps;
244
251
  let adapter = deps.adapter;
245
252
  const maxRounds = clampImageMaxRounds(deps.maxRounds ?? DEFAULT_MAX_ROUNDS);
@@ -347,7 +354,11 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
347
354
  void adapter
348
355
  .runTurn(
349
356
  iterParsed,
350
- { headers: deps.forwardHeaders ? new Headers(deps.forwardHeaders) : new Headers(), abortSignal: signal },
357
+ {
358
+ headers: deps.forwardHeaders ? new Headers(deps.forwardHeaders) : new Headers(),
359
+ abortSignal: signal,
360
+ translatorBudget,
361
+ },
351
362
  queue.push,
352
363
  )
353
364
  .then(() => queue.close())
@@ -396,6 +407,9 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
396
407
  // runTurn adapters signal errors via {type:"error"} events, not HTTP status codes.
397
408
  const errorEvent = events.find(e => e.type === "error");
398
409
  if (errorEvent && errorEvent.type === "error") {
410
+ if (errorEvent.code === "translation_buffer_limit") {
411
+ throw new TranslatorBudgetExceededError("retained_collectors", TRANSLATOR_MAX_TURN_BYTES);
412
+ }
399
413
  throw new LoopError(502, errorEvent.message);
400
414
  }
401
415
 
@@ -414,11 +428,14 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
414
428
  const request = await requestAdapter.buildRequest(iterParsed, {
415
429
  headers: deps.forwardHeaders ? new Headers(deps.forwardHeaders) : new Headers(),
416
430
  abortSignal: headerDeadline.signal,
431
+ translatorBudget,
417
432
  });
418
433
  try { deps.onRequestBuilt?.(request); } catch { /* diagnostics are best-effort */ }
419
434
  deps.onAttemptSend?.();
420
- const response = requestAdapter.fetchResponse
421
- ? await requestAdapter.fetchResponse(request, {
435
+ let response: Response;
436
+ try {
437
+ response = requestAdapter.fetchResponse
438
+ ? await requestAdapter.fetchResponse(request, {
422
439
  abortSignal: headerDeadline.signal,
423
440
  timeoutMs: connectTimeoutMs,
424
441
  returnRawErrors: true,
@@ -436,7 +453,10 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
436
453
  });
437
454
  },
438
455
  { abortSignal: headerDeadline.signal, label: "image-bridge-loop" },
439
- );
456
+ );
457
+ } finally {
458
+ request.releaseBodyObservation?.();
459
+ }
440
460
  return { response, responseAdapter: requestAdapter };
441
461
  };
442
462
 
@@ -476,6 +496,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
476
496
  }
477
497
  return prepared;
478
498
  } catch (error) {
499
+ if (isTranslatorBudgetExceededError(error)) throw error;
479
500
  if (headerDeadline.didExpire()) {
480
501
  throw new LoopError(504, `Provider response-header timeout after ${connectTimeoutMs}ms during image-bridge`);
481
502
  }
@@ -503,11 +524,13 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
503
524
  for await (const event of parseStreamWithProgress(prepared.response, parse, {
504
525
  signal,
505
526
  inactivityTimeoutMs: stallTimeoutMs,
527
+ translatorBudget,
506
528
  })) {
507
529
  if (event.type === "heartbeat") yield event;
508
530
  else events.push(event);
509
531
  }
510
532
  } catch (error) {
533
+ if (isTranslatorBudgetExceededError(error)) throw error;
511
534
  if (signal.aborted) throw new LoopError(499, "client closed request during image-bridge");
512
535
  if (error instanceof RoutedModelInactivityError) throw new LoopError(504, error.message);
513
536
  if (error instanceof WebSearchStreamProtocolError) throw new LoopError(502, error.message);
@@ -520,7 +543,12 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
520
543
  throw new LoopError(502, `Image-bridge adapter stream protocol error: expected one final terminal event, received ${terminalIndexes.length}`);
521
544
  }
522
545
  const terminal = events[terminalIndexes[0]!];
523
- if (terminal.type === "error") throw new LoopError(502, terminal.message);
546
+ if (terminal.type === "error") {
547
+ if (terminal.code === "translation_buffer_limit") {
548
+ throw new TranslatorBudgetExceededError("retained_collectors", TRANSLATOR_MAX_TURN_BYTES);
549
+ }
550
+ throw new LoopError(502, terminal.message);
551
+ }
524
552
  return scanEventsForImageCall(events, mediaToolNames);
525
553
  };
526
554
 
@@ -754,12 +782,23 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
754
782
  });
755
783
  }
756
784
  } catch (e) {
757
- yield {
758
- type: "error",
759
- message: e instanceof LoopError ? e.message : (e instanceof Error ? e.message : String(e)),
760
- ...(e instanceof LoopError ? { status: e.status } : {}),
761
- ...(hiddenUsage ? { usage: hiddenUsage } : {}),
762
- };
785
+ if (isTranslatorBudgetExceededError(e)) {
786
+ yield {
787
+ type: "error",
788
+ status: 502,
789
+ errorType: "upstream_error",
790
+ code: e.code,
791
+ message: "upstream translation buffer exceeded the safe limit",
792
+ ...(hiddenUsage ? { usage: hiddenUsage } : {}),
793
+ };
794
+ } else {
795
+ yield {
796
+ type: "error",
797
+ message: e instanceof LoopError ? e.message : (e instanceof Error ? e.message : String(e)),
798
+ ...(e instanceof LoopError ? { status: e.status } : {}),
799
+ ...(hiddenUsage ? { usage: hiddenUsage } : {}),
800
+ };
801
+ }
763
802
  return;
764
803
  }
765
804
  }
@@ -773,6 +812,7 @@ export async function runWithImageBridge(deps: ImageBridgeDeps): Promise<Respons
773
812
  internalAbort.abort("client closed responses stream");
774
813
  }, 2_000,
775
814
  {
815
+ translatorBudget,
776
816
  ...(deps.forceEmptyResponseId ? { responseId: "" } : {}),
777
817
  hideThinkingSummary: parsed.options.hideThinkingSummary,
778
818
  stallTimeoutSec: deps.stallTimeoutSec,
@@ -0,0 +1,83 @@
1
+ export const RETAINED_TRUNCATION_MARKER = "\n…[truncated by opencodex]";
2
+
3
+ export class ResourceAdmissionError extends Error {
4
+ readonly code: string = "server_busy";
5
+
6
+ constructor(readonly resource: string, readonly limit: number) {
7
+ super(`${resource} capacity reached (${limit})`);
8
+ this.name = "ResourceAdmissionError";
9
+ }
10
+ }
11
+
12
+ export interface AdmissionMetrics {
13
+ active: number;
14
+ peak: number;
15
+ admitted: number;
16
+ rejected: number;
17
+ releaseMisses: number;
18
+ }
19
+
20
+ export interface AdmissionLease {
21
+ release(): void;
22
+ }
23
+
24
+ export interface AdmissionReservation<T> extends AdmissionLease {
25
+ bind(value: T): void;
26
+ }
27
+
28
+ export function createAdmissionGate(name: string, limit: number): {
29
+ tryAcquire(): AdmissionLease | null;
30
+ metrics(): Readonly<AdmissionMetrics>;
31
+ } {
32
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError(`${name} admission limit must be positive`);
33
+ const state: AdmissionMetrics = { active: 0, peak: 0, admitted: 0, rejected: 0, releaseMisses: 0 };
34
+ return {
35
+ tryAcquire() {
36
+ if (state.active >= limit) {
37
+ state.rejected += 1;
38
+ return null;
39
+ }
40
+ state.active += 1;
41
+ state.admitted += 1;
42
+ state.peak = Math.max(state.peak, state.active);
43
+ let active = true;
44
+ return {
45
+ release() {
46
+ if (!active) {
47
+ return;
48
+ }
49
+ active = false;
50
+ state.active -= 1;
51
+ },
52
+ };
53
+ },
54
+ metrics() {
55
+ return { ...state };
56
+ },
57
+ };
58
+ }
59
+
60
+ export function retainedUtf8Bytes(value: string): number {
61
+ return new TextEncoder().encode(value).byteLength;
62
+ }
63
+
64
+ function utf8Prefix(value: string, maxBytes: number): string {
65
+ if (maxBytes <= 0) return "";
66
+ let bytes = 0;
67
+ let result = "";
68
+ for (const character of value) {
69
+ const size = retainedUtf8Bytes(character);
70
+ if (bytes + size > maxBytes) break;
71
+ result += character;
72
+ bytes += size;
73
+ }
74
+ return result;
75
+ }
76
+
77
+ export function truncateRetainedUtf8(value: string, maxBytes: number): string {
78
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new RangeError("maxBytes must be a non-negative integer");
79
+ if (retainedUtf8Bytes(value) <= maxBytes) return value;
80
+ const markerBytes = retainedUtf8Bytes(RETAINED_TRUNCATION_MARKER);
81
+ if (markerBytes > maxBytes) return utf8Prefix(RETAINED_TRUNCATION_MARKER, maxBytes);
82
+ return utf8Prefix(value, maxBytes - markerBytes) + RETAINED_TRUNCATION_MARKER;
83
+ }
@@ -0,0 +1,173 @@
1
+ import {
2
+ enforceAppOwnedMemoryBudget,
3
+ registerRetainedStore,
4
+ registerObservedBuffer,
5
+ type RetainedStoreRegistration,
6
+ type RetainedStoreSnapshot,
7
+ } from "./app-owned-memory";
8
+ import { registerStateSweepAfterTick } from "./state-store-sweeper";
9
+ import { debugBufferMetrics, evictOldestDebugEntryForBudget } from "./debug-log-buffer";
10
+ import { injectionBufferMetrics, evictOldestInjectionEntryForBudget } from "./injection-debug-log";
11
+ import { crashRingMetrics, evictOldestCrashTraceForBudget } from "./crash-guard";
12
+ import { claudeInboundDebugMetrics, evictOldestClaudeInboundForBudget } from "../claude/inbound-debug";
13
+ import {
14
+ evictOldestRequestLogForBudget,
15
+ requestLogRetainedStoreSnapshot,
16
+ } from "../server/request-log";
17
+ import {
18
+ anthropicImageNormalizeRetainedStoreSnapshot,
19
+ evictOldestAnthropicImageNormalizeForBudget,
20
+ } from "../adapters/anthropic-image-normalize";
21
+ import {
22
+ evictOldestVisionDescriptionForBudget,
23
+ visionDescriptionRetainedStoreSnapshot,
24
+ } from "../vision";
25
+ import {
26
+ antigravityReplayRetainedStoreSnapshot,
27
+ evictOldestAntigravityReplayForBudget,
28
+ } from "../adapters/google-antigravity-replay";
29
+ import {
30
+ evictOldestModelCacheForBudget,
31
+ modelCacheRetainedStoreSnapshot,
32
+ } from "../codex/model-cache";
33
+ import {
34
+ evictOldestUsageSummaryForBudget,
35
+ usageSummaryRetainedStoreSnapshot,
36
+ } from "../server/management/usage-summary-cache";
37
+ import {
38
+ cursorBlobRetainedStoreSnapshot,
39
+ evictOldestCursorBlobForBudget,
40
+ } from "../adapters/cursor/native-exec";
41
+ import {
42
+ evictOldestResponseContinuationForBudget,
43
+ responseContinuationRetainedStoreSnapshot,
44
+ } from "../responses/state";
45
+ import { translatorObservedBufferSnapshot } from "./translator-budget";
46
+ import { imageFulfillmentTailSnapshot } from "../images/fulfill";
47
+ import { oauthMutationTailSnapshot } from "../oauth/store";
48
+ import { grokApplyFlightSnapshot } from "../server/management/agent-settings-routes";
49
+
50
+ function ringSnapshot(metrics: { entries: number; bytes: number; oldestAt: number | null }): RetainedStoreSnapshot {
51
+ return {
52
+ count: metrics.entries,
53
+ bytes: metrics.bytes,
54
+ evictableBytes: metrics.bytes,
55
+ pinnedBytes: 0,
56
+ oldestAt: metrics.oldestAt,
57
+ };
58
+ }
59
+
60
+ function providerDebugSnapshot(): RetainedStoreSnapshot {
61
+ return ringSnapshot(debugBufferMetrics());
62
+ }
63
+
64
+ function injectionDebugSnapshot(): RetainedStoreSnapshot {
65
+ return ringSnapshot(injectionBufferMetrics());
66
+ }
67
+
68
+ function claudeDebugSnapshot(): RetainedStoreSnapshot {
69
+ return ringSnapshot(claudeInboundDebugMetrics());
70
+ }
71
+
72
+ function crashRingSnapshot(): RetainedStoreSnapshot {
73
+ return ringSnapshot(crashRingMetrics());
74
+ }
75
+
76
+ export const APP_OWNED_RETAINED_STORE_REGISTRATIONS = [
77
+ {
78
+ id: "request_log",
79
+ category: "logs",
80
+ snapshot: requestLogRetainedStoreSnapshot,
81
+ evictOldest: evictOldestRequestLogForBudget,
82
+ },
83
+ {
84
+ id: "provider_debug",
85
+ category: "logs",
86
+ snapshot: providerDebugSnapshot,
87
+ evictOldest: evictOldestDebugEntryForBudget,
88
+ },
89
+ {
90
+ id: "injection_debug",
91
+ category: "logs",
92
+ snapshot: injectionDebugSnapshot,
93
+ evictOldest: evictOldestInjectionEntryForBudget,
94
+ },
95
+ {
96
+ id: "claude_debug",
97
+ category: "logs",
98
+ snapshot: claudeDebugSnapshot,
99
+ evictOldest: evictOldestClaudeInboundForBudget,
100
+ },
101
+ {
102
+ id: "crash_ring",
103
+ category: "logs",
104
+ snapshot: crashRingSnapshot,
105
+ evictOldest: evictOldestCrashTraceForBudget,
106
+ },
107
+ {
108
+ id: "image_normalize",
109
+ category: "caches",
110
+ snapshot: anthropicImageNormalizeRetainedStoreSnapshot,
111
+ evictOldest: evictOldestAnthropicImageNormalizeForBudget,
112
+ },
113
+ {
114
+ id: "vision_descriptions",
115
+ category: "caches",
116
+ snapshot: visionDescriptionRetainedStoreSnapshot,
117
+ evictOldest: evictOldestVisionDescriptionForBudget,
118
+ },
119
+ {
120
+ id: "antigravity_replay",
121
+ category: "caches",
122
+ snapshot: antigravityReplayRetainedStoreSnapshot,
123
+ evictOldest: evictOldestAntigravityReplayForBudget,
124
+ },
125
+ {
126
+ id: "model_cache",
127
+ category: "caches",
128
+ snapshot: modelCacheRetainedStoreSnapshot,
129
+ evictOldest: evictOldestModelCacheForBudget,
130
+ },
131
+ {
132
+ id: "usage_summary",
133
+ category: "caches",
134
+ snapshot: usageSummaryRetainedStoreSnapshot,
135
+ evictOldest: evictOldestUsageSummaryForBudget,
136
+ },
137
+ {
138
+ id: "cursor_blobs",
139
+ category: "blobs",
140
+ snapshot: cursorBlobRetainedStoreSnapshot,
141
+ evictOldest: evictOldestCursorBlobForBudget,
142
+ },
143
+ {
144
+ id: "responses_continuation",
145
+ category: "continuation",
146
+ snapshot: responseContinuationRetainedStoreSnapshot,
147
+ evictOldest: evictOldestResponseContinuationForBudget,
148
+ },
149
+ ] as const satisfies readonly RetainedStoreRegistration[];
150
+
151
+ export function registerDefaultAppOwnedMemoryStores(): void {
152
+ for (const registration of APP_OWNED_RETAINED_STORE_REGISTRATIONS) {
153
+ registerRetainedStore(registration);
154
+ }
155
+ }
156
+
157
+ export const APP_OWNED_OBSERVED_BUFFER_REGISTRATIONS = [
158
+ { id: "translator_buffers", category: "translator", snapshot: translatorObservedBufferSnapshot },
159
+ { id: "image_fulfillment_tail", category: "serialized_tails", snapshot: imageFulfillmentTailSnapshot },
160
+ { id: "oauth_mutation_tail", category: "serialized_tails", snapshot: oauthMutationTailSnapshot },
161
+ { id: "grok_apply_flight", category: "serialized_tails", snapshot: grokApplyFlightSnapshot },
162
+ ] as const;
163
+
164
+ export function registerDefaultAppOwnedObservedBuffers(): void {
165
+ for (const registration of APP_OWNED_OBSERVED_BUFFER_REGISTRATIONS) registerObservedBuffer(registration);
166
+ }
167
+
168
+ export function registerAppOwnedMemorySweepFallback(): void {
169
+ registerStateSweepAfterTick({
170
+ name: "app-owned-memory-budget",
171
+ afterTick: enforceAppOwnedMemoryBudget,
172
+ });
173
+ }