From 8f3686fa92ff95a37cd6b27776f604de75e210dd Mon Sep 17 00:00:00 2001 From: luvs01 Date: Mon, 14 Sep 2026 09:41:08 +0900 Subject: [PATCH 1/2] fix(responses): bound combo recall model retention --- docs-site/src/content/docs/guides/combos.md | 5 +- .../src/content/docs/ko/guides/combos.md | 2 +- src/lib/state-store-registrations.ts | 4 +- src/server/responses/combo-session-recall.ts | 54 ++++++++++++-- structure/adapters/registry.md | 2 + structure/catalog.md | 2 + structure/clients/claude-desktop.md | 2 + structure/clients/integrations.md | 2 + structure/data-planes/images.md | 2 + structure/data-planes/inbound-compat.md | 2 + structure/gui-and-management-api.md | 2 +- structure/ops/docs-and-release.md | 2 + structure/ops/service-and-sidecars.md | 2 + structure/overview.md | 2 + structure/providers/xai-grok.md | 2 + structure/runtime.md | 2 + structure/subagents.md | 2 + structure/transports/byte-accounting.md | 2 + structure/transports/inventory.md | 2 + structure/transports/responses.md | 12 ++-- structure/transports/streaming-health.md | 2 + tests/oauth/state-store-sweeper.test.ts | 70 +++++++++++++++++++ 22 files changed, 161 insertions(+), 18 deletions(-) diff --git a/docs-site/src/content/docs/guides/combos.md b/docs-site/src/content/docs/guides/combos.md index 6018d7e1bd..07c6123982 100644 --- a/docs-site/src/content/docs/guides/combos.md +++ b/docs-site/src/content/docs/guides/combos.md @@ -83,7 +83,10 @@ The request then follows normal combo selection and failover. Explicit provider/combo selectors and configured combo aliases take precedence over this recall. Failed, incomplete, or cancelled responses do not replace the last successful selection. Recall is -process-local and bounded to 256 lanes for 30 minutes; it does not store account credentials. +process-local and bounded to 256 lanes for 30 minutes; expired records are also swept periodically. +Model names are limited to 1 KiB each and 64 KiB total UTF-8 payload; the oldest records are evicted +when the count or byte budget is exceeded. A newer accepted completion with an oversized model +clears that lane's old record instead of retaining an outdated selection. No account credentials are stored. Without usable conversation identity or valid remembered state, normal compaction routing applies. A restart clears the remembered state. diff --git a/docs-site/src/content/docs/ko/guides/combos.md b/docs-site/src/content/docs/ko/guides/combos.md index b8d4087431..b5e508891f 100644 --- a/docs-site/src/content/docs/ko/guides/combos.md +++ b/docs-site/src/content/docs/ko/guides/combos.md @@ -66,7 +66,7 @@ alias는 클라이언트가 요청하는 공개 이름만 바꿉니다. 콤보 클라이언트가 콤보를 바꾼 뒤 공급자 접두사 없는 모델 이름으로 압축을 요청하면, opencodex는 같은 대화에서 가장 최근에 응답을 성공적으로 마친 콤보를 기억해 사용할 수 있습니다. 모델 이름이 완료된 응답과 일치하고, 현재 설정에 해당 콤보와 대상이 남아 있어야 합니다. 압축 요청도 일반 콤보 선택과 페일오버를 따릅니다. -명시한 공급자·콤보 선택자와 설정된 콤보 별칭이 기억한 값보다 우선합니다. 실패·미완료·취소된 응답은 마지막 성공 기록을 덮어쓰지 않습니다. 기록은 프로세스 안에서 최대 256개 대화, 30분 동안 유지하며 계정 자격증명을 저장하지 않습니다. 유효한 대화 식별자나 기록이 없으면 일반 압축 라우팅을 사용합니다. 재시작하면 기록은 사라집니다. +명시한 공급자·콤보 선택자와 설정된 콤보 별칭이 기억한 값보다 우선합니다. 실패·미완료·취소된 응답은 마지막 성공 기록을 덮어쓰지 않습니다. 기록은 프로세스 안에서 최대 256개 대화, 30분 동안 유지하며 만료된 기록을 주기적으로 정리합니다. 모델명은 UTF-8 기준 개별 1 KiB, 합계 64 KiB로 제한하고 개수나 용량 한도를 넘으면 가장 오래된 기록부터 제거합니다. 새로 승인된 완료 응답의 모델명이 너무 크면 해당 대화의 이전 기록도 지워 오래된 선택을 남기지 않습니다. 계정 자격증명은 저장하지 않습니다. 유효한 대화 식별자나 기록이 없으면 일반 압축 라우팅을 사용합니다. 재시작하면 기록은 사라집니다. ## 전략 선택 diff --git a/src/lib/state-store-registrations.ts b/src/lib/state-store-registrations.ts index 13a22bfce0..630f66a70f 100644 --- a/src/lib/state-store-registrations.ts +++ b/src/lib/state-store-registrations.ts @@ -21,7 +21,7 @@ import { } from "../combos/failover"; import { reconcileComboWarningMemos } from "../combos/request"; import { reconcileComboRotationState } from "../combos/resolve"; -import { reconcileComboRecall } from "../server/responses/combo-session-recall"; +import { reconcileComboRecall, sweepExpiredComboRecall } from "../server/responses/combo-session-recall"; import { listLiveComboTargetKeys } from "../combos/types"; import { listLiveConfigOwnershipRoots, @@ -112,7 +112,7 @@ export const STATE_STORE_REGISTRATIONS = [ { name: "model-cache-history", reconcileGeneration: reconcileModelCacheGeneration }, { name: "pool-rotation", reconcileGeneration: reconcilePoolRotationState }, { name: "combo-rotation", reconcileGeneration: reconcileComboRotationState }, - { name: "combo-session-recall", reconcileGeneration: reconcileComboRecall }, + { name: "combo-session-recall", sweepExpired: sweepExpiredComboRecall, reconcileGeneration: reconcileComboRecall }, { name: "guardian-backoff", reconcileGeneration: reconcileGuardianBackoff }, { name: "codex-reauth", reconcileGeneration: reconcileCodexReauthState }, { name: "oauth-reauth", reconcileGeneration: reconcileOAuthReauthState }, diff --git a/src/server/responses/combo-session-recall.ts b/src/server/responses/combo-session-recall.ts index 84dfd8d466..f4d01e5f0e 100644 --- a/src/server/responses/combo-session-recall.ts +++ b/src/server/responses/combo-session-recall.ts @@ -7,12 +7,16 @@ interface ComboRecallEntry { comboId: string; target: Pick; responseModel: string; + responseModelBytes: number; at: number; } const RECALL_CAPACITY = 256; const RECALL_TTL_MS = 30 * 60 * 1000; +const RECALL_MODEL_MAX_BYTES = 1024; +const RECALL_MODEL_TOTAL_BYTES = 64 * 1024; const recall = new Map(); +let retainedModelBytes = 0; let lastReconciledGeneration = 0; let liveOwners: Pick | undefined; @@ -22,6 +26,20 @@ function ownsEntry(context: Pick RECALL_MODEL_MAX_BYTES) return undefined; + const bytes = new TextEncoder().encode(model).byteLength; + return bytes <= RECALL_MODEL_MAX_BYTES ? bytes : undefined; +} + export function rememberComboForLane( lane: string | undefined, comboId: string, @@ -29,17 +47,28 @@ export function rememberComboForLane( responseModel: string, writerGeneration: number, ): void { - if (!lane || !comboId || !responseModel.trim()) return; + if (!lane || !comboId) return; // Reject even a same-named recreated owner: its previous in-flight turn is obsolete. if (writerGeneration < Math.max(lastReconciledGeneration, captureConfigGeneration())) return; - const entry = { comboId, target: { provider: target.provider, model: target.model }, responseModel, at: Date.now() }; + const entry = { comboId, target: { provider: target.provider, model: target.model }, responseModel, responseModelBytes: 0, at: Date.now() }; if (liveOwners && !ownsEntry(liveOwners, entry)) return; - recall.delete(lane); + // Whitespace-only completions were always a no-op; test before the size branch so an + // oversized blank cannot fall into the clear path, and without allocating a trimmed copy. + if (!/\S/u.test(responseModel)) return; + const modelBytes = boundedModelBytes(responseModel); + if (modelBytes === undefined) { + // This accepted completion supersedes the lane even when its model cannot be retained. + deleteEntry(lane); + return; + } + entry.responseModelBytes = modelBytes; + deleteEntry(lane); recall.set(lane, entry); - while (recall.size > RECALL_CAPACITY) { + retainedModelBytes += modelBytes; + while (recall.size > RECALL_CAPACITY || retainedModelBytes > RECALL_MODEL_TOTAL_BYTES) { const oldest = recall.keys().next().value; if (oldest === undefined) break; - recall.delete(oldest); + deleteEntry(oldest); } } @@ -57,7 +86,7 @@ export function recallComboForLane( || !Object.hasOwn(config.providers, entry.target.provider) || !provider || provider.disabled === true || !combo?.targets.some(target => targetKey(target) === targetKey(entry.target))) { - recall.delete(lane); + deleteEntry(lane); return undefined; } return entry.responseModel === model ? entry.comboId : undefined; @@ -74,16 +103,27 @@ export function reconcileComboRecall(context: GenerationContext): number { let removed = 0; for (const [lane, entry] of recall) { if (!ownsEntry(context, entry) || Date.now() - entry.at >= RECALL_TTL_MS) { - recall.delete(lane); + deleteEntry(lane); removed += 1; } } return removed; } +export function sweepExpiredComboRecall(now: number): number { + let removed = 0; + for (const [lane, entry] of recall) { + if (now - entry.at < RECALL_TTL_MS) continue; + deleteEntry(lane); + removed += 1; + } + return removed; +} + /** Test-only reset, alongside the combo rotation/cooldown resets. */ export function clearComboRecallForTests(): void { recall.clear(); + retainedModelBytes = 0; lastReconciledGeneration = 0; liveOwners = undefined; } diff --git a/structure/adapters/registry.md b/structure/adapters/registry.md index 908633f265..3e40c81974 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -180,3 +180,5 @@ implement legacy call/result pairing. Modern tool-image carriers are unchanged. raw passthrough; `tests/responses/chat-media-translation.test.ts` reaches the real HTTP translation boundary and verifies that rejection sends no upstream request. Canonical Responses identity sanitation and narrowly scoped pre-output combo recovery follow [request-local target compatibility](../runtime.md#request-local-target-compatibility); other adapter contracts remain unchanged. + +Combo recall follows the [Responses retention contract](../transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/catalog.md b/structure/catalog.md index 47a827426b..980e21f041 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -349,3 +349,5 @@ Exact [model input declarations](config.md#explicit-per-model-capability-declara ## Renamed destination reasoning metadata `src/providers/derive.ts` fills missing reasoning tables for renamed providers accepted by the existing fixed-key destination matcher. Model entries are cloned and explicit user entries (including empty arrays) win. Provider-wide effort defaults fill only when undefined; Command Code unknown models therefore keep the registry's empty picker policy unless overridden. Identity, transport and other capability axes are unchanged. The gathered row drives client exports; this metadata contract does not prove arbitrary gateway routing. + +Combo recall follows the [Responses retention contract](transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/clients/claude-desktop.md b/structure/clients/claude-desktop.md index f64a278757..e52d65df44 100644 --- a/structure/clients/claude-desktop.md +++ b/structure/clients/claude-desktop.md @@ -152,3 +152,5 @@ The [explicit model-capability contract](../config.md#explicit-per-model-capabil Exact [model input declarations](../config.md#explicit-per-model-capability-declarations) now feed text-only eligibility and catalog hints; existing image-description/omission handling consumes them before the main upstream send. Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. + +Combo recall follows the [Responses retention contract](../transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/clients/integrations.md b/structure/clients/integrations.md index eb4ace5975..7e61f93b52 100644 --- a/structure/clients/integrations.md +++ b/structure/clients/integrations.md @@ -195,3 +195,5 @@ existing explicit confirmation. The journal endpoint evaluates Undo against the Recovery reads commit history and ownership through strict store methods. Unreadable or malformed metadata is uncertainty, never evidence that a transaction did not commit. Pending records validate complete ownership, exact Cline paths and result fingerprints before either native file is replaced. + +Combo recall follows the [Responses retention contract](../transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/data-planes/images.md b/structure/data-planes/images.md index fdbdee6b32..3f0b58f4a2 100644 --- a/structure/data-planes/images.md +++ b/structure/data-planes/images.md @@ -118,3 +118,5 @@ Live sideband admission and its bounded upstream handshake follow the [runtime c 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. Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. + +Combo recall follows the [Responses retention contract](../transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index aa9aa15f52..1d7dc267a4 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -328,3 +328,5 @@ Modern `tool` images continue through the existing following-user carrier. These an OpenCodex conversion limit, not a provider capability claim. Final Responses-to-adapter admission follows the [registry contract](../adapters/registry.md#untranslated-input-media). Canonical Responses identity sanitation and narrowly scoped pre-output combo recovery follow [request-local target compatibility](../runtime.md#request-local-target-compatibility); other adapter contracts remain unchanged. + +Combo recall follows the [Responses retention contract](../transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/gui-and-management-api.md b/structure/gui-and-management-api.md index 0496735405..1e4a64d910 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -597,4 +597,4 @@ The [explicit model-capability contract](config.md#explicit-per-model-capability Exact [model input declarations](config.md#explicit-per-model-capability-declarations) now feed text-only eligibility and catalog hints; existing image-description/omission handling consumes them before the main upstream send. -The raw provider editor round-trips `autoReviewModel` and `autoReviewModelOverrides` through editor-owned DTO fields. POST/PATCH/PUT share validation; PUT copies schema-normalized values into the persisted and live candidate before adoption. Canonical `openai` rejects these fields, including clear forms. Field-masked writes (PATCH, editor PUT, reload) pin every registry-seed key and ignore operator overlays the seed never defines, most commonly `selectedModels`; POST keeps the exact-key comparison. Canonical `openai` still rejects `allowPrivateNetwork`, which must not short-circuit destination DNS checks on the ChatGPT forward row. Existing authentication, origin checks and stale-baseline protection still govern the writes. See [reviewer projection](catalog.md#provider-scoped-approval-reviewer). +The raw provider editor round-trips `autoReviewModel` and `autoReviewModelOverrides` through editor-owned DTO fields. POST/PATCH/PUT share validation; PUT copies schema-normalized values into the persisted and live candidate before adoption. Canonical `openai` rejects these fields, including clear forms. Field-masked writes (PATCH, editor PUT, reload) pin every registry-seed key and ignore operator overlays the seed never defines, most commonly `selectedModels`; POST keeps the exact-key comparison. Canonical `openai` still rejects `allowPrivateNetwork`, which must not short-circuit destination DNS checks on the ChatGPT forward row. Existing authentication, origin checks and stale-baseline protection still govern the writes. See [reviewer projection](catalog.md#provider-scoped-approval-reviewer). Combo recall follows the [Responses retention contract](transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/ops/docs-and-release.md b/structure/ops/docs-and-release.md index f4474c6419..87df4acad3 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -382,3 +382,5 @@ Exact [model input declarations](../config.md#explicit-per-model-capability-decl Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. Renamed fixed-key providers receive [missing reasoning metadata](../catalog.md#renamed-destination-reasoning-metadata) during derivation; explicit per-model entries and provider defaults retain precedence. + +Combo recall follows the [Responses retention contract](../transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/ops/service-and-sidecars.md b/structure/ops/service-and-sidecars.md index e9d8fb7c00..a1d5beea80 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -179,3 +179,5 @@ Live sideband admission and its bounded upstream handshake follow the [runtime c 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. Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. + +Combo recall follows the [Responses retention contract](../transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/overview.md b/structure/overview.md index 0151115adc..7192e00ad2 100644 --- a/structure/overview.md +++ b/structure/overview.md @@ -147,3 +147,5 @@ Translated Chat request construction uses the [inline-image budget](transports/s 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. Provider-scoped approval reviewer settings are projected by the [catalog owner](catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. + +Combo recall follows the [Responses retention contract](transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/providers/xai-grok.md b/structure/providers/xai-grok.md index 0ca554f4e0..04a11cdd8a 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -138,3 +138,5 @@ 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 audio/file admission follows the [final-adapter input contract](../adapters/registry.md#untranslated-input-media); native raw passthrough remains separate. + +Combo recall follows the [Responses retention contract](../transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/runtime.md b/structure/runtime.md index aa977c51df..6bffce48d7 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -416,3 +416,5 @@ Translated audio/file admission follows the [final-adapter input contract](adapt The combo may advance to its next eligible unattempted target before output commitment. It records no target/provider cooldown for these request-local mismatches and does not silently drop reasoning controls or raise `none` to a supported rung. Cancellation, origin/cyber-policy rejection, non-replayable post-send errors and the existing streaming commit boundary stay authoritative. Other invalid requests remain terminal. Regression coverage: `tests/responses/responses-forward-prompt-envelope.test.ts`, `tests/routing/router-combo-failover-classification.test.ts`, and `tests/server/server-combo-failover-e2e.test.ts`. + +Combo recall follows the [Responses retention contract](transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/subagents.md b/structure/subagents.md index 69047b076b..8d92994332 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -371,3 +371,5 @@ Exact [model input declarations](config.md#explicit-per-model-capability-declara Provider-scoped approval reviewer settings are projected by the [catalog owner](catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. Renamed fixed-key providers receive [missing reasoning metadata](catalog.md#renamed-destination-reasoning-metadata) during derivation; explicit per-model entries and provider defaults retain precedence. + +Combo recall follows the [Responses retention contract](transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/transports/byte-accounting.md b/structure/transports/byte-accounting.md index e758afeaf2..8d069a456a 100644 --- a/structure/transports/byte-accounting.md +++ b/structure/transports/byte-accounting.md @@ -36,3 +36,5 @@ These optimizations do not add request queues, retry policies, or RSS-based admi Translated audio/file admission follows the [final-adapter input contract](../adapters/registry.md#untranslated-input-media); native raw passthrough remains separate. Canonical Responses identity sanitation and narrowly scoped pre-output combo recovery follow [request-local target compatibility](../runtime.md#request-local-target-compatibility); other adapter contracts remain unchanged. + +Combo recall follows the [Responses retention contract](responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index b98bf57473..2a192b1f1a 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -145,3 +145,5 @@ Renamed fixed-key providers receive [missing reasoning metadata](../catalog.md#r Translated audio/file admission follows the [final-adapter input contract](../adapters/registry.md#untranslated-input-media); native raw passthrough remains separate. Canonical Responses identity sanitation and narrowly scoped pre-output combo recovery follow [request-local target compatibility](../runtime.md#request-local-target-compatibility); other adapter contracts remain unchanged. + +Combo recall follows the [Responses retention contract](responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/structure/transports/responses.md b/structure/transports/responses.md index 1bc2b4de0d..0c890fcc35 100644 --- a/structure/transports/responses.md +++ b/structure/transports/responses.md @@ -166,12 +166,12 @@ alone never opt a gateway in. and before the `/v1/*` guard. Unknown `/v1/*` paths return JSON 404 errors instead of falling through to GUI static serving. -Combo compaction recall uses accepted completed-response callbacks to record the final client-visible -model and originating combo target. The existing child callback gate defers publication until an -attempt is accepted and drops discarded/failed attempts. Both compaction entry points preserve -explicit configured selectors before consulting bounded lane state. The existing state-store -reconciliation owns removal of obsolete targets and generation fencing; core imports no registration -composition root or Lab code. Recall retains routing identity only, never account credentials. +Combo compaction recall records accepted completed-response callbacks; discarded/failed attempts never publish. +Explicit configured selectors precede lane recall. `src/server/responses/combo-session-recall.ts` bounds upstream model +strings to 1 KiB each and 64 KiB total UTF-8 payload, plus 256 lanes and a 30-minute TTL. Oldest entries yield to either cap. +An oversized accepted completion clears prior lane recall only after writer-generation and owner checks; stale writers cannot clear it. +The shared state-store registration sweeps dormant expiry; all removal paths release the model-byte budget. Config reconciliation +retains generation fencing. Recall stores routing identity, not credentials; core imports no registration composition root or Lab code. > Decision record: [ADR-0038](../decisions/ADR-0038-responses-http-sse.md) diff --git a/structure/transports/streaming-health.md b/structure/transports/streaming-health.md index 0b54c97058..82d8e93c60 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -249,3 +249,5 @@ Live sideband admission and its bounded upstream handshake follow the [runtime c 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. Provider-scoped approval reviewer settings are projected by the [catalog owner](../catalog.md#provider-scoped-approval-reviewer); this surface retains its existing routing, transport and account-selection behavior. + +Combo recall follows the [Responses retention contract](responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. diff --git a/tests/oauth/state-store-sweeper.test.ts b/tests/oauth/state-store-sweeper.test.ts index b162e2214b..9d4afd5f20 100644 --- a/tests/oauth/state-store-sweeper.test.ts +++ b/tests/oauth/state-store-sweeper.test.ts @@ -188,6 +188,76 @@ describe("state-store sweeper", () => { expect(reconcileLiveStateStores()).toEqual({ storesVisited: 1, rowsRemoved: 2 }); }); + describe("combo recall byte budget", () => { + const config: OcxConfig = { + port: 0, defaultProvider: "a", + providers: { a: { adapter: "openai-chat", baseUrl: "https://a.example/v1" } }, + combos: { first: { targets: [{ provider: "a", model: "m1" }] } }, + }; + const target = { provider: "a", model: "m1" }; + const remember = (lane: string, model: string) => + rememberComboForLane(lane, "first", target, model, captureConfigGeneration()); + + test("rejects oversized ASCII and UTF-8 models and accepts the byte boundary", () => { + for (const model of ["x".repeat(1025), "é".repeat(513)]) { + remember("lane", model); + expect(recallComboForLane(config, "lane", model)).toBeUndefined(); + } + remember("lane", "é".repeat(512)); + expect(recallComboForLane(config, "lane", "é".repeat(512))).toBe("first"); + }); + + test("new unretainable completion clears prior recall but stale or unowned writers cannot", () => { + registerStateStore(STATE_STORE_REGISTRATIONS.find(row => row.name === "combo-session-recall")!); + setLiveStateStoreConfig(config); + const oldGeneration = captureConfigGeneration(); + reconcileLiveStateStores(); + remember("lane", "visible"); + rememberComboForLane("lane", "first", target, "x".repeat(1025), oldGeneration); + expect(recallComboForLane(config, "lane", "visible")).toBe("first"); + rememberComboForLane("lane", "removed", target, "x".repeat(1025), captureConfigGeneration()); + expect(recallComboForLane(config, "lane", "visible")).toBe("first"); + remember("lane", "x".repeat(1025)); + expect(recallComboForLane(config, "lane", "visible")).toBeUndefined(); + }); + + test("evicts the oldest model at the aggregate limit without double-counting replacements", () => { + for (let i = 0; i < 64; i++) remember(`lane-${i}`, `${i}`.padEnd(1024, "x")); + remember("lane-63", "63".padEnd(1024, "x")); + expect(recallComboForLane(config, "lane-0", "0".padEnd(1024, "x"))).toBe("first"); + remember("lane-64", "64".padEnd(1024, "x")); + expect(recallComboForLane(config, "lane-0", "0".padEnd(1024, "x"))).toBeUndefined(); + expect(recallComboForLane(config, "lane-1", "1".padEnd(1024, "x"))).toBe("first"); + }); + + test("registered sweeper expires dormant models and releases their budget", () => { + registerStateStore(STATE_STORE_REGISTRATIONS.find(row => row.name === "combo-session-recall")!); + for (let i = 0; i < 64; i++) remember(`lane-${i}`, `${i}`.padEnd(1024, "x")); + expect(sweepExpired(Date.now() + 30 * 60 * 1000)).toEqual({ storesVisited: 1, rowsRemoved: 64 }); + for (let i = 0; i < 64; i++) remember(`new-${i}`, `${i}`.padEnd(1024, "x")); + expect(recallComboForLane(config, "new-0", "0".padEnd(1024, "x"))).toBe("first"); + }); + + test("retains the independent lane-count cap for short model names", () => { + for (let i = 0; i < 257; i++) remember(`lane-${i}`, "short"); + expect(recallComboForLane(config, "lane-0", "short")).toBeUndefined(); + expect(recallComboForLane(config, "lane-1", "short")).toBe("first"); + }); + + test("whitespace-only models stay no-ops that neither replace a lane nor consume budget", () => { + remember("lane", "visible"); + remember("lane", " ".repeat(1024)); + remember("lane", " ".repeat(1024)); + remember("lane", " ".repeat(1025)); + remember("lane", "\t\n "); + expect(recallComboForLane(config, "lane", " ".repeat(1024))).toBeUndefined(); + expect(recallComboForLane(config, "lane", "visible")).toBe("first"); + for (let i = 0; i < 63; i++) remember(`lane-${i}`, `${i}`.padEnd(1024, "x")); + expect(recallComboForLane(config, "lane-0", "0".padEnd(1024, "x"))).toBe("first"); + expect(recallComboForLane(config, "lane", "visible")).toBe("first"); + }); + }); + test("combo recall watermark rejects writers after a partially failed generation", () => { registerStateStore(STATE_STORE_REGISTRATIONS.find(row => row.name === "combo-session-recall")!); const warning = spyOn(console, "warn").mockImplementation(() => {}); From fa2f27335334034b9aedfe42a1d577a3031e2ddd Mon Sep 17 00:00:00 2001 From: luvs01 Date: Wed, 16 Sep 2026 11:29:29 +0900 Subject: [PATCH 2/2] docs(runtime): resolve combo recall merge conflict with dev Move the unchanged combo recall retention reference into the Lifecycle section instead of appending it at EOF, where dev adds the independent combo default effort precedence contract. Preserve both contracts without changing runtime code, tests, or rewriting existing history. Validation: verified the original PR and dev document blob hashes; reproduced the original conflict with git merge-file and confirmed the relocated paragraph merges cleanly with dev 3070d64d8822c6d8c62989665f82ab665e4d164c. This is document-level merge validation, not a new application test-suite run. --- structure/runtime.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/structure/runtime.md b/structure/runtime.md index 767063a486..11ba425f30 100644 --- a/structure/runtime.md +++ b/structure/runtime.md @@ -79,6 +79,8 @@ their own files. ## Lifecycle +Combo recall follows the [Responses retention contract](transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup. + Startup catalog sync and native restore apply the [retired-native policy](catalog.md#shared-catalog). Codex quota processing has shared and Reserve scopes; retired model evidence is suppressed as described in [OpenAI quota ownership](providers/openai-tiers.md#public-provider-contract). @@ -430,5 +432,3 @@ Translated audio/file admission follows the [final-adapter input contract](adapt The combo may advance to its next eligible unattempted target before output commitment. It records no target/provider cooldown for these request-local mismatches and does not silently drop reasoning controls or raise `none` to a supported rung. Cancellation, origin/cyber-policy rejection, non-replayable post-send errors and the existing streaming commit boundary stay authoritative. Other invalid requests remain terminal. Regression coverage: `tests/responses/responses-forward-prompt-envelope.test.ts`, `tests/routing/router-combo-failover-classification.test.ts`, and `tests/server/server-combo-failover-e2e.test.ts`. - -Combo recall follows the [Responses retention contract](transports/responses.md#responses-httpsse), including model-byte limits, accepted-writer invalidation and shared TTL cleanup.