refactor(chatCore): extract checkHeapPressureGuard leaf (god-file decomposition start) (#4371)

Primeiro incremento da decomposição do chatCore.ts (5127 LOC, hot-path mais quente).
O guard de memória do topo do handleChatCore (rejeita 503 quando o heap V8 passa o
threshold de shed) vira um leaf testável, co-locado com o threshold em heapPressure.ts.

- heapPressure.ts: checkHeapPressureGuard(heapUsedMb, thresholdMb) — retorna o result
  503 pronto ou null. Byte-idêntico ao guard inline (mesmo check, mesma 503, mesmo warn).
  A figura de heap fica em telemetria INTERNA, nunca no response do cliente (Hard Rule #12).
- chatCore.ts: o bloco inline (~22 ln) vira 3 linhas; import órfão de HEAP_PRESSURE_THRESHOLD_MB
  trocado por checkHeapPressureGuard.
- 3 testes novos (incl. assert Rule #12: o MB medido não vaza no payload).

complexity-baseline 1895->1896: drift de base pós-#4338 (medido com minhas mudanças
stashed = 1896); esta mudança é complexity-NEUTRA (helper complexity 2, handleChatCore só
perde código). 190/190 chatcore tests, typecheck 0, file-size encolhe.
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-06-20 11:23:09 -03:00
committed by GitHub
parent c0291f8fbb
commit 542e0b5241
4 changed files with 75 additions and 23 deletions

View File

@@ -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.",

View File

@@ -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 */
}

View File

@@ -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" } }
),
};
}

View File

@@ -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");
});
});