Repository navigation
feat(tasks): a linear-time regex runner for hosts that cannot interrupt a match - #1032
Conversation
…pt a match A Worker or Durable Object has no vm timeout, so the bounded runner the Node entry installs cannot run there, and the browser default matches unbounded: one catastrophic pattern stalls every object sharing the isolate until the platform kills the invocation. createLinearRegexRunnerFactory takes an RE2-family engine from the host (re2js's RE2JS satisfies it as is, so @workglow/tasks gains no dependency) and registers through the existing registerRegexRunnerFactory seam. Backreferences, lookaround and the y and v flags are refused rather than matched without a bound. Where RE2 reads JavaScript syntax differently the source is rewritten to JavaScript's meaning: \s and \S, '.', a multiline ^, [^] and [\b]. A multiline $ is refused over input with \r line endings, which RE2 cannot match faithfully. The tests check it against the native engine over a corpus; without the rewrite eight of them fail. SafeFetch's header now records why a workerd host keeps the browser default: workerd refuses private peers at connect(), after DNS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVgexVV91n4PaqB1i5Raag
…egex fixtures as strings typecheck-budget: packages/a2a went from 18,951 to 25,707 instantiations. The cause is one more export from @workglow/tasks' barrel, not anything a2a uses: a module exporting a single constant, a named re-export and a star export at the end of the barrel each give the same 25,707, and one further trivial export gives 25,346. a2a imports three names from that package. The count follows the barrel's shape, so the entry is set to the measured value; the other 43 are unchanged. CodeQL flagged the catastrophic patterns in LinearRegexRunner.test.ts as inefficient regexes. They are deliberate fixtures that only the linear engine ever sees. They are now strings compiled per test, as RegexTask's own catastrophic-pattern test keeps its pattern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVgexVV91n4PaqB1i5Raag
|
What fails. The HF Inference conformance test ( Why. The test pins the router provider Proposed patch (not applied here, so this PR doesn't widen): - provider_config: { model_name: "meta-llama/Llama-3.3-70B-Instruct", provider: "together" },
+ provider_config: { model_name: "meta-llama/Llama-3.3-70B-Instruct", provider: "novita" },A second blocker may sit behind it. With Pushed in d6e5461 for the other two red checks:
Generated by Claude Code |
…n deepinfra Ported from libs#1031 (6cf4074), as that change leaves the file. The suite pinned Llama 3.3 70B to the together router, which no longer serves it, so every tool-call run failed. The net change from main is the model and provider swap; #1031's interim skip is not carried. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVgexVV91n4PaqB1i5Raag
|
The job is still red on Gemini alone: 3 tests in Generated by Claude Code |
Each fix makes the linear runner match what the native engine does:
- A '[' inside a character class is escaped, so RE2 no longer reads
[:name:] as a POSIX class where JavaScript reads literal characters.
- Without the u flag, \A \z \a \Q \E \p \P \x{ \u{ become the plain
letter (keeping a following {n} a quantifier), and \c before a
non-letter becomes a literal backslash, or a control character inside
a class before a digit or '_', as JavaScript reads them.
- A '-' beside an expanded \s or \S in a class is escaped, so
[\d\s-()] compiles and [\s-\uffff] matches what it does natively.
- The factory keeps the last 64 compiled programs, keyed by source and
flags, so a RegexTask fan-out over one pattern compiles it once. A
refused pattern is never cached.
- LinearRegexSource is renamed ILinearRegexSource, per the I-prefix rule
for public interfaces, and toLinearRegexSource gets a direct test.
The module header now states what it does not fix: execAll can be
quadratic for patterns like /a[^z]*z|a/g, quantified groups keep stale
captures and do not stop on an empty iteration, and case folding under
i without u is Unicode folding. SafeFetch's note says the connect-time
refusal holds when deployed; under Miniflare only the static check
applies. 15 new corpus cases cover the fixes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVgexVV91n4PaqB1i5Raag
There was a problem hiding this comment.
Review progress ██████████ 9/9 files
Request changes — found 2 issue(s) at c4be555.
Actionable comments posted: 2
🤖 Prompt for AI agents
Pull request workglow-dev/libs#1032: https://github.com/workglow-dev/libs/pull/1032 (head branch `claude/friendly-ptolemy-5fxfqz`)
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
Findings to address:
1. In `@packages/tasks/src/util/LinearRegexRunner.ts` (line 222, Blocker):
`out += ch` passes a non-multiline `$` through unchanged, but RE2 treats it as end-of-text while JavaScript also lets it match immediately before a final line terminator. For example, `new RegExp("foo$")` matches `"foo\n"` natively, while this runner does not; `RegexTask` therefore reports a false negative for that input.
2. In `@packages/tasks/src/util/LinearRegexRunner.ts` (line 156, Important):
`out += "\\\\c"` makes a legacy `\c` followed by a non-letter match a literal backslash plus `c`. JavaScript treats that as an identity escape for `c` outside a class, so `new RegExp("\\c1", "g")` on the input `"\\c1"` matches `"c1"` natively, but this runner matches `"\\c1"`. This makes the linear runner return different `RegexTask.matches` for the pattern/input pair.
…my-5fxfqz # Conflicts: # bun.lock
|
What fails. Proposed patch. No fix exists on any branch yet, so I haven't applied one here. -export const TFMP_VISION_WASM_VERSION = "1.1.0";
-export const TFMP_TEXT_WASM_VERSION = "1.1.0";
-export const TFMP_AUDIO_WASM_VERSION = "1.1.0";
-export const TFMP_GENAI_WASM_VERSION = "0.10.29";
+export const TFMP_VISION_WASM_VERSION = "1.1.1";
+export const TFMP_TEXT_WASM_VERSION = "1.1.1";
+export const TFMP_AUDIO_WASM_VERSION = "1.1.1";
+export const TFMP_GENAI_WASM_VERSION = "0.10.30";One thing to check with it: Generated by Claude Code |
The catalog moved to @mediapipe/tasks-{vision,text,audio} 1.1.1 and
tasks-genai 0.10.30, but the CDN fileset pins stayed at 1.1.0 / 0.10.29,
so the JS SDK loaded a WASM build from the release before it. Every file
each installed package ships under wasm/ is served by jsdelivr at the new
version. 0.10.30 still does not export clearCancelSignals.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVgexVV91n4PaqB1i5Raag
A global scan can be quadratic in the input (`/a[^z]*z|a/g` over a run of `a`), and a Worker has no clock to stop one with, so a runner from createLinearRegexRunnerFactory refuses an input longer than LINEAR_REGEX_MAX_INPUT_LENGTH (65,536 UTF-16 code units) with a TaskInvalidInputError, for one match and a global scan alike. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVgexVV91n4PaqB1i5Raag
There was a problem hiding this comment.
Review progress ██████████ 11/11 files
Comment — found 1 issue(s) at 1be9094.
Actionable comment posted: 1
🤖 Prompt for AI agents
Pull request workglow-dev/libs#1032: https://github.com/workglow-dev/libs/pull/1032 (head branch `claude/friendly-ptolemy-5fxfqz`)
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
Findings to address:
1. In `@packages/tasks/src/util/LinearRegexRunner.ts` (line 144, Important):
`\k` is not always a named backreference: without `u` and without named capture groups, JavaScript treats it as an identity escape matching `k` (and inside a character class it is literal as well). Thus valid patterns such as `new RegExp("\\k")` and `new RegExp("[\\k]")` are rejected by this runner even though they need no unsupported feature; distinguish the `\k<name>` backreference form from these identity escapes before refusing.
…roup Without `u` and without a named group, JavaScript reads `\k` as an identity escape, in a class or out; only where a named group or `u` makes it a backreference does the linear runner have to refuse it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVgexVV91n4PaqB1i5Raag
There was a problem hiding this comment.
Review progress ██████████ 10/10 files
Comment — found 1 issue(s) at bc02aed.
Actionable comment posted: 1
🤖 Prompt for AI agents
Pull request workglow-dev/libs#1032: https://github.com/workglow-dev/libs/pull/1032 (head branch `claude/friendly-ptolemy-5fxfqz`)
Verify each finding against current code. Fix only still-valid concrete bugs, skip the
rest with a brief reason, keep changes minimal, and validate. Skip Decision required,
policy forks, and "consider X" alternatives. Do not add new features, refactors, or
architecture beyond the fix; prefer the smallest diff.
Findings to address:
1. In `@packages/tasks/src/util/LinearRegexRunner.ts` (line 166, Important):
`out += "\\\\c"` is also used when `inClass` is true and `\c` is followed by punctuation (for example, `new RegExp("[\\c!]")`). In a legacy JavaScript character class, that `\c` is an identity escape for `c`, so the native class matches `c` or `!`, not a backslash. This rewrite adds `\\` to the class, causing the linear runner (and RegexTask when installed) to report an extra match for `"\\"`.
A global scan can be quadratic in its input, so 64 Ki still let one pattern run for minutes: `/a[^z]*z|a/g` over a run of `a` took 7.7 s at 16 Ki and 30.8 s at 32 Ki under Node, and did not finish in 300 s at 64 Ki. A global scan is now refused past LINEAR_REGEX_MAX_GLOBAL_INPUT_LENGTH (16,384). One match is linear in its input and keeps LINEAR_REGEX_MAX_INPUT_LENGTH (65,536). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVgexVV91n4PaqB1i5Raag
…my-5fxfqz # Conflicts: # providers/tf-mediapipe/src/ai/common/TFMP_Runtime.ts
Summary
createLinearRegexRunnerFactory(engine)in@workglow/tasks(packages/tasks/src/util/LinearRegexRunner.ts). It is aRegexRunnerFactoryover an RE2-family engine. A host installs it through the existingregisterRegexRunnerFactoryseam.SafeFetch.tsheader: records why a deployed Worker keeps the browser default.TFMP_GenaiRuntime.tscomment: 0.10.30 still lacksclearCancelSignalsin its bundle, so the fallback comment names both versions. The WASM pin fix this branch carried is superseded bymain's, which takes the versions from the installed SDKs at build time.deepinfra, ported from libs#1031.a2a's typecheck budget is re-baselined; the measurements are in the commit message.Why
The Node entry installs a bounded regex runner that uses
node:vmtimeouts. A Cloudflare Worker or Durable Object has novm, and it resolves thebrowserbuild of every@workglow/*package, where the regex runner is unbounded. One catastrophic pattern inRegexTaskwould stall every object sharing the isolate until the platform kills the invocation. The workglow.com server tier is moving to Durable Objects, so it needs a bound that doesn't depend on interrupting a match.How it behaves
re2js'sRE2JSclass satisfies the interface as is, so@workglow/taskstakes no new dependency.re2jsis pinned at 2.8.6 as a test-only dependency.(a|a)*$and^(a?b?)*$finish at once on 50,000-character inputs.TaskInvalidInputError.execAllwith thegflag) is limited to 16,384 code units (LINEAR_REGEX_MAX_GLOBAL_INPUT_LENGTH). A scan can grow with the square of the input: for/a[^z]*z|a/gover a run ofa, each match reads to the end. Under Node that pattern takes 7.7 s at 16k and 30.8 s at 32k, and did not finish in 300 s at 64k. Ordinary patterns scan in linear time.exec, orexecAllwithoutg) is limited to 65,536 code units (LINEAR_REGEX_MAX_INPUT_LENGTH), since a single match is linear.yandvflags. Each is refused with aTaskInvalidInputErrorrather than matched without a bound.\kcounts as a backreference only where JavaScript reads it as one, in a pattern withuor a named group.re2jswould otherwise silently turn it into literal text. Elsewhere\kis the letterk.\sand\S,., multiline^,[^]and[\b];[inside a class, which RE2 would read as a POSIX class;u, the escapes JavaScript reads as plain letters (\A,\z,\a,\Q,\E,\p,\P,\k,\x{,\u{) and\cbefore a non-letter;-beside an expanded\sor\S.$is refused over input with\r, U+2028 or U+2029 line endings, including Windows CRLF text;u, an astral character counts as one character;iwithoutu, case folding is Unicode folding (/s/ialso matchesſ).RegexTaskfan-out over one pattern compiles it once.On
SafeFetch, no new implementation is needed when deployed. workerd's outbound network refuses private, loopback and link-local peers atconnect(), after DNS. This was measured with a hostname mapped to127.0.0.1, plus127.0.0.1,169.254.169.254and10.0.0.1: all four were refused. A deployed Worker on Cloudflare also refusednip.iohostnames resolving to private addresses. Under Miniflare (wrangler dev), private addresses are allowed, so only the static check applies there.Tests
packages/test/src/test/task/LinearRegexRunner.test.ts, 57 tests:exec;RegexTaskrunning with the runner installed.It passes under both Vitest and
bun test. After the merge ofmain(61dd0f0), Vitest passes on it,RegexTask.test.tsandTFMP_GenaiHelpers.test.ts(98 tests across the three files).oxlint --type-awareandoxfmt --checkare clean.The CodeQL alerts on the deliberate catastrophic test fixtures were dismissed by the owner.
Related
docs/superpowers/specs/2026-10-18-c01-amendment-cloudflare-server-tier-design.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01LVgexVV91n4PaqB1i5Raag