diff --git a/config/quality/complexity-baseline.json b/config/quality/complexity-baseline.json index 561236c948..070f1b8ed5 100644 --- a/config/quality/complexity-baseline.json +++ b/config/quality/complexity-baseline.json @@ -2,6 +2,7 @@ "_comment": "Catraca de complexidade (check-complexity.mjs, ESLint core rules complexity>=15 e max-lines-per-function>80 sobre src+open-sse+electron+bin via eslint.complexity.config.mjs). Conta total de violacoes; so pode cair. --update ratcheta.", "count": 1896, "_rebaseline_2026_06_20_postlote_concurrent_drift": "Reconciliacao release-volatil: 1895->1896 (+1). Drift de condicional NOVO de PRs mergeados pela sessao concorrente APOS o #4338 ratchetar para 1895 (#4355 pricing gpt-5.x-pro / #4364 cli active-context cred / #4363 compliance cleanup / #4358 mitm mask / #4332 injection-guard-16KB). O fast-path do release nao roda check:complexity (so release->main), entao o ramo acumula sem rebaselinar. Medido com `node scripts/check/check-complexity.mjs` no tip cdfd71c17. Mesma familia — crescimento de feature legitimo, nao regressao.", + "_note_2026_06_20_4371_chatcore_heap_leaf": "PR #4371 (extract checkHeapPressureGuard leaf, god-file decomposition start) e complexity-NEUTRO: handleChatCore so PERDE codigo e o novo heapPressure.ts checkHeapPressureGuard fica sob o teto — a contagem permanece 1896 (medido no tip mesclado com check-complexity.mjs).", "_ratchet_2026_06_19_phasecombosetup_fix": "1896->1895 (-1, ratchet DOWN — melhoria, NAO reconciliacao). O #4336 reconciliou o drift do lote para 1896 INCLUINDO a violacao que o #4326 (ComboContext) introduziu: phaseComboSetup media complexity 17 (>15) porque a extracao concentrou os condicionais de pinning/ternarios numa funcao que estourava o teto (irônico p/ uma decomposicao). Este PR CORRIGE na origem — extrai resolveContextCachePin (helper do pinning), phaseComboSetup volta a <15 — baixando a contagem 1. Medido com `node scripts/check/check-complexity.mjs` no tip pos-#4336.", "_rebaseline_2026_06_19_lote3_postdeploy_drift": "Reconciliacao release-volatil pos-merge do lote adicional (6 PRs apos o deploy): 1890->1896 (+6). Drift de condicionais NOVOS de #4327 (per-key USD usage quotas — apiKeyUsageLimits.ts + validation/policy branches), #4334 (cache-aware compression guard) e #4326 (phaseComboSetup extraido). Medido no tip real da release com `node scripts/check/check-complexity.mjs`. Mesma familia/justificativa do _rebaseline_2026_06_19_lote3_merge_drift abaixo — feature legitima, nao regressao.", "_rebaseline_2026_06_19_lote3_merge_drift": "Reconciliacao release-volatil pos-merge do lote de 13 PRs (release/v3.8.30): 1888->1890 (+2). Drift de condicionais NOVOS trazidos por #4313 (5 harvested features — combo allowlist intersection, serviceKind filter) e #4323 (compression e2e — novos ramos em ultra/aggressive/gcf/strategySelector), merges que entraram DEPOIS do #4318 medir 1888. O fast-path do release nao roda check:complexity (so release->main), entao os ramos acumulam sem rebaselinar. Medido no tip real da release pos-merge com `node scripts/check/check-complexity.mjs`. Mesma familia dos rebaselines anteriores — crescimento de feature legitimo, nao regressao; reducao fica como debt de refactor dedicado.", diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index f01e763185..76eb361ca1 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -41,7 +41,7 @@ import { resolveMemoryOwnerId, } from "./chatCore/memoryExtraction.ts"; import { CORS_HEADERS } from "../utils/cors.ts"; -import { HEAP_PRESSURE_THRESHOLD_MB } from "../utils/heapPressure.ts"; +import { checkHeapPressureGuard } from "../utils/heapPressure.ts"; import { normalizeHeaders } from "../utils/headers.ts"; import { detectFormatFromEndpoint, getTargetFormat } from "../services/provider.ts"; import { injectSystemPrompt } from "../services/systemPrompt.ts"; @@ -643,27 +643,8 @@ export async function handleChatCore({ // cascading OOM when many large-context requests arrive concurrently. try { const heapUsedMB = process.memoryUsage().heapUsed / (1024 * 1024); - if (heapUsedMB > HEAP_PRESSURE_THRESHOLD_MB) { - // Internal telemetry only — never expose the heap figure to clients (Hard Rule #12). - console.warn( - `[chatCore] heap pressure guard tripped: ${Math.round(heapUsedMB)}MB > ${HEAP_PRESSURE_THRESHOLD_MB}MB; returning 503` - ); - return { - success: false, - status: 503, - error: "Service temporarily unavailable due to resource pressure. Retry shortly.", - response: new Response( - JSON.stringify({ - error: { - message: "Service temporarily unavailable due to resource pressure. Retry shortly.", - type: "server_error", - code: "heap_pressure", - }, - }), - { status: 503, headers: { "Content-Type": "application/json", "Retry-After": "5" } } - ), - }; - } + const heapGuard = checkHeapPressureGuard(heapUsedMB); + if (heapGuard) return heapGuard; } catch { /* memoryUsage() never throws */ } diff --git a/open-sse/utils/heapPressure.ts b/open-sse/utils/heapPressure.ts index 85f6050d2c..659853356d 100644 --- a/open-sse/utils/heapPressure.ts +++ b/open-sse/utils/heapPressure.ts @@ -42,3 +42,40 @@ export const HEAP_PRESSURE_THRESHOLD_MB = computeHeapPressureThresholdMb( v8.getHeapStatistics().heap_size_limit / (1024 * 1024), process.env.HEAP_PRESSURE_THRESHOLD_MB ); + +const HEAP_PRESSURE_MESSAGE = + "Service temporarily unavailable due to resource pressure. Retry shortly."; + +export type HeapPressureGuardResult = { + success: false; + status: 503; + error: string; + response: Response; +}; + +/** + * Memory-pressure shed guard for the chat pipeline (extracted from chatCore's handleChatCore). + * Returns a ready-to-return 503 result when live heap usage exceeds the shed threshold, else null + * to proceed. The heap figure is logged for INTERNAL telemetry only and is NEVER placed in the + * client-facing response (Hard Rule #12). Behaviour is byte-identical to the previous inline guard. + */ +export function checkHeapPressureGuard( + heapUsedMb: number, + thresholdMb: number = HEAP_PRESSURE_THRESHOLD_MB +): HeapPressureGuardResult | null { + if (heapUsedMb <= thresholdMb) return null; + console.warn( + `[chatCore] heap pressure guard tripped: ${Math.round(heapUsedMb)}MB > ${thresholdMb}MB; returning 503` + ); + return { + success: false, + status: 503, + error: HEAP_PRESSURE_MESSAGE, + response: new Response( + JSON.stringify({ + error: { message: HEAP_PRESSURE_MESSAGE, type: "server_error", code: "heap_pressure" }, + }), + { status: 503, headers: { "Content-Type": "application/json", "Retry-After": "5" } } + ), + }; +} diff --git a/tests/unit/heap-pressure.test.ts b/tests/unit/heap-pressure.test.ts index 621b1ea712..0f3fd0f0d7 100644 --- a/tests/unit/heap-pressure.test.ts +++ b/tests/unit/heap-pressure.test.ts @@ -1,6 +1,9 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; -import { computeHeapPressureThresholdMb } from "../../open-sse/utils/heapPressure.ts"; +import { + checkHeapPressureGuard, + computeHeapPressureThresholdMb, +} from "../../open-sse/utils/heapPressure.ts"; // Regression guard for the v3.8.8 "Service temporarily unavailable due to resource // pressure" outage: a fixed 200MB threshold sat below the app's ~260MB working set, @@ -46,3 +49,33 @@ describe("computeHeapPressureThresholdMb", () => { } }); }); + +// Extracted from chatCore's handleChatCore (god-file decomposition, first leaf). The SET of +// behaviours below is byte-identical to the previous inline guard. +describe("checkHeapPressureGuard", () => { + it("returns null (proceed) when heap usage is at or below the threshold", () => { + assert.equal(checkHeapPressureGuard(100, 256), null); + assert.equal(checkHeapPressureGuard(256, 256), null, "boundary: == threshold proceeds"); + }); + + it("returns a 503 shed result when heap usage exceeds the threshold", async () => { + const guard = checkHeapPressureGuard(300, 256); + assert.ok(guard, "must shed above threshold"); + assert.equal(guard.success, false); + assert.equal(guard.status, 503); + assert.equal(guard.response.status, 503); + assert.equal(guard.response.headers.get("Retry-After"), "5"); + assert.equal(guard.response.headers.get("Content-Type"), "application/json"); + const payload = await guard.response.json(); + assert.equal(payload.error.code, "heap_pressure"); + assert.equal(payload.error.type, "server_error"); + }); + + it("never leaks the heap figure to the client response (Hard Rule #12)", async () => { + const guard = checkHeapPressureGuard(987, 256); + assert.ok(guard); + const text = JSON.stringify(await guard.response.clone().json()) + (guard.error ?? ""); + assert.ok(!text.includes("987"), "the measured heap MB must not appear in the client payload"); + assert.ok(!/\bMB\b/.test(text), "no heap-size detail should reach the client"); + }); +});