Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b8f9a45
fix(responses): refuse ambiguous OpenCode Go resets
luvs01 Sep 21, 2026
808dd85
test(responses): prove OpenCode Go pre-answer reset refusal by execution
luvs01 Sep 21, 2026
35fb727
fix(retries): refuse transient 5xx after an operator-authorized reset…
luvs01 Sep 21, 2026
940b318
fix(retries): word the replay refusal for the post-response path too
luvs01 Sep 21, 2026
db854bf
fix(routing): isolate policy retry body snapshot
luvs01 Sep 20, 2026
b037810
test(routing): pin the retry snapshot against nested input mutation
luvs01 Sep 21, 2026
e6f9339
fix(responses): isolate policy compaction state
luvs01 Sep 20, 2026
f86a534
fix(tests): bound the cold-spawn warm-up child on a live event loop
devin-ai-integration[bot] Sep 20, 2026
76b40f9
fix(responses): fail closed on synthetic or stale compaction source s…
devin-ai-integration[bot] Sep 21, 2026
385f338
fix(codex): bind scoped quota suppression to alternate
luvs01 Sep 20, 2026
feb0c16
fix(ci): restore core.ts to file-size ratchet cap
devin-ai-integration[bot] Sep 20, 2026
466c75c
fix(codex): record wrapped quota on suppressed moves and bind caller …
devin-ai-integration[bot] Sep 21, 2026
9050722
test(server): move scoped-quota auth cases into a sibling file under …
devin-ai-integration[bot] Sep 21, 2026
b2eda92
fix(codex): re-check abort after the scoped-quota body read
devin-ai-integration[bot] Sep 21, 2026
1069b54
fix(codex): release the discarded compact rejection body on abort
devin-ai-integration[bot] Sep 21, 2026
37a006e
fix(codex): keep compact abort cleanup off the return path
devin-ai-integration[bot] Sep 21, 2026
be1fee9
test(responses): preserve terminal refusal across recovery boundaries
luvs01 Sep 22, 2026
67c4f57
Merge commit '41ec40f7e31969d1ca7140e0d510077f74f4cbe3' into stack/re…
luvs01 Sep 22, 2026
286df4a
Merge commit 'a077087b741a7f7cb1d07b801ec831965d180839' into work/bou…
luvs01 Sep 22, 2026
f732aa4
Keep malformed UTF-8 from erasing a cyber-policy stop
luvs01 Sep 20, 2026
6e6bd22
fix(bounded-body): report utf8Valid on the fatal decode EOF path
luvs01 Sep 21, 2026
7aaf959
fix(kiro): bound fallback HTTP error body reads
luvs01 Sep 20, 2026
cc466ed
test(recovery): preserve bounded classification and cancellation cont…
luvs01 Sep 22, 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
3 changes: 3 additions & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,7 @@
"kiro-retry.test.ts": "providers/kiro",
"kiro-review-regressions.test.ts": "providers/kiro",
"kiro-stream.test.ts": "providers/kiro",
"kiro-fallback-error-body.test.ts": "providers/kiro",
"kiro-usage-quota.test.ts": "providers/kiro",
"kiro-windows-cli-db-path.test.ts": "providers/kiro",
"kiro-windows-cli-executable-path.test.ts": "providers/kiro",
Expand Down Expand Up @@ -1280,6 +1281,7 @@
"responses-account-label.test.ts": "responses",
"responses-canonical-only-top-level-fields.test.ts": "responses",
"responses-compact-handoff-admission.test.ts": "responses",
"responses-compaction-policy-identity.test.ts": "responses",
"responses-compaction-override.test.ts": "responses",
"responses-compaction-routing.test.ts": "responses",
"responses-compaction.test.ts": "responses",
Expand Down Expand Up @@ -1363,6 +1365,7 @@
"self-launch-argv.test.ts": "lib",
"server-403-permission-e2e.test.ts": "server",
"server-agent-task-recovery-replay.test.ts": "server",
"server-auth-scoped-quota.test.ts": "server",
"server-auth.test.ts": "server",
"server-background-lifecycle.test.ts": "server",
"server-clickjacking-headers.test.ts": "server",
Expand Down
1 change: 1 addition & 0 deletions src/adapters/kiro/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
});
return {
response,
abortSignal: requestAbortSignal,
inputTokens: retry.inputTokens,
contextInputEstimate: retry.contextInputEstimate,
nameMap: retry.nameMap,
Expand Down
4 changes: 3 additions & 1 deletion src/adapters/kiro/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { noteKiroTransientThrottle } from "../kiro-retry";
import { KiroThinkingParser } from "../kiro-thinking";
import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "../kiro-truncation";
import { isValidKiroConversationId } from "../kiro-wire";
import { readDisplaySafeErrorPayloadText } from "../upstream-http-error";
import { tagKiroReasoningBlob } from "./reasoning";
import { estimateKiroTokens, kiroUpstreamContextWindow } from "./usage";

Expand Down Expand Up @@ -74,6 +75,7 @@ function createKiroAttemptRetention(budget: TranslatorBudget): KiroAttemptRetent

interface KiroFallbackAttempt {
response: Response;
abortSignal?: AbortSignal;
inputTokens: number;
contextInputEstimate: number;
nameMap: Map<string, string>;
Expand Down Expand Up @@ -1092,7 +1094,7 @@ export async function* parseKiroStream(
firstResult.releaseRetained();
fallback.releaseRequestBody?.();
if (!fallback.response.ok) {
const payload = await fallback.response.text().catch(() => "");
const payload = await readDisplaySafeErrorPayloadText(fallback.response, fallback.abortSignal);
const failure = classifyKiroHttpError(fallback.response.status, fallback.response.headers, payload);
yield {
type: "error",
Expand Down
14 changes: 14 additions & 0 deletions src/codex/auth-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,20 @@ function selectedCodexToken(headers: Headers): { accessToken: string; chatgptAcc
};
}

/**
* The workspace account id a request-owned `main` credential materializes under, or
* `undefined` when the caller's headers carry none. This is the `chatgpt-account-id`
* `materializeCodexUpstreamAuth` would set for a caller-owned `{ kind: "main" }` context,
* read here without touching a credential store so a rotation gate can compare workspace
* scope before a send is ever built.
*/
export function callerCodexWorkspaceAccountId(headers: Headers): string | undefined {
const explicit = headers.get("chatgpt-account-id");
if (explicit) return explicit;
const bearer = headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
return bearer ? extractAccountId(undefined, bearer) : undefined;
}

function assertMaterializedReserve(headers: Headers, ctx: CodexAuthContext, options: CodexAuthMaterializationOptions): void {
if (!requiresReserveAuthorization(options.config, options.modelId, options.admission)) return;
assertReserveAdmission(options.config!);
Expand Down
12 changes: 8 additions & 4 deletions src/codex/quota-rejection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,9 +349,10 @@ export async function codexScopedExhaustionCode(
* Status alone and message text are intentionally insufficient. The broad
* alternate-account retry remains eligible for 429/402 to preserve #584.
*
* The one carve-out from that breadth is an organization- or project-scoped exhaustion
* ({@link SCOPED_EXHAUSTION_CODE_VALUES}), which reports `alternateRetryEligible: false`
* because every credential inside the refusing limit would be refused by the same counter.
* Organization- or project-scoped exhaustion ({@link SCOPED_EXHAUSTION_CODE_VALUES}) remains
* alternate-retry eligible here because the response does not identify the refusing scope. The
* account-rotation path may suppress the send later when the resolved alternate carries binding
* evidence that it shares an organization-level counter.
*/
export async function classifyCodexPreStreamRejection(
response: Response,
Expand All @@ -377,7 +378,10 @@ export async function classifyCodexPreStreamRejection(
});
}
if (scoped) {
return rejection(status, "scoped-quota-exhaustion", { scopedExhaustionCode: scoped });
return rejection(status, "scoped-quota-exhaustion", {
alternateRetryEligible: true,
scopedExhaustionCode: scoped,
});
}
return rejection(
status,
Expand Down
30 changes: 30 additions & 0 deletions src/lib/bounded-body.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export interface BoundedBodyOptions {
* Reader cancellation and lock release still run. Defaults to false.
*/
fatalUtf8?: boolean;
/** Report UTF-8 validity without rejecting malformed bodies. */
reportUtf8Validity?: boolean;
/**
* Byte ceiling for retained body data. Defaults to BOUNDED_BODY_MAX_BYTES (64 KiB),
* which suits error bodies; callers materializing whole success payloads (e.g. a
Expand Down Expand Up @@ -44,6 +46,8 @@ export interface BoundedBodyResult {
oversized: boolean;
/** False means callers should use a status-only fallback, not `text`. */
displaySafe: boolean;
/** Present when reportUtf8Validity was requested and the retained body reached EOF. */
utf8Valid?: boolean;
}

export interface BoundedBytesOptions {
Expand Down Expand Up @@ -238,6 +242,14 @@ function decodeUtf8(chunks: readonly Uint8Array[], fatal: boolean, timedOut = fa
}
}

function decodeUtf8WithValidity(bytes: Uint8Array): { text: string; utf8Valid: boolean } {
try {
return { text: decodeUtf8([bytes], true), utf8Valid: true };
} catch {
return { text: decodeUtf8([bytes], false), utf8Valid: false };
}
}

/**
* Consume the original response body under strict memory and time bounds.
*
Expand Down Expand Up @@ -326,6 +338,24 @@ export async function readBoundedResponseBody(

const { value, done } = outcome as ReadableStreamReadResult<Uint8Array>;
if (done) {
if (options.reportUtf8Validity) {
const bytes = retained.subarray(0, retainedBytes);
// A fatal decode that returned already proved the bytes valid; still
// honour the reporting contract instead of dropping utf8Valid.
const decoded = options.fatalUtf8 === true
? { text: decodeUtf8([bytes], true), utf8Valid: true }
: decodeUtf8WithValidity(bytes);
return {
text: decoded.text,
truncated: false,
timedOut: false,
totalTimedOut: false,
inactivityTimedOut: false,
oversized: false,
displaySafe: true,
utf8Valid: decoded.utf8Valid,
};
}
return {
text: decodeUtf8([retained.subarray(0, retainedBytes)], options.fatalUtf8 === true),
truncated: false,
Expand Down
31 changes: 30 additions & 1 deletion src/server/responses/compact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import { describeImagesInPlace, planVisionSidecar, shouldResolveOpenAiVisionSide
import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
import {
applyCodexAuthContextToProvider,
callerCodexWorkspaceAccountId,
createCodexReserveDispatchGuard,
unwrapUpstreamRetryEvidenceError,
CodexMainProfileDrainingError,
Expand Down Expand Up @@ -186,6 +187,7 @@ import {
handleResponses,
preAuthUpstreamHostCircuitKey,
poolCredentialRefreshIncompleteResponse,
shouldRetryCodexScopedQuotaOnAlternate,
upstreamHostCircuitOpenResponse,
usesCodexForwardPoolAuth,
} from "./core";
Expand Down Expand Up @@ -1211,9 +1213,36 @@ export async function handleResponsesCompact(
if (alternate && req.signal.aborted) {
releaseCodexAuthContextProbeLease(alternate.authCtx);
recordCompactPoolOutcome(outcomeCtx, 499);
void upstream.body?.cancel(req.signal.reason).catch(() => undefined);
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
}
if (alternate) {
// The same scope binding the regular path applies: an organization-scoped
// exhaustion refuses every credential in that workspace, so a proven
// same-workspace alternate pays a cold prompt prefix for no new capacity.
// Suppression is not silence — the buffered recorder below still attributes
// the 429/402 to the account that produced it.
const sharedWorkspaceScope = alternate != null
&& !await shouldRetryCodexScopedQuotaOnAlternate(
upstream,
authCtx.chatgptAccountId,
alternate.authCtx.kind === "pool" || alternate.authCtx.kind === "main-pool"
? alternate.authCtx.chatgptAccountId
: callerCodexWorkspaceAccountId(req.headers),
req.signal,
);
// The scope check reads the rejection body asynchronously — the same window the
// comment above covers. Re-check before the branch below records A, cancels its
// body, and sends B for a caller that is gone.
if (alternate && req.signal.aborted) {
releaseCodexAuthContextProbeLease(alternate.authCtx);
recordCompactPoolOutcome(outcomeCtx, 499);
void upstream.body?.cancel(req.signal.reason).catch(() => undefined);
return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
}
if (alternate && sharedWorkspaceScope) {
releaseCodexAuthContextProbeLease(alternate.authCtx);
}
if (alternate && !sharedWorkspaceScope) {
// Same order the regular path uses (core.ts:349-357): a 429/402 carries the
// quota snapshot that produced it, so refresh A's cache before recording its
// rejection. Skipping this leaves quota-strategy routing and the dashboard
Expand Down
31 changes: 28 additions & 3 deletions src/server/responses/compaction-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { isDeclaredReasoningEffort } from "../../reasoning-effort";
import { COMPACTION_TRIGGERS } from "../../config/schema/compaction-triggers";
import { routeConcreteModel, type RouteResult } from "../../router";
import { resolveComboId } from "../../combos/identifiers";
import { resolvePolicyProfileId } from "../../routing/profile";
import { parseSyntheticRowId } from "../fast-row";
import { recallComboForLane } from "./combo-session-recall";
import { sessionLaneIdFromRequest } from "../request-log-conversation";

Expand Down Expand Up @@ -83,7 +85,7 @@ export function applyCompactionRoutingOverride(
if (trigger === undefined) return null;

const sourceModel = raw.model;
const sourceCombo = recallComboForLane(config, sessionLaneIdFromRequest(headers), sourceModel);
const sourceCombo = recallComboForLane(config, sessionLaneIdFromRequest(headers), sourceSelectorOf(config, sourceModel));
const targetCombo = resolveComboId(config, override.model.trim()) ?? undefined;
raw.model = override.model.trim();
if (override.reasoningEffort !== undefined) {
Expand All @@ -92,19 +94,42 @@ export function applyCompactionRoutingOverride(
return { sourceModel, ...(sourceCombo ? { sourceCombo } : {}), ...(targetCombo ? { targetCombo } : {}) };
}

/**
* The selector a synthetic-row grammar actually routed on. `--fast`/`--effort` suffixes are
* decoration applied at ingress; identity checks must see the base id or a decorated
* virtual selector (`alias--fast`) slips past them.
*/
function sourceSelectorOf(config: OcxConfig, sourceModel: string): string {
const { fastRow, effortRow } = parseSyntheticRowId(sourceModel, config);
return fastRow?.baseId ?? effortRow?.baseId ?? sourceModel;
}

/** Same provider identity keeps caller auth and may use native compact; its ciphertext replays only there. */
export function compactionRoutingKeepsProviderIdentity(
config: OcxConfig,
override: CompactionRoutingOverride,
route: RouteResult,
): boolean {
if (route.combo || override.sourceCombo || override.targetCombo || resolveComboId(config, override.sourceModel)) return false;
// `sourceModel` is the selector as the client sent it, so a synthetic `--fast` or
// effort suffix can still be attached. The base id is what the conversation routed on,
// and only the base can match the combo/policy guards below.
const sourceSelector = sourceSelectorOf(config, override.sourceModel);
if (route.combo || override.sourceCombo || override.targetCombo || resolveComboId(config, sourceSelector)) return false;
// A policy selector does not identify one stable serving backend: its route depends on
// request evidence and live candidate state that this post-rewrite check no longer has.
// Treat it as crossing identity rather than reconstructing it through concrete routing,
// which deliberately bypasses policy evaluation and may fall through to defaultProvider.
if (resolvePolicyProfileId(config, sourceSelector) !== null) return false;
let source: RouteResult;
try {
source = routeConcreteModel(config, override.sourceModel);
source = routeConcreteModel(config, sourceSelector);
} catch {
return false;
}
// The default-provider branch is where every unrecognized selector lands — including a
// policy/combo alias that was renamed or deleted since the conversation began. Such a
// selector cannot prove which backend served it, so it can never match an identity.
if (source.routeReason === "default-provider") return false;
return source.providerName === route.providerName
&& source.codexAccountMode === route.codexAccountMode
&& source.codexAccountNamespace === route.codexAccountNamespace;
Expand Down
Loading
Loading