Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
06ec553
Merge pull request #3678 from lidge-jun/codex/promote-main-243-01a07240
lidge-jun Sep 5, 2026
116c2ac
Merge commit '44ea9576e27c6be8be7f13a86e32bb349368c54d' into codex/re…
invalid-email-address Sep 6, 2026
07b48da
Merge pull request #3785 from lidge-jun/codex/release-244-main-07c0
lidge-jun Sep 6, 2026
bcdf559
chore(release): promote validated 2.45.0 to main [skip ci]
invalid-email-address Sep 6, 2026
b0900e5
chore(release): promote 2.45.0 to main (#3813)
lidge-jun Sep 6, 2026
3970601
chore(release): prepare 2.46.0 stable promotion
invalid-email-address Sep 7, 2026
bba6322
Merge pull request #3851 from lidge-jun/codex/release-246-main
lidge-jun Sep 7, 2026
3d53e5f
release: prepare 2.47.0 from audited regression candidate
invalid-email-address Sep 7, 2026
eda8754
Merge commit '48ab3e1e66cfa6e0c873de2fafa4540ac61d6c7d' into codex/re…
invalid-email-address Sep 7, 2026
f9e3515
Merge commit '57252193b' into codex/release-247-main
invalid-email-address Sep 7, 2026
6f71931
release: promote 2.47.0 to main (#3929)
lidge-jun Sep 7, 2026
9a60256
Merge commit 'd0737cff3' into codex/release-247-main-final
invalid-email-address Sep 7, 2026
9e9b1d3
Merge commit 'f48c322c0' into codex/release-247-main-final
invalid-email-address Sep 7, 2026
947bae9
Merge commit '0d7652ad1' into codex/release-247-main-final
invalid-email-address Sep 7, 2026
f7f890f
release: apply final roster correction to main (#3933)
lidge-jun Sep 7, 2026
544ebee
release: promote 2.48.0 to main
invalid-email-address Sep 8, 2026
d24ff57
release: set main channel version 2.48.0
invalid-email-address Sep 8, 2026
9a27e86
Merge pull request #4011 from lidge-jun/codex/release-248-main
lidge-jun Sep 8, 2026
62849df
release: promote verified 2.49.0 product tree to main
lidge-jun Sep 9, 2026
2f3f736
Merge pull request #4117 from lidge-jun/codex/release-249-main-01a08498
lidge-jun Sep 9, 2026
3a3de88
release: promote verified 2.50.0 product tree to main
lidge-jun Sep 10, 2026
2d4d7a2
Merge pull request #4195 from lidge-jun/codex/release-250-main-01a08a81
lidge-jun Sep 10, 2026
cf456e8
release: promote verified 2.51.0 product tree to main
lidge-jun Sep 11, 2026
c155cc7
Merge pull request #4271 from lidge-jun/codex/release-251-main
lidge-jun Sep 11, 2026
95c4875
release: promote verified 2.52.0 product tree to main
lidge-jun Sep 12, 2026
4d37c35
Merge pull request #4407 from lidge-jun/codex/release-2520-main
lidge-jun Sep 12, 2026
641b05a
release: promote verified 2.53.0 product tree to main
lidge-jun Sep 13, 2026
aa05b3e
Merge pull request #4507 from lidge-jun/codex/release-2530-main
lidge-jun Sep 13, 2026
8e532c5
release: promote verified 2.54.0 product tree to main
lidge-jun Sep 13, 2026
9f7397e
Merge pull request #4540 from lidge-jun/codex/release-2540-main
lidge-jun Sep 13, 2026
673d542
fix(adapters): bound inline image decoding
luvs01 Sep 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions src/adapters/anthropic-image-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ export const MAX_INPUT_BASE64_LENGTH = 64 * MiB;
* would widen the adapter contract with no demonstrated need.
*/
export const IMAGE_NORMALIZE_CONCURRENCY = 4;
export const MAX_INPUT_PIXELS = 100_000_000;
// Keep one decoded RGBA surface below 64 MiB. Native codecs may allocate additional
// working buffers, so this is paired with process-wide admission in the normalizer.
export const MAX_INPUT_PIXELS = 16_000_000;


/** Formats Anthropic accepts as-is; anything else must be transcoded or dropped. */
Expand Down Expand Up @@ -218,6 +220,9 @@ export const bunImageEncode: EncodeFn = async (input, spec, quality) => {
const meta = await image.metadata();
const w = typeof meta.width === "number" ? meta.width : 0;
const h = typeof meta.height === "number" ? meta.height : 0;
if (w <= 0 || h <= 0 || w * h > MAX_INPUT_PIXELS) {
throw new Error("image dimensions exceed the safe decode limit");
}
let pipeline = new Bun.Image(input);
if (w > spec.maxEdge || h > spec.maxEdge) {
const scale = spec.maxEdge / Math.max(w, h);
Expand All @@ -233,7 +238,14 @@ export const bunImageEncode: EncodeFn = async (input, spec, quality) => {
* instead of riding pass-through to an Anthropic 400 (C-gate round 1, blocker 1).
*/
export const bunImageValidate: ValidateFn = async input => {
await new Bun.Image(input).resize(1, 1).jpeg({ quality: 1 }).toBuffer();
const image = new Bun.Image(input);
const meta = await image.metadata();
const w = typeof meta.width === "number" ? meta.width : 0;
const h = typeof meta.height === "number" ? meta.height : 0;
if (w <= 0 || h <= 0 || w * h > MAX_INPUT_PIXELS) {
throw new Error("image dimensions exceed the safe decode limit");
}
await image.resize(1, 1).jpeg({ quality: 1 }).toBuffer();
};

/**
Expand Down
50 changes: 48 additions & 2 deletions src/adapters/anthropic-image-normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,37 @@ import { bunImageEncode, bunImageValidate, processAt, TERMINAL_POS, TIER0_COUNT,
import { IMAGE_NORMALIZE_CONCURRENCY, MAX_INPUT_BASE64_LENGTH, MAX_INPUT_PIXELS } from "./anthropic-image-codec";
import type { NormalizeOptions } from "./anthropic-image-codec";

const IMAGE_DECODE_PROCESS_CONCURRENCY = IMAGE_NORMALIZE_CONCURRENCY;
let activeImageDecodes = 0;
const imageDecodeWaiters: Array<() => void> = [];

async function enterImageDecode(signal?: AbortSignal): Promise<() => void> {
if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
let transferred = false;
if (activeImageDecodes >= IMAGE_DECODE_PROCESS_CONCURRENCY) {
await new Promise<void>((resolve, reject) => {
const admit = (): void => {
transferred = true;
signal?.removeEventListener("abort", abort);
resolve();
};
const abort = (): void => {
const index = imageDecodeWaiters.indexOf(admit);
if (index >= 0) imageDecodeWaiters.splice(index, 1);
reject(signal?.reason ?? new DOMException("Aborted", "AbortError"));
};
imageDecodeWaiters.push(admit);
signal?.addEventListener("abort", abort, { once: true });
});
}
if (!transferred) activeImageDecodes++;
return () => {
const next = imageDecodeWaiters.shift();
if (next) next();
else activeImageDecodes--;
};
}

const UNDECODABLE_TEXT = "[image omitted: undecodable or corrupt image data]";
const BOMB_TEXT = "[image omitted: image too large to process safely]";
const OVERFLOW_DROP_TEXT = "[image omitted: total image payload exceeded the provider request budget; older images were dropped]";
Expand Down Expand Up @@ -79,6 +110,8 @@ export interface NormalizeTarget {
}

export interface NormalizeTargetsOptions extends NormalizeOptions {
/** Cancels queued native decode work and stops pulling more images. */
abortSignal?: AbortSignal;
Comment on lines 112 to +114

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass abort signals through every normalizer wrapper

The new process-wide queue is cancellable only when abortSignal reaches this option, but the Anthropic production paths do not expose or pass it: normalizeAnthropicImages still accepts only NormalizeOptions, createAnthropicAdapter().buildRequest omits incoming.abortSignal, and native Claude normalization omits req.signal. When all four slots are occupied, disconnected Anthropic requests therefore remain in imageDecodeWaiters, retain their image payloads, and later consume decode capacity, allowing cancelled traffic to build a CPU/memory backlog that delays live requests. Extend the wrapper options and thread the request signal through these callers.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

/** Total base64 budget across all targets. Default: TOTAL_IMAGE_BASE64_BUDGET. */
budget?: number;
/**
Expand All @@ -103,8 +136,21 @@ export async function normalizeImageTargets(targets: NormalizeTarget[], options:
const budget = options.budget ?? TOTAL_IMAGE_BASE64_BUDGET;
const overflowAction = options.overflowAction ?? "none";
const processLimit = options.processLimit ?? Number.POSITIVE_INFINITY;
const abortSignal = options.abortSignal;
const n = targets.length;

const process = async (b64: string, pos: number, mediaType: string) => {
if (abortSignal?.aborted) throw abortSignal.reason ?? new DOMException("Aborted", "AbortError");
const leave = await enterImageDecode(abortSignal);
try {
const result = await processAt(b64, pos, mediaType, encode, validate);
if (abortSignal?.aborted) throw abortSignal.reason ?? new DOMException("Aborted", "AbortError");
return result;
} finally {
leave();
}
};

// sourceB64/sourceMedia are the ORIGINAL input (encode source + cache identity);
// size always reflects the bytes currently ON the wire for this target (the core is
// the only mutator, so tracked size cannot drift from reality).
Expand Down Expand Up @@ -150,7 +196,7 @@ export async function normalizeImageTargets(targets: NormalizeTarget[], options:
}
const sourceMedia = target.mediaType.toLowerCase();
const pos = initialPosition(newestFirstIndex, bias);
const result = await processAt(b64, pos, sourceMedia, encode, validate);
const result = await process(b64, pos, sourceMedia);
if (result.kind === "failed") {
target.drop(UNDECODABLE_TEXT);
if (target.retainsBytesOnDrop) {
Expand Down Expand Up @@ -192,7 +238,7 @@ export async function normalizeImageTargets(targets: NormalizeTarget[], options:
while (sum > budget) {
const entry = entries.find((e): e is Entry => e !== null && !e.done);
if (!entry) break; // all terminal — overflowAction below decides
const result = await processAt(entry.sourceB64, entry.pos + 1, entry.sourceMedia, encode, validate);
const result = await process(entry.sourceB64, entry.pos + 1, entry.sourceMedia);
if (result.kind === "failed") {
entry.target.drop(UNDECODABLE_TEXT);
if (entry.target.retainsBytesOnDrop) {
Expand Down
4 changes: 3 additions & 1 deletion src/adapters/openai-chat-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import {
export const OPENAI_CHAT_IMAGE_BASE64_BUDGET = 3_670_016; // 3.5MiB

export interface NormalizeOpenAIChatImagesOptions
extends Pick<NormalizeOptions, "encode" | "tierBias" | "validate"> {}
extends Pick<NormalizeOptions, "encode" | "tierBias" | "validate"> {
abortSignal?: AbortSignal;
}

/** Whether `value` is a plain object, so message and part shapes can be walked safely. */
function isRecord(value: unknown): value is Record<string, unknown> {
Expand Down
8 changes: 7 additions & 1 deletion src/adapters/openai-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1679,7 +1679,13 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
};
};
if (hasShrinkableOpenAIChatImages(messages)) {
return normalizeOpenAIChatImages(messages, { tierBias: incoming?.imageTierBias }).then(finish, finish);
return normalizeOpenAIChatImages(messages, {
tierBias: incoming?.imageTierBias,
abortSignal: incoming?.abortSignal,
}).then(finish, error => {
if (incoming?.abortSignal?.aborted) throw error;
return finish();
});
}
return finish();
},
Expand Down
7 changes: 6 additions & 1 deletion src/server/responses/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7524,7 +7524,11 @@ async function handleResponsesInner(
const remainingTransientSendBudget = (budget: number): number =>
Math.max(1, budget - transientSendsUsed);
try {
initialRequest = await activeAdapter.buildRequest(parsed, { headers: selectedForwardHeaders, translatorBudget });
initialRequest = await activeAdapter.buildRequest(parsed, {
headers: selectedForwardHeaders,
translatorBudget,
abortSignal: upstream.signal,
});
Comment on lines +7527 to +7531

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the abort signal in continuation rebuilds

This propagates cancellation for the initial build and the recovery rebuild, but fetchContinuation later calls activeAdapter.buildRequest without abortSignal around core.ts:8206. For an oversized OpenAI-chat image request that reaches terminal-guard continuation, disconnecting while normalization is queued will not remove that work from the new process-wide gate, so the request continues retaining and decoding images after cancellation. Pass upstream.signal to that continuation build as well.

AGENTS.md reference: src/AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

refreshRequestToolAliases(initialRequest);
recordAdapterReasoning(logCtx, initialRequest);
recordAdapterTier(logCtx, initialRequest);
Expand Down Expand Up @@ -7659,6 +7663,7 @@ async function handleResponsesInner(
retryRequest = await activeAdapter.buildRequest(parsed, {
headers: selectedForwardHeaders,
translatorBudget,
abortSignal: upstream.signal,
...(imageTierBias > 0 ? { imageTierBias } : {}),
});
recordAdapterReasoning(logCtx, retryRequest);
Expand Down
2 changes: 1 addition & 1 deletion structure/adapters/registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ Combo child requests normalize effort and thinking controls against the selected

Live sideband admission and its bounded upstream handshake follow the [runtime contract](../runtime.md#live-sideband-handshake); the ordinary Responses WebSocket exchange remains separate.

Translated Chat request construction uses the [inline-image budget](../transports/streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached.
Translated Chat request construction uses the [inline-image budget](../transports/streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached, rejects inputs above the safe decoded-pixel ceiling, caps native decode work process-wide, and stops queued work when the request is cancelled.

The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior.

Expand Down
2 changes: 1 addition & 1 deletion structure/data-planes/inbound-compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ Account quota surfaces use [safe probe diagnostics](../transports/inventory.md#a

Live sideband admission and its bounded upstream handshake follow the [runtime contract](../runtime.md#live-sideband-handshake); the ordinary Responses WebSocket exchange remains separate.

Translated Chat request construction uses the [inline-image budget](../transports/streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached.
Translated Chat request construction uses the [inline-image budget](../transports/streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached, rejects inputs above the safe decoded-pixel ceiling, caps native decode work process-wide, and stops queued work when the request is cancelled.

The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior.

Expand Down
2 changes: 1 addition & 1 deletion structure/providers/chat-compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,4 +291,4 @@ byte-limit boundaries.

Canonical Spark Lite metadata follows the final serialized model and surviving nonempty Lite tool catalog; see [Responses transport](../transports/responses.md).

Translated Chat request construction uses the [inline-image budget](../transports/streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached.
Translated Chat request construction uses the [inline-image budget](../transports/streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached, rejects inputs above the safe decoded-pixel ceiling, caps native decode work process-wide, and stops queued work when the request is cancelled.
2 changes: 1 addition & 1 deletion structure/providers/cursor.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,4 @@ Combo child requests normalize effort and thinking controls against the selected

`src/adapters/cursor.ts` surfaces the first bare context overflow before attempting conversation remint on later eligible requests. `cursorClientThreadOwner` recognizes both client thread aliases; `src/adapters/cursor/thread-continuity.ts` limits recovery to three remints per retained identity-scoped owner, with a one-hour idle TTL and 2,048-entry bound. Conversation-only requests have no stable owner and do not automatically remint. Quota/rate errors, tool-result resumes, partial output, local side effects, isolated helper/shadow requests and compaction remain fail-closed. Isolated requests neither consume the parent allowance nor invalidate its checkpoint. Eligible overflow checks refresh existing retention timestamps and LRU position even after the cap is exhausted, without allocating absent scopes. Retention expiry, eviction or process restart resets the in-memory allowance; this is not a persistent lifetime cap or semantic-progress policy.

Translated Chat request construction uses the [inline-image budget](../transports/streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached.
Translated Chat request construction uses the [inline-image budget](../transports/streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached, rejects inputs above the safe decoded-pixel ceiling, caps native decode work process-wide, and stops queued work when the request is cancelled.
2 changes: 1 addition & 1 deletion structure/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ The account history response can include a [low-confidence effective capacity es

Account quota surfaces use [safe probe diagnostics](transports/inventory.md#account-quota-failure-diagnostics) separately from quota validity, credential health and routing authority.

Translated Chat request construction uses the [inline-image budget](transports/streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached.
Translated Chat request construction uses the [inline-image budget](transports/streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached, rejects inputs above the safe decoded-pixel ceiling, caps native decode work process-wide, and stops queued work when the request is cancelled.

OpenCode catalog discovery in `src/cli/opencode.ts` uses the local admin credential and a validated numeric-loopback management origin. It dials through `src/server/direct-local-http.ts`, rejects redirects and preserves the request/body deadline. Hub ingress selection stays separate from exported inference settings.

Expand Down
2 changes: 2 additions & 0 deletions structure/transports/byte-accounting.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Byte Accounting

Translated Chat inline-image normalization keeps retained wire bytes in its aggregate budget while process-wide native-decode admission and a decoded-pixel ceiling bound memory outside that byte accounting.

How opencodex measures request and stream bytes without allocating copies solely to count
them. These contracts are shared by request parsing, SSE rewriting, the provider adapters and
the translator budget, which is why so many documents link here rather than restating them.
Expand Down
2 changes: 1 addition & 1 deletion structure/transports/inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ Antigravity account quota probes expose only a closed `quotaFailure` category wh

Live sideband admission and its bounded upstream handshake follow the [runtime contract](../runtime.md#live-sideband-handshake); the ordinary Responses WebSocket exchange remains separate.

Translated Chat request construction uses the [inline-image budget](streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached.
Translated Chat request construction uses the [inline-image budget](streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached, rejects inputs above the safe decoded-pixel ceiling, caps native decode work process-wide, and stops queued work when the request is cancelled.

The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior.

Expand Down
2 changes: 1 addition & 1 deletion structure/transports/responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ Pool quota producers and account commands follow the [bounded raw-observation co

Live sideband admission and its bounded upstream handshake follow the [runtime contract](../runtime.md#live-sideband-handshake); the ordinary Responses WebSocket exchange remains separate.

Translated Chat request construction uses the [inline-image budget](streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached.
Translated Chat request construction uses the [inline-image budget](streaming-health.md#translated-chat-inline-image-budget); the shared normalizer counts retained bytes even when a wire-specific drop callback keeps the image attached, rejects inputs above the safe decoded-pixel ceiling, caps native decode work process-wide, and stops queued work when the request is cancelled.

The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior.

Expand Down
2 changes: 1 addition & 1 deletion structure/transports/streaming-health.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ Live sideband admission and its bounded upstream handshake follow the [runtime c

## Translated Chat inline-image budget

`src/adapters/openai-chat-images.ts` reuses the shared image normalization ladder for translated Chat bodies above a 3.5 MiB base64-image budget. This is best effort, not a whole-request ceiling. Remote URLs are not fetched; unprocessable and terminal images remain attached, and retained bytes continue to count during demotion. Under-budget construction stays synchronous; delegating MiMo awaits conditional asynchronous construction. Native Chat passthrough and Anthropic-only 413 retry policy retain their existing behavior.
`src/adapters/openai-chat-images.ts` reuses the shared image normalization ladder for translated Chat bodies above a 3.5 MiB base64-image budget. This is best effort, not a whole-request ceiling. Remote URLs are not fetched; unprocessable and terminal images remain attached, and retained bytes continue to count during demotion. Under-budget construction stays synchronous; delegating MiMo awaits conditional asynchronous construction. Native decoding is admitted through a process-wide four-operation gate, guarded by a 16-million-pixel ceiling confirmed from codec metadata, and request cancellation stops queued and subsequent normalization work. Native Chat passthrough and Anthropic-only 413 retry policy retain their existing behavior.

The [explicit model-capability contract](../config.md#explicit-per-model-capability-declarations) preserves operator declarations through provider storage and catalog capture; it does not infer upstream capability or change this surface's routing behavior.

Expand Down
21 changes: 21 additions & 0 deletions tests/adapters/anthropic/anthropic-image-normalize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,27 @@ describe("bounded parallel first pass (WP170)", () => {
expect(g.stats().arrivals).toBe(10);
});

test("the decoder concurrency limit is shared across simultaneous requests", async () => {
const g = gatedEncoder();
const first = normalizeAnthropicImages(
[userMsg(distinctImages(8).map(b64 => imageBlock(b64)))],
{ encode: g.encode },
);
const second = normalizeAnthropicImages(
[userMsg(distinctImages(8).map(b64 => imageBlock(b64)))],
{ encode: g.encode },
);

await g.waitForArrivals(IMAGE_NORMALIZE_CONCURRENCY);
expect(g.stats().active).toBe(IMAGE_NORMALIZE_CONCURRENCY);
await Bun.sleep(10);
expect(g.stats().arrivals).toBe(IMAGE_NORMALIZE_CONCURRENCY);

g.release();
await Promise.all([first, second]);
expect(g.stats().peak).toBe(IMAGE_NORMALIZE_CONCURRENCY);
});

test("a thrown target callback rejects the call, settles in-flight work, and stops new pulls", async () => {
// processAt swallows encode/validate throws into {kind:"failed"} (its own catch),
// so the production escape hatch is a throwing target callback (drop/replace).
Expand Down
Loading
Loading