@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.
- package/README.md +24 -0
- package/bin/ocx.mjs +32 -4
- package/gui/dist/assets/index-CHwf3tTD.css +1 -0
- package/gui/dist/assets/index-u5eFOv2y.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-normalize.ts +114 -20
- package/src/adapters/anthropic.ts +126 -10
- package/src/adapters/azure.ts +3 -3
- package/src/adapters/base.ts +7 -3
- package/src/adapters/cursor/discovery.ts +14 -4
- package/src/adapters/cursor/effort-map.ts +18 -6
- package/src/adapters/cursor/framing.ts +102 -27
- package/src/adapters/cursor/kv-store.ts +30 -3
- package/src/adapters/cursor/live-models.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +245 -49
- package/src/adapters/cursor/mcp-manager.ts +105 -8
- package/src/adapters/cursor/native-exec-mcp.ts +5 -3
- package/src/adapters/cursor/native-exec-shell.ts +296 -14
- package/src/adapters/cursor/native-exec.ts +381 -33
- package/src/adapters/cursor/protobuf-events.ts +28 -1
- package/src/adapters/cursor/protobuf-request.ts +71 -39
- package/src/adapters/cursor/request-builder.ts +2 -2
- package/src/adapters/cursor/transport.ts +2 -0
- package/src/adapters/cursor.ts +13 -2
- package/src/adapters/google-antigravity-replay.ts +184 -17
- package/src/adapters/google.ts +58 -8
- package/src/adapters/kiro-thinking.ts +23 -9
- package/src/adapters/kiro-tools.ts +49 -18
- package/src/adapters/kiro.ts +377 -133
- package/src/adapters/mimo-free.ts +36 -4
- package/src/adapters/openai-chat.ts +143 -17
- package/src/adapters/openai-responses.ts +130 -14
- package/src/adapters/run-turn-queue.ts +7 -1
- package/src/bridge.ts +466 -69
- package/src/chat/outbound.ts +144 -38
- package/src/claude/inbound-debug.ts +53 -8
- package/src/claude/outbound.ts +224 -38
- package/src/cli/agent-driven.ts +34 -1
- package/src/cli/catalog-prewarm.ts +5 -2
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/doctor.ts +12 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +13 -3
- package/src/cli/init.ts +129 -102
- package/src/cli/opencode.ts +36 -151
- package/src/cli/star-prompt.ts +13 -4
- package/src/cli/status-oauth.ts +12 -2
- package/src/clients/config-export.ts +377 -0
- package/src/codex/account-runtime-state.ts +19 -1
- package/src/codex/account-store.ts +162 -82
- package/src/codex/auth-api.ts +467 -159
- package/src/codex/auth-context.ts +15 -2
- package/src/codex/catalog/aggregation.ts +15 -0
- package/src/codex/catalog/effort.ts +16 -6
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +3 -1
- package/src/codex/catalog/provider-fetch.ts +29 -0
- package/src/codex/catalog/sync.ts +64 -7
- package/src/codex/catalog.ts +2 -2
- package/src/codex/inject.ts +5 -5
- package/src/codex/main-account-cache.ts +8 -1
- package/src/codex/model-cache.ts +81 -2
- package/src/codex/pool-rotation.ts +39 -0
- package/src/codex/project-config-warnings.ts +12 -1
- package/src/codex/quota.ts +35 -3
- package/src/codex/routing.ts +46 -1
- package/src/codex/shim.ts +10 -4
- package/src/codex/subagent-model-fallback.ts +12 -0
- package/src/codex/websocket-registry.ts +27 -0
- package/src/combos/failover.ts +31 -1
- package/src/combos/request.ts +9 -0
- package/src/combos/resolve.ts +60 -4
- package/src/combos/types.ts +12 -0
- package/src/config.ts +510 -55
- package/src/github/star-state.ts +13 -1
- package/src/images/fulfill.ts +39 -1
- package/src/images/loop.ts +52 -12
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bun-stream-caps.ts +31 -7
- package/src/lib/config-ownership.ts +33 -0
- package/src/lib/crash-guard.ts +65 -5
- package/src/lib/debug-log-buffer.ts +47 -6
- package/src/lib/destination-policy.ts +12 -1
- package/src/lib/errors.ts +3 -0
- package/src/lib/gcp-adc.ts +40 -2
- package/src/lib/injection-debug-log.ts +26 -2
- package/src/lib/provider-outbound.ts +3 -0
- package/src/lib/sidecar-tracker.ts +5 -2
- package/src/lib/sse-decoder.ts +257 -37
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/windows-secret-acl.ts +33 -12
- package/src/lib/winsw.ts +14 -1
- package/src/oauth/anthropic-routing.ts +31 -7
- package/src/oauth/google-antigravity.ts +2 -1
- package/src/oauth/health.ts +30 -12
- package/src/oauth/index.ts +127 -23
- package/src/oauth/kiro-credentials.ts +72 -1
- package/src/oauth/kiro.ts +23 -4
- package/src/oauth/store.ts +165 -18
- package/src/oauth/token-guardian.ts +43 -4
- package/src/oauth/types.ts +2 -1
- package/src/providers/base-url-choices.ts +10 -0
- package/src/providers/derive.ts +12 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/key-failover.ts +12 -0
- package/src/providers/openai-sidecar.ts +4 -1
- package/src/providers/quota.ts +68 -7
- package/src/providers/registry.ts +279 -3
- package/src/responses/parser.ts +5 -1
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +520 -102
- package/src/router.ts +18 -1
- package/src/server/adapter-resolve.ts +20 -3
- package/src/server/auth-cors.ts +121 -28
- package/src/server/chat-completions.ts +57 -12
- package/src/server/claude-messages.ts +85 -13
- package/src/server/index.ts +242 -100
- package/src/server/lifecycle.ts +155 -25
- package/src/server/management/agent-settings-routes.ts +79 -36
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/combo-routes.ts +5 -1
- package/src/server/management/config-routes.ts +42 -12
- package/src/server/management/logs-usage-routes.ts +41 -21
- package/src/server/management/model-routes.ts +188 -54
- package/src/server/management/oauth-account-routes.ts +115 -26
- package/src/server/management/provider-routes.ts +56 -6
- package/src/server/management/shared.ts +16 -3
- package/src/server/management/sidebar-routes.ts +50 -1
- package/src/server/management/system-restart.ts +13 -6
- package/src/server/management/system-routes.ts +15 -3
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +39 -5
- package/src/server/management-auth.ts +65 -14
- package/src/server/port-reclaim.ts +58 -12
- package/src/server/ports.ts +2 -0
- package/src/server/proxy-liveness.ts +60 -14
- package/src/server/relay-eager.ts +20 -4
- package/src/server/relay.ts +548 -154
- package/src/server/request-decompress.ts +51 -4
- package/src/server/request-log.ts +134 -15
- package/src/server/responses/collaboration.ts +15 -4
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +241 -66
- package/src/server/responses-image-gen-repair.ts +19 -5
- package/src/server/responses-item-id-repair.ts +23 -5
- package/src/server/sse-payload-rewrite.ts +71 -12
- package/src/server/startup-health-cache.ts +14 -1
- package/src/server/system-env.ts +8 -1
- package/src/server/windows-tcp-drop.ts +15 -5
- package/src/server/ws-bridge.ts +25 -0
- package/src/service.ts +179 -13
- package/src/storage/policy-job.ts +93 -23
- package/src/storage/policy-worker.ts +6 -0
- package/src/storage/restore-job.ts +62 -16
- package/src/storage/restore-worker.ts +6 -0
- package/src/storage/storage-mutation-coordinator.ts +36 -6
- package/src/storage/worker-lifecycle.ts +181 -47
- package/src/tray/windows.ts +97 -25
- package/src/types.ts +39 -6
- package/src/update/index.ts +24 -5
- package/src/update/job.ts +598 -73
- package/src/usage/log.ts +115 -17
- package/src/usage/summary.ts +67 -2
- package/src/vision/index.ts +112 -22
- package/src/web-search/loop.ts +38 -6
- package/src/web-search/progress-stream.ts +14 -3
- package/gui/dist/assets/index-BDjpkcRN.js +0 -67
- package/gui/dist/assets/index-BHsKRFh9.css +0 -1
package/src/github/star-state.ts
CHANGED
|
@@ -79,7 +79,8 @@ async function spawnGh(args: string[], timeoutMs: number): Promise<{ status: num
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
const
|
|
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
|
package/src/images/fulfill.ts
CHANGED
|
@@ -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
|
-
|
|
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: `.href})`,
|
|
110
144
|
};
|
|
145
|
+
} finally {
|
|
146
|
+
releaseFulfillmentBytes(callBytes);
|
|
147
|
+
pendingFulfillments -= 1;
|
|
148
|
+
}
|
|
111
149
|
}
|
package/src/images/loop.ts
CHANGED
|
@@ -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
|
-
{
|
|
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
|
-
|
|
421
|
-
|
|
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")
|
|
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
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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
|
+
}
|