Skip to content

perf(pii): replace resident RegexSet prefilter with byte-oriented candidate scan#119

Merged
senamakel merged 2 commits into
mainfrom
perf/pii-byte-prefilter
Jul 22, 2026
Merged

perf(pii): replace resident RegexSet prefilter with byte-oriented candidate scan#119
senamakel merged 2 commits into
mainfrom
perf/pii-byte-prefilter

Conversation

@senamakel

Copy link
Copy Markdown
Member

Why

OpenHuman resource profiling (openhuman docs/resource-profiling-session-2026-07-21.md) identified pii::SCREEN — the always-resident 17-pattern multilingual RegexSet prefilter — as the dominant live Rust heap allocation family during normal memory capture: the combined NFA plus per-thread hybrid-DFA caches. The target deployment runs 100-1000 concurrent agents, so the per-thread caches multiply with thread count.

What

Replaces SCREEN with scan_candidates(): a single linear byte pass producing per-class boolean flags (digit-run stats, separator presence, NANP digit-sep-digit window, case-insensitive keyword probes including the non-Latin needles). Each flag gates whether that class's precise LazyLock<Regex> is compiled and run, so:

  • the combined NFA and its per-thread lazy-DFA caches are gone entirely;
  • a workload only ever compiles the precise regexes for PII classes it actually encounters;
  • redact_pii / has_likely_pii public API and semantics are unchanged (identical hits, priority, dedup order), including the documented "bare 10-digit NANP is never reached" behavior.

Correctness

Every flag is a necessary condition of its class's precise regex, so the prefilter is a strict superset of the old screen: false positives possible (they just run a precise regex that then rejects), false negatives impossible. Pinned by prefilter_is_superset_of_legacy_screen, which rebuilds the old RegexSet at test time as an oracle over a mixed corpus.

  • Before: memory::store::safety 60 passed / 0 failed
  • After: 66 passed / 0 failed; full crate suite 1225 passed / 0 failed
  • 6 new tests incl. Devanagari Aadhaar and JP My-Number keyword coverage

Measured (via the openhuman library-profile benchmark harness)

  • memory-ingest and subagents scenarios: at par with baselines (within noise) — the single-process mock workloads have low PII density, so no regression on the common path.
  • dhat heap profile of memory-ingest: old vs new essentially identical (Total 14.83 MB, t-gmax 1.12 MiB, t-end 0.44 MiB).
  • The win is architectural: the resident, thread-scaling automaton allocation is eliminated; it pays off at high agent/thread concurrency.

Verbose [pii] debug logging added on the scan paths; matched text is never logged.

…filter

Drop the always-resident combined SCREEN RegexSet (shared NFA + per-thread
lazy-DFA caches) in favor of a single cheap byte pass that derives per-class
candidate flags. Precise validation regexes stay LazyLock and now compile only
when their class is actually flagged, so a workload only ever pays for the PII
classes it sees. Each flag is a necessary condition of its precise regex, so
redact_pii / has_likely_pii semantics are unchanged; the union is a superset of
the old SCREEN set (pinned by prefilter_is_superset_of_legacy_screen).

Adds per-class scan tests (incl. Devanagari/Japanese keywords), clean-text
negatives, the bare-10-digit NANP guard, and the legacy-SCREEN parity oracle.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b354964d-10ea-4c87-b005-e6c30ae43209

📥 Commits

Reviewing files that changed from the base of the PR and between 9a0603a and 33e8ee7.

📒 Files selected for processing (4)
  • src/memory/store/safety/pii.rs
  • src/memory/store/safety/pii/normalize.rs
  • src/memory/store/safety/pii/prefilter.rs
  • src/memory/store/safety/pii_tests.rs

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b289c732e3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/memory/store/safety/pii.rs Outdated
// E.164 `\+\d{7,15}` — a `+` and a 7+ digit run.
phone_e164: has_plus && max_digit_run >= 7,
// NANP — screen-entry necessary condition (internal separator).
phone_nanp: nanp_sep,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore redaction for bare country-code NANP numbers

When content contains a separator-less country-code NANP number such as 12025551234, the old SCREEN matched the \d{11,} entry and then PHONE_NANP_RE redacted it; this new flag stays false because nanp_sep only records an internal separator, so collect_redactions_inner skips the phone regex and leaves the number unredacted. Keep this gate broad enough for the legacy long-digit SCREEN path while still excluding the documented bare 10-digit case.

Useful? React with 👍 / 👎.

Comment thread src/memory/store/safety/pii.rs Outdated
Comment on lines +234 to +235
b"mynumber",
b"my number",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recognize all My Number keyword whitespace

When the English keyword uses whitespace accepted by the precise regex, e.g. My\tNumber 123456789012 or a line break between the words, this keyword list does not set mynumber, so the My Number regex is never run and the 12 digits remain unredacted. The previous gate used My\s?Number, so the candidate scan needs to preserve that whitespace variant rather than only mynumber and my number.

Useful? React with 👍 / 👎.

Comment thread src/memory/store/safety/pii.rs Outdated
b'.' => has_dot = true,
b'-' => has_dash = true,
b'/' => has_slash = true,
b' ' | b'\t' => has_space = true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat regex whitespace as Aadhaar separators

