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 83e8f4f466..f957926476 100644 --- a/structure/adapters/registry.md +++ b/structure/adapters/registry.md @@ -183,3 +183,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 2fd03722df..c70e0a71ef 100644 --- a/structure/catalog.md +++ b/structure/catalog.md @@ -390,3 +390,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 01a583c182..5f914d20d9 100644 --- a/structure/clients/claude-desktop.md +++ b/structure/clients/claude-desktop.md @@ -157,3 +157,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 a7420072bf..5a63a43898 100644 --- a/structure/data-planes/images.md +++ b/structure/data-planes/images.md @@ -121,3 +121,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 33ca8c76b0..0d9f3eb249 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -331,3 +331,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 4628ca5e50..3a390bc3f0 100644 --- a/structure/gui-and-management-api.md +++ b/structure/gui-and-management-api.md @@ -631,4 +631,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 0c0ffe38d0..33b09c168f 100644 --- a/structure/ops/docs-and-release.md +++ b/structure/ops/docs-and-release.md @@ -384,3 +384,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 e28575b432..3cebca7839 100644 --- a/structure/ops/service-and-sidecars.md +++ b/structure/ops/service-and-sidecars.md @@ -182,3 +182,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 dc982c5639..a0b0d92224 100644 --- a/structure/providers/xai-grok.md +++ b/structure/providers/xai-grok.md @@ -141,3 +141,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 bd9ebbd561..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). diff --git a/structure/subagents.md b/structure/subagents.md index c0c92891f7..f07c61413e 100644 --- a/structure/subagents.md +++ b/structure/subagents.md @@ -374,3 +374,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 7f01dee197..96dada1413 100644 --- a/structure/transports/byte-accounting.md +++ b/structure/transports/byte-accounting.md @@ -39,3 +39,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 ca80373a8e..53b2c8be92 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -148,3 +148,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 4a6a664cad..9ffaae0da1 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 682fbb2ca2..e4ed5468d3 100644 --- a/structure/transports/streaming-health.md +++ b/structure/transports/streaming-health.md @@ -252,3 +252,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 5b691d28d6..7440ef621f 100644 --- a/tests/oauth/state-store-sweeper.test.ts +++ b/tests/oauth/state-store-sweeper.test.ts @@ -189,6 +189,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(() => {});