Follow-up from #599 / #606. Found while validating the first real plugin manifest against the shipped feature.
Summary
matchWithBudget in middleware/src/plugins/setupFieldPattern.ts measures a window that includes worker startup and IPC round-trip, not just regex execution. Two consequences, both timing-dependent — which means they surface as intermittent "my correct value was rejected" reports, the hardest kind to diagnose.
- A valid value can be rejected fail-closed because the machine was busy, not because the value was wrong.
- A healthy pattern gets recorded in the problems registry and is then reported to the operator as "this field declares a format check that could not be applied" — permanently, for the life of the process.
Both land in exactly the failure class the customer test report was about: the UI stating something that is not true.
Measurements (compiled dist, i.e. the production path, node 22.22.3)
Trivial pattern ^[^@\s]+@[^@\s]+\.[A-Za-z]{2,63}$ against assistant@te-printline.de, five sequential calls in a fresh process:
call 1: 838.0 ms → accepted
call 2: 10.8 ms → accepted
call 3: 11.7 ms → accepted
call 4: 4.0 ms → accepted
call 5: 35.7 ms → accepted
Two things stand out:
- Call 1 costs 838 ms — worker boot dominates. It happened to be accepted in that run, but in another run on the same machine the equivalent first call overran and was rejected, emitting:
[setup] pattern match exceeded 50ms for gw_subject_default; treating the value as a violation.
So whether the first credential save after a restart succeeds is a coin flip on machine load.
- Call 5 took 35.7 ms for a trivial match — 71% of the entire budget, with no regex work to speak of. The 4–36 ms spread on identical input is IPC and scheduling noise, so a loaded host crossing 50 ms is not exotic.
The tsx path is worse still: driving the module under node --import tsx makes every call overrun — even /^a$/ against 'a', at 84–212 ms — because the worker re-runs tsx's loader. Once a worker is terminated the next call pays boot again, so it becomes self-sustaining. node --test does not hit this, which is why the test suite is green.
Registry poisoning
An overrun writes into getPatternProblems():
registry: [{"context":"gw_subject_default",
"pattern":"^[^@\\s]+@[^@\\s]+\\.[A-Za-z]{2,63}$",
"reason":"match exceeded the 50ms execution budget"}]
Every subsequent call with that same pattern succeeded in 4–12 ms — the pattern is completely fine. But the entry persists, and per #599's design that registry is what surfaces "this field declares a format check that could not be applied" to the operator. A single unlucky first call permanently mislabels a healthy pattern.
Why the design is still right
To be clear: the worker budget is the correct mechanism and should stay. A regex cannot be interrupted on the main thread, and #599 measured ^(a|a)+$ blocking the event loop for 1739 ms at a 26-character subject. Nothing here argues for removing it — only for measuring the right window.
Suggested fixes
- Start the clock when the worker acknowledges the job, not when it is dispatched. The budget should bound regex execution, which is the only thing that can run away.
- Warm the worker at startup, or keep it pooled and pre-booted, so no operator-facing request ever pays the ~838 ms boot.
- Do not write a budget overrun into the pattern-problems registry. An overrun is evidence about this execution, not about the pattern. Log it, count it, and only mark the pattern problematic after it overruns repeatedly (say N consecutive times) — mirroring the circuit-breaker pattern already used for plugin activation.
- Consider raising the budget, or making it adaptive to observed IPC latency. 50 ms is tight when the measurement floor is already ~10 ms of pure overhead.
- Fix the
tsx case or document it, so anyone driving this module in dev outside node --test is not silently blocked.
Repro
// from middleware/, after npm run build
import { checkSetupFieldPattern } from './dist/plugins/setupFieldPattern.js';
const f = { key: 'email', type: 'string', label: 'e',
pattern: String.raw`^[^@\s]+@[^@\s]+\.[A-Za-z]{2,63}$` };
for (let i = 1; i <= 5; i++) {
const t = process.hrtime.bigint();
const r = await checkSetupFieldPattern(f, 'assistant@te-printline.de');
console.log(i, Number(process.hrtime.bigint() - t) / 1e6, r === null ? 'accepted' : 'REJECTED');
}
Follow-up from #599 / #606. Found while validating the first real plugin manifest against the shipped feature.
Summary
matchWithBudgetinmiddleware/src/plugins/setupFieldPattern.tsmeasures a window that includes worker startup and IPC round-trip, not just regex execution. Two consequences, both timing-dependent — which means they surface as intermittent "my correct value was rejected" reports, the hardest kind to diagnose.Both land in exactly the failure class the customer test report was about: the UI stating something that is not true.
Measurements (compiled
dist, i.e. the production path, node 22.22.3)Trivial pattern
^[^@\s]+@[^@\s]+\.[A-Za-z]{2,63}$againstassistant@te-printline.de, five sequential calls in a fresh process:Two things stand out:
The
tsxpath is worse still: driving the module undernode --import tsxmakes every call overrun — even/^a$/against'a', at 84–212 ms — because the worker re-runs tsx's loader. Once a worker is terminated the next call pays boot again, so it becomes self-sustaining.node --testdoes not hit this, which is why the test suite is green.Registry poisoning
An overrun writes into
getPatternProblems():Every subsequent call with that same pattern succeeded in 4–12 ms — the pattern is completely fine. But the entry persists, and per #599's design that registry is what surfaces "this field declares a format check that could not be applied" to the operator. A single unlucky first call permanently mislabels a healthy pattern.
Why the design is still right
To be clear: the worker budget is the correct mechanism and should stay. A regex cannot be interrupted on the main thread, and #599 measured
^(a|a)+$blocking the event loop for 1739 ms at a 26-character subject. Nothing here argues for removing it — only for measuring the right window.Suggested fixes
tsxcase or document it, so anyone driving this module in dev outsidenode --testis not silently blocked.Repro