When a formatted Aadhaar uses a newline or other \s separator, such as 2341\n2341\n2346, the legacy screen and AADHAAR_FMT_RE both match, but this scan only marks literal space/tab as has_space; without a dash and with only four-digit runs, aadhaar_fmt remains false and redaction is skipped. The candidate condition needs to cover the same whitespace class as the precise pattern.

Useful? React with 👍 / 👎.

Comment thread src/memory/store/safety/pii.rs Outdated
iban: bool,
cc: bool,
cnpj_bare: bool,
cpf_bare: bool,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Split the oversized PII module

This adds the byte prefilter directly to pii.rs, taking the implementation from 495 lines to 762 lines; the repository guideline asks source files stay under 500 lines and be split into focused modules before they grow past that point. Moving the candidate scan/types into a focused sibling module would keep this change within the documented structure.

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

…odule

The byte prefilter dropped three PII cases the old resident RegexSet redacted,
because its candidate gates were narrower than the precise regexes they guard:

- Bare country-code NANP (`12025551234`): old SCREEN reached PHONE_NANP_RE via
  the `\d{11,}` run. The new `phone_nanp` gate keyed only on an internal
  separator, and the bare-CPF fallback's checksum rejects it — so it was left
  unredacted. Gate now also fires on `max_digit_run >= 11` (a bare 10-digit run
  still stays out of the phone path).
- English `My\s?Number` with a tab/newline separator: the keyword needle scan
  only matched a literal-space `my number`. Now gates on both `my` and `number`
  substrings, covering every `\s` variant.
- Newline-separated Aadhaar (`2341\n2341\n2346`): `has_space` tracked only
  space/tab, but the precise `[\s-]` separator class includes `\n`/`\r`/etc.
  Renamed to `has_ws` covering all ASCII whitespace.

Also splits the oversized `pii.rs` (762 -> 475 lines) into focused siblings
`pii/prefilter.rs` and `pii/normalize.rs`, per the <500-line repo guideline.

Adds regression tests for all three cases and extends the
`prefilter_is_superset_of_legacy_screen` corpus with the whitespace variants.

Addresses Codex review comments on PR #119.
@senamakel

Copy link
Copy Markdown
Member Author

Pushed 33e8ee7 addressing the Codex review comments.

All three prefilter correctness flags were confirmed real regressions (verified empirically — the number was left unredacted under the new prefilter but redacted by the old resident RegexSet):

  • P1 — bare country-code NANP (12025551234): the phone_nanp gate keyed only on an internal separator; the bare-CPF fallback's checksum rejects the number, so it was dropped. Gate now also fires on max_digit_run >= 11 (the old SCREEN reached PHONE_NANP_RE via its \d{11,} entry). A bare 10-digit run still stays out of the phone path.
  • P2 — My Number whitespace (My\tNumber …, newline, etc.): keyword scan only matched a literal-space my number. Now gates on both my and number substrings, covering every \s variant the precise My\s?Number accepts.
  • P2 — Aadhaar \s separators (2341\n2341\n2346): has_space tracked only space/tab; renamed to has_ws covering all ASCII whitespace, matching the precise [\s-] class.
  • P2 — oversized module: pii.rs split 762 → 475 lines into focused siblings pii/prefilter.rs + pii/normalize.rs, under the <500-line guideline.

Regression tests added for all three cases, and the prefilter_is_superset_of_legacy_screen parity corpus now includes the whitespace/country-code variants so this class is caught in future.


Heads-up on the red Rust SDK check — pre-existing, unrelated to this PR. It fails at the Documentation step on a broken intra-doc link in src/memory/persona/retrieve.rs:3 ([\DigestObservation`]), introduced by #77. main's own CI run for that commit (run 29332314788) fails with the identical error, and this PR touches only pii.rs. A rerun won't help (deterministic rustdoc error, not a flake). Every other check is green; this PR's own fmt / clippy / test / doc surface is clean. Flagging for a maintainer — it's a one-line fix best made on main`.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 33e8ee7951

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
}

if matches!(b, b' ' | b'\t' | b'.' | b'-') && i > 0 && i + 1 < bytes.len() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept regex whitespace in NANP prefilter

When a formatted NANP number uses another \s separator accepted by both the legacy screen and PHONE_NANP_RE, such as 415\n555\n0123, this gate never sets nanp_sep; the digit runs are only 3/3/4, so max_digit_run >= 11 is false as well and the phone regex is skipped, leaving the number unredacted. Include the same whitespace class here before deciding phone_nanp.

Useful? React with 👍 / 👎.

Comment on lines +31 to +32
#[derive(Default, Clone, Copy)]
pub(super) struct Candidates {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Move the new candidate type to types.rs

This introduces the new Candidates type directly in prefilter.rs, but the repository convention requires module type definitions to live in a dedicated types.rs; keeping the type in the implementation module makes the new split inconsistent with the documented module layout. Please move the type definition into an appropriate types.rs and import it from the prefilter implementation.

AGENTS.md reference: AGENTS.md:L17-L17

Useful? React with 👍 / 👎.

@senamakel
senamakel merged commit bb7911f into main Jul 22, 2026
6 of 7 checks passed
senamakel added a commit that referenced this pull request Jul 22, 2026
Introduced in #77; fails the Rust SDK Documentation CI step on main and
on every PR branched from it (e.g. #119).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant