fix(process): bound normalized terminal retention before allocation

Pre-count ordered normalization stages before materializing output. Charge original and normalized terminal bytes independently, omit whole over-budget chunks and preserve exact raw counts plus explicit unknown final sizes. Bound rendered normalization, retain legacy capture compatibility, and remove temporary per-match output arrays.

Validation: UTF-8 surrogate boundaries, shrinking/no-op/literal rules, capture validation and public MCP expansion/equal-size controls with verified cleanup. CPU, process-family memory and token changes are unmeasured; no performance speedup claimed. Nonterminal derived payload bounds remain separate work.

Fixes #3168
This commit is contained in:
morluto committed 2026-10-14 16:54:08 +08:00
1 parent 7fb0a0cc57
commit 1d826f304f
14 files changed
+894 -76

No files matched your search

@@ -1,6 +1,7 @@
import { expect, it } from "vitest";
import {
createBoundedProcessTextNormalizer,
createProcessTextNormalizer,
normalizeProcessSamples,
normalizeProcessText,
@@ -95,6 +96,178 @@ it("replaces caller-declared patterns literally, without expanding replacement p
).toBe("listening on $&$&; $$; $1=alpha; tail $1");
});
it("bounds ordered normalization before replacement expansion and preserves later smaller chunks", () => {
const normalize = createBoundedProcessTextNormalizer(
scenario({
paths: false,
ports: false,
pids: false,
patterns: [{ pattern: "a", replacement: "x".repeat(500) }],
}),
"/temporary",
rootPid,
);
expect(normalize("a".repeat(100), 1_000)).toEqual({ state: "over_budget" });
expect(normalize("z", 1_000)).toEqual({
state: "normalized",
value: "z",
utf8Bytes: 1,
});
});
it("keeps ordered intermediate expansion explicit even when a later rule could shrink it", () => {
const normalize = createBoundedProcessTextNormalizer(
scenario({
paths: false,
ports: false,
pids: false,
patterns: [
{ pattern: "a", replacement: "x".repeat(20) },
{ pattern: "x", replacement: "y" },
],
}),
"/temporary",
rootPid,
);
expect(normalize("a", 10)).toEqual({ state: "over_budget" });
});
it("counts UTF-8 bytes across replacement boundaries like the final JavaScript string", () => {
const splitPair = scenario({
paths: false,
ports: false,
pids: false,
patterns: [{ pattern: "\ude00", replacement: "\ude01" }],
});
const splitPairResult = createBoundedProcessTextNormalizer(
splitPair,
"/temporary",
rootPid,
)("😀", 4);
expect(splitPairResult).toEqual({
state: "normalized",
value: normalizeProcessText("😀", splitPair, "/temporary", rootPid),
utf8Bytes: 4,
});
expect(
splitPairResult.state === "normalized" && splitPairResult.utf8Bytes,
).toBe(
Buffer.byteLength(
normalizeProcessText("😀", splitPair, "/temporary", rootPid),
),
);
const joinedPair = scenario({
paths: false,
ports: false,
pids: false,
patterns: [{ pattern: "\ude00\ud83d", replacement: "" }],
});
const joinedInput = "😀😀";
const joinedExpected = normalizeProcessText(
joinedInput,
joinedPair,
"/temporary",
rootPid,
);
expect(joinedExpected).toBe("😀");
expect(
createBoundedProcessTextNormalizer(
joinedPair,
"/temporary",
rootPid,
)(joinedInput, Buffer.byteLength(joinedExpected)),
).toEqual({
state: "normalized",
value: joinedExpected,
utf8Bytes: Buffer.byteLength(joinedExpected),
});
const emptyPattern = scenario({
paths: false,
ports: false,
pids: false,
patterns: [{ pattern: "", replacement: "\ude00" }],
});
const input = "\ud83d";
const expected = normalizeProcessText(
input,
emptyPattern,
"/temporary",
rootPid,
);
expect(
createBoundedProcessTextNormalizer(
emptyPattern,
"/temporary",
rootPid,
)(input, Buffer.byteLength(expected)),
).toEqual({
state: "normalized",
value: expected,
utf8Bytes: Buffer.byteLength(expected),
});
expect(
createBoundedProcessTextNormalizer(
emptyPattern,
"/temporary",
rootPid,
)(input, Buffer.byteLength(expected) - 1),
).toEqual({ state: "over_budget" });
const loneSurrogate = scenario({
paths: false,
ports: false,
pids: false,
patterns: [{ pattern: "x", replacement: "y" }],
});
const loneInput = "\ud83dx";
const loneExpected = normalizeProcessText(
loneInput,
loneSurrogate,
"/temporary",
rootPid,
);
expect(
createBoundedProcessTextNormalizer(
loneSurrogate,
"/temporary",
rootPid,
)(loneInput, Buffer.byteLength(loneExpected)),
).toEqual({
state: "normalized",
value: loneExpected,
utf8Bytes: Buffer.byteLength(loneExpected),
});
});
it("lets a later pattern shrink text when an earlier pattern does not match", () => {
const declared = scenario({
paths: false,
ports: false,
pids: false,
patterns: [
{ pattern: "z", replacement: "x" },
{ pattern: "aa", replacement: "b" },
],
});
const expected = normalizeProcessText("aa", declared, "/temporary", rootPid);
expect(expected).toBe("b");
expect(
createBoundedProcessTextNormalizer(
declared,
"/temporary",
rootPid,
)("aa", Buffer.byteLength(expected)),
).toEqual({
state: "normalized",
value: expected,
utf8Bytes: Buffer.byteLength(expected),
});
});
it("reuses matchers while observing the terminal PID after launch", () => {
let pid = -1;
const normalize = createProcessTextNormalizer(