Browser Pre-filter + Server Verdict: A One-Hour Teardown of a PR Review Bot

A one-hour pairing session splits a PR review bot into client-side pre-filtering, server-side verdicts, and CI merge decisions.
The article recounts a one-hour pairing session to re-architect a PR review bot. The engineers first settled the budget question—"what number makes us stop"—then rejected three approaches: moving the whole bot into the browser (duplicate sources of truth), sending raw diffs to a shared endpoint (lockfiles wasting context and money), and caching verdicts by diff hash alone (silently reusing stale summaries). What survived is a routing table: classification and trimming on the caller's machine, risk summaries on a shared server, a verdict cache keyed on model id + template hash + diff hash, and merge decisions always left to a human in CI. A budget script probes cold/warm latency and projects tokens before the webhook is enabled, exiting non-zero if over budget.
An automated review bot posted risk summaries on every PR for eleven days straight—until a senior reviewer booked a one-hour pairing session to settle a boundary question: which work belongs in the browser, and which must go to the server. In that hour they tried three dead ends and ended up with just one decision—a script and a routing table.

Three questions on the whiteboard
The first ten minutes of the pairing session were spent without opening an editor. Three things went on the whiteboard in order: which bytes in the diff actually need a model? Where does the model run—browser, shared server, or CI? And what number would make this pair stop shipping?
The third question is what changed the architecture. Without it, the first two answers are just preferences.
Dead end one: move the whole bot into the browser
The most obvious approach is to run inference locally in the browser—no server, no cold start, no egress traffic. It looks like free compute.
It failed on state, not speed. The bot needs to know which PRs have already been summarized, and a browser tab can't hold that queue across machines. Reimplementing the queue in IndexedDB amounts to building a second source of truth next to the webhook log. Another finding: the local file classifier had been copied into the browser bundle, so the same rule file now existed in two copies, ready to drift.
What survived: pre-filtering stays on the client, model calls don't. Rules like "this is a lockfile, skip it" are cheap, deterministic, and testable without a network.
IndexedDB is a built-in client-side NoSQL database in browsers. It supports transactions and asynchronous reads/writes, and is typically used for caching or offline storage. It can hold a lot of data (usually bounded by device quota), but its scope is limited to the same browser, the same origin, and the same machine—another tab or device can't see it. So if you put the bot's queue of PRs to summarize in IndexedDB, each developer machine maintains its own copy when several are registered. Once someone switches browser environments, the data is lost or diverges—effectively creating another "source of truth" alongside the original webhook log, requiring extra sync logic to reconcile.
Deterministic pure functions mean the same input always produces the same output, with no dependence on the network, clock, random numbers, or external state. Writing rules like lockfile detection and path skipping as pure functions has a big payoff: you can assert results in local unit tests without mocks, and the filter results produced on the client are exactly the same as on the server, so they won't drift due to environment differences.
Dead end two: send every diff to a shared endpoint
The second attempt simply deleted the classifier and sent raw diffs straight to a hosted endpoint. The first generated lockfile in the sample was 900 lines, and the model wasted context describing dependency upgrades that the regex already knew about.
Trimming has to happen before the request, not after the bill. The author provided a deterministic trimDiff function: split by diff --git, use regex to skip paths like package-lock.json, pnpm-lock.yaml, dist, and vendor, and truncate each file to 120 lines.
The key is not to trust savings percentages from blog posts—measure on your own repo. The author provided a shell snippet that walks the last 30 merges and computes the retained-byte ratio for each. If that ratio is high, pre-filtering isn't working, and splitting later won't help.
Diff trimming must happen before the request because models bill by token, and token count is roughly linear in request size. Lockfiles usually contain thousands of lines of auto-generated content; a regex match on the filename lets you skip the whole block and save most of those bytes. Truncating retained files to a fixed number of lines trades a small amount of context for a controllable budget. The trimming function takes and returns strings, making it a pure function that can be unit-tested, so it can run in the same process as the classification step with no extra network round trip.
Measuring the byte ratio matters because lockfiles and build artifacts vary enormously across repos—the "90% savings" often quoted in blog posts may not hold. Walking the real diffs of the last N merges and dividing retained bytes by original bytes gives you your own repo's model-input ratio. If that ratio is high, the pre-filter rules don't cover the generated-file paths specific to that repo, and you should add regexes rather than keep tuning parameters.
Dead end three: cache verdicts by prompt hash
The third idea was the most tempting and the most dangerous: cache verdicts by a hash of the prompt text so duplicate diffs don't get re-sent. Two weeks later, a merged PR went live with a summary generated from an old template.
The cache key itself was the bug. A verdict is a function of the diff, the prompt template, and the answering model. Hashing only the diff means that when upstream changes an identifier, stale text gets silently reused. The key that finally passed review contains three things: modelId, templateHash, and diffHash, joined with \u0000 and then SHA-256'd.
The conclusion is clear: caching pre-filter decisions is safe. Caching verdicts is only safe when all three inputs are present—and the model identifier must come from config, with no default value.
Cache key design is the first prerequisite for cache correctness. A verdict depends on three independent variables: the diff content that was read, the prompt template sent to the model, and the identifier of the model that performed inference. Hash only the diff, and after a template upgrade or model switch, old verdicts will still be silently hit—so modified content ships with a summary from the old template. Joining the three with an invisible separator (such as \u0000) and then applying SHA-256 avoids field-boundary confusion (e.g., concatenating ab+c and a+bc yields the same string) while keeping the key short enough for cache storage.
The model identifier must come from config with no default value because hosted endpoints quietly change behavior—and even pricing—across versions. If the code hardcodes a default, the cache key won't change when upstream switches models, effectively reusing output from a completely different model. Failing explicitly when config is missing surfaces the problem at startup rather than in production traffic.
The surviving decision: split plus budget
The hour ended with a routing table and a script that refuses to enable the bot when the numbers don't clear the bar.
| Work | Runs where | Why | Failure mode |
|---|---|---|---|
| File classification | Caller's machine | Zero model cost, pure function | Rule file drifts from repo structure |
| Diff trimming | Same process as classification | Deterministic, unit-testable | Chunking cuts off context |
| Risk summary | Shared server + hosted model | Needs a queue and a stable webhook target | Tier latency or quota changes |
| Verdict cache | Server, keyed on three inputs | Avoids duplicate cost | Model id not pinned in config |
| Merge decision | CI, never handed to a model | Humans stay accountable | Never delegate this step |
The companion split-budget.mjs runs before the first real review: it sends two small requests and one representative diff, records cold-start and warm latency, projects tokens from daily review volume, and exits non-zero if any measured number breaks the budget declared in budget.json. The author specifically stresses not to hardcode quotas in config, so a tier-terms change surfaces as tokens_over_budget rather than a silent bill.
exit=0 is permission to enable the webhook; exit=1 names the reason and leaves the bot off. The gate is small enough that the senior reviewer could read every line in the final hour—that's the point.
How the free tier entered this session
(Disclosure: this article was prepared as part of promoting the MonkeyCode product.)
This pairing needed two things a laptop couldn't provide: a hosted model to answer the warm-real probe, and a machine with a public address to receive webhooks during testing. The operator's free tier supplied both—free model access and a free server option—and the project is open source, so the routing code could be read before trusting it with diffs. The operator says the tier includes 10 million tokens, which should be treated as a dated claim rather than a constant. The script therefore reads limits from budget.json and never hardcodes quotas.
Limitations and who should skip this
- Low-volume repos: below roughly 20 PRs a week, the gate and routing table cost more attention than they save. Keep pre-filtering, drop the split.
- Compliance-constrained teams: sending diffs to a shared hosted tier may be outright prohibited. The trimming and classification stages can still work on their own.
- Unknown latency: a shared free endpoint is not suitable as a merge-path dependency. If cold-start behavior can't be measured, make the bot advisory only and never blocking.
- Token estimation: the character-based fallback is labeled
estimatein the ledger for a reason—it drifts on code-heavy input, so prefer provider-reported usage. - The gate measures, it doesn't guarantee: passing means the numbers fit today's config, not that they'll still fit next quarter.
Checklist for the next pairing hour
- Count how many diffs in the last 30 merges actually need a model.
- Pin the model identifier in config and fail loudly when it's missing.
- The verdict cache must be keyed on model id, template hash, and diff hash.
- Run the budget script against the real endpoint before the webhook exists.
- Re-check the ledger whenever tier terms change.
The real value of this split isn't in the code—it's in the order of that one hour: ask "what number makes us stop" first, then decide where the code lives. Most rework when AI features ship happens precisely because that question was left for last.
One-line Summary
A one-hour pairing session split a PR review bot into client-side pre-filtering, server-side verdicts, and CI merge decisions.
Paragraph Summary
The article recounts a one-hour pairing session to re-architect a PR review bot. The two engineers first settled the budget question—"what number makes us stop"—then rejected three approaches in turn: moving the whole bot into the browser (duplicate sources of truth for the queue and rule files), sending raw diffs to a shared endpoint (lockfiles wasting context and money), and caching verdicts by diff hash alone (silently reusing stale summaries after template or model changes). What survived is a routing table: classification and trimming stay on the caller's machine, risk summaries run on a shared server, the verdict cache is keyed on a hash of model id + template hash + diff hash, and merge decisions always go to a human in CI. A companion budget script probes cold/warm latency and projects tokens before the webhook is enabled, exiting non-zero if over budget. The author also notes that low-volume repos and compliance-constrained teams should skip the split, and stresses measuring your own repo's trimming savings rather than quoting blog numbers.
Related articles

Understanding DeepSeek in 3 Minutes: The Rise from a Quant Hedge Fund to an Open-Source AI Leader
DeepSeek's history in 3 minutes: from High-Flyer Quant's compute build-up to V2's MLA inference cost cuts, V3's $5.576M training bill, R1's open-source global shock, and V4 with a $7B Series A.

Local Creative Writing Model Showdown: Fine-tuned Qwen3.8 27B Wins, DeepSeek v4.1 Underwhelms
Bilibili creator tests seven local creative writing models: Hemingway and HumanLike Chat based on Qwen3.8 27B perform best, DeepSeek v4.1 is plain, Dongfang Shuo ranks last with confused characters. Includes three test prompts, scores, and usage advice.

Liang Wenfeng's Three Big Bets: Why Did DeepSeek Open-Source Its 82% Gross Margin?
Why did DeepSeek open-source DeepInfer, its 82% gross margin inference framework, and then launch a hundred-billion funding round? An analysis of Liang Wenfeng's three big bets and the open-source strategy.