#985 was about AGENT_ASK_RATE_LIMIT_PER_HOUR=0 silently becoming 30, and #990 fixes it. While verifying that fix I grepped backend/ for the same shape — parseInt(process.env.X || '…', 10) || FALLBACK — to see whether it was one site or a habit.
It is five sites. They do not all mean the same thing, and that is the point of this issue: a shared mechanism licenses "check the other sites", never "the other sites are also broken". I read each consumer. Two are real, one is load-bearing protection that must be left alone, one is marginal.
Real — backend/services/agentEventService.ts:531
const thresholdKb = Math.max(
64,
Number.parseInt(process.env.AGENT_SESSION_MAX_SIZE_KB || '', 10) || 400,
);
| env |
resolves to |
unset / '' / abc |
400 |
10 |
64 (floor) |
0 |
400 |
The floor exists to stop an operator setting the session-clear threshold absurdly low. 0 is the most aggressive value expressible, and it produces the laxest threshold of any input — 6× the floor it was trying to undercut. Someone reaching for 0 here is trying to clear sessions eagerly (the CLAUDE.md rule about session bloat being broken behaviour is exactly the pressure that produces that reach); they get the opposite, with no error. Math.max makes the fallback unnecessary anyway: parse, Number.isNaN → 400, then clamp.
Real, and sharper — backend/services/schedulerService.ts:767
const BATCH_PAUSE_MS = parseInt(process.env.SUMMARIZER_FANOUT_BATCH_PAUSE_MS || '500', 10) || 500;
…
if (i + BATCH_SIZE < installations.length && BATCH_PAUSE_MS > 0) {
await new Promise((resolve) => setTimeout(resolve, BATCH_PAUSE_MS));
}
The consumer already branches on BATCH_PAUSE_MS > 0 — the code anticipates "no pause between batches" as a supported configuration. The parser on the line above guarantees that branch is never taken: every input yields 500 or a positive integer, and 0 yields 500. So this is two defects wearing one line:
SUMMARIZER_FANOUT_BATCH_PAUSE_MS=0 silently does the opposite of what it says — a half-second pause per batch.
&& BATCH_PAUSE_MS > 0 is dead code. It can never be false.
Whichever way it is resolved, both halves should move together — fixing the parse without noticing the guard leaves a reader thinking the guard was always live, and deleting the guard without fixing the parse removes the only evidence that 0 was meant to work.
Not a defect — backend/services/schedulerService.ts:766
const BATCH_SIZE = parseInt(process.env.SUMMARIZER_FANOUT_BATCH_SIZE || '10', 10) || 10;
Three lines above the previous one and syntactically identical, so a mechanical sweep would "fix" it. Don't. The loop is for (let i = 0; i < installations.length; i += BATCH_SIZE). A delivered 0 is an infinite loop that enqueues the same batch forever against the event queue. Here || 10 is the only thing standing between a typo and a runaway hourly job. If anything it deserves an explicit Math.max(1, …) and a comment saying why, not a Number.isNaN conversion.
Marginal — backend/gateway/index.ts:80
const port = options.port || parseInt(process.env.GATEWAY_PORT, 10) || 5001;
0 is meaningful to Node (bind an ephemeral port) and is a real thing to want in tests, so it is the same class of bug. But nothing in-tree passes 0, and the failure is "you got 5001 instead of a random port", which is loud immediately. Worth folding in if the file is being touched; not worth a PR.
Suggested scope
One PR covering agentEventService.ts:531 and schedulerService.ts:767 (+ its dead guard), each with a test in the shape #990 established — pin 0, '', unset, and a valid value, so that once 0 stops routing to the default an empty string reading as 0 cannot become the next regression. A k8s env block spells "unset" as '', which is why that control pair matters more here than it looks.
Explicitly out of scope: schedulerService.ts:766. Leaving a note in this issue is cheaper than re-deriving why it is different when someone greps the pattern again.
Found while reviewing #990 (verified red/green there); not filed on that PR so it does not race the merge.
#985 was about
AGENT_ASK_RATE_LIMIT_PER_HOUR=0silently becoming30, and #990 fixes it. While verifying that fix I greppedbackend/for the same shape —parseInt(process.env.X || '…', 10) || FALLBACK— to see whether it was one site or a habit.It is five sites. They do not all mean the same thing, and that is the point of this issue: a shared mechanism licenses "check the other sites", never "the other sites are also broken". I read each consumer. Two are real, one is load-bearing protection that must be left alone, one is marginal.
Real —
backend/services/agentEventService.ts:531''/abc100The floor exists to stop an operator setting the session-clear threshold absurdly low.
0is the most aggressive value expressible, and it produces the laxest threshold of any input — 6× the floor it was trying to undercut. Someone reaching for0here is trying to clear sessions eagerly (the CLAUDE.md rule about session bloat being broken behaviour is exactly the pressure that produces that reach); they get the opposite, with no error.Math.maxmakes the fallback unnecessary anyway: parse,Number.isNaN→ 400, then clamp.Real, and sharper —
backend/services/schedulerService.ts:767The consumer already branches on
BATCH_PAUSE_MS > 0— the code anticipates "no pause between batches" as a supported configuration. The parser on the line above guarantees that branch is never taken: every input yields500or a positive integer, and0yields500. So this is two defects wearing one line:SUMMARIZER_FANOUT_BATCH_PAUSE_MS=0silently does the opposite of what it says — a half-second pause per batch.&& BATCH_PAUSE_MS > 0is dead code. It can never be false.Whichever way it is resolved, both halves should move together — fixing the parse without noticing the guard leaves a reader thinking the guard was always live, and deleting the guard without fixing the parse removes the only evidence that
0was meant to work.Not a defect —
backend/services/schedulerService.ts:766Three lines above the previous one and syntactically identical, so a mechanical sweep would "fix" it. Don't. The loop is
for (let i = 0; i < installations.length; i += BATCH_SIZE). A delivered0is an infinite loop that enqueues the same batch forever against the event queue. Here|| 10is the only thing standing between a typo and a runaway hourly job. If anything it deserves an explicitMath.max(1, …)and a comment saying why, not aNumber.isNaNconversion.Marginal —
backend/gateway/index.ts:800is meaningful to Node (bind an ephemeral port) and is a real thing to want in tests, so it is the same class of bug. But nothing in-tree passes0, and the failure is "you got 5001 instead of a random port", which is loud immediately. Worth folding in if the file is being touched; not worth a PR.Suggested scope
One PR covering
agentEventService.ts:531andschedulerService.ts:767(+ its dead guard), each with a test in the shape #990 established — pin0,'', unset, and a valid value, so that once0stops routing to the default an empty string reading as0cannot become the next regression. A k8s env block spells "unset" as'', which is why that control pair matters more here than it looks.Explicitly out of scope:
schedulerService.ts:766. Leaving a note in this issue is cheaper than re-deriving why it is different when someone greps the pattern again.Found while reviewing #990 (verified red/green there); not filed on that PR so it does not race the merge.