feat(hooks): gate junk queries out of the WebSearch MISS ledger - #179
feat(hooks): gate junk queries out of the WebSearch MISS ledger#179vraspar wants to merge 1 commit into
Conversation
A recorded MISS is a local obligation: the Stop hook nags the agent to publish it back or close it. Most web searches are not marketplace questions, so the ledger filled with demand nobody has. A MISS whose query matches one of four narrow patterns is no longer written to searches.json. It is still POSTed to /api/agent/search, so the server row survives for demand telemetry. A CANDIDATES response is always recorded and always hinted, whatever the patterns say. Hook script version 18, so `tenjin install` rewrites the scripts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Your trial has ended. Reactivate Greptile to resume code reviews.
A1igator
left a comment
There was a problem hiding this comment.
Review: the gate's shape is right, pattern 1 is not, and the base is behind a refactor of the line it patches
Reviewed against main at 014932a, verified at fe93a05. Findings below were run against the head checkout and the regexes were executed rather than read.
What's solid:
- The asymmetry is the correct one and it is stated where it belongs: an unsure query is a real query, because a suppressed question costs the finding it would have become and a missed junk line costs one row in a batch the agent already skims.
- The CANDIDATES override, and pinning it as owner-locked. If the marketplace surfaced answers, the question was marketplace-shaped, and that outranks any syntactic guess.
- Rejecting the operator-soup rule, with the flagged examples shown rather than described. Three durable questions out of that rule's own output is the kind of evidence that settles a design argument, and the variant sweep showing the whole family sits in the same band is what makes the rejection stick rather than read as taste.
- Leaving the POST alone, so the server row still reaches tenjin#704's classifier and only the local obligation is dropped.
Major
-
[correctness] pattern 1 matches platform names on their own, which is where durable integration questions live: the first alternation flags
reddit,discord,youtube,linkedin,instagram,podcastandhacker newsas bare tokens (hook-scripts.ts:98-104). Those are not junk signals, they are the most-searched API surfaces on the web. I ran the four shipped regexes over eleven realistic technical queries and all eleven were suppressed, includingdiscord.js v14 gateway intents GUILD_MEMBERS privileged 4014 disallowed intent,YouTube Data API v3 search.list quota cost 100 units per call,LinkedIn OAuth r_liteprofile deprecated replacement openid profile scope migrationandReddit API pricing 2023 rate limit 100 QPM per OAuth client id. Every one is a version-specific integration gotcha, which is in-scope demand by thetenjin-searchskill's own definition, and each would be silently unrecorded: no entry, no nag, no--search-idto report against, and nothing on the machine showing it happened. The must-not-flag set is seven queries and not one of them names a platform, so this class is unpinned in both directions. What makes the fix cheap: of your six must-flag cases only two depend on a bare platform token, so requiring the platform to co-occur with an artifact or discussion noun (thread,post,comments,AMA,video) keeps four of them untouched. The junk in your examples is the artifact, not the platform. -
[testing]
awesome-[a-z-]+\bnever matches a list name containing a digit, and no test would notice:[a-z-]+cannot consume the digit and the trailing\bthen cannot be satisfied, soawesome-x402,awesome-web3andawesome-vue2all fail whileawesome-selfhostedpasses (hook-scripts.ts:101). Your own must-flag casex402 protocol Discord Telegram community awesome-x402 list buildersis therefore caught by the barediscordtoken, not by the curated-list rule it looks written for, which is why deleting that rule entirely would leave all fourteen tests green. The revert check described in the PR body drops the gate as a whole, so it establishes that the gate bites but not that any individual pattern does. Fix:awesome-[a-z0-9-]+, and revert each pattern separately so every one of the four is known to be load-bearing for at least one case. -
[hygiene] the base predates the refactor of the exact line this patches, and re-applying it is a decision rather than a merge: the merge base is
0c92f5band GitHub reports the PR CONFLICTING. Since then #180 landed and movedrecordSearchout of the WebSearch hook'smain()into the sharedmarketplaceSource, split the body intoaskTenjin/hintLines, added a dispatch hook that records through the same function, and already tookHOOK_SCRIPT_VERSIONto 18, so this PR's 18 has to become 19 and the ordering paragraph's "keeps the v17 record-everything behavior" is stale. The part that is not mechanical: oncerecordSearchis shared, "in the generated WebSearch hook only" stops being a location and becomes a choice. The obligation rationale genuinely does not apply to the dispatch arm, since the Stop hook never raises those, but that arm is also the one nothing ever drains, so junk there sits in the demand budget until evicted. Whichever way you take it, say so at the gate, because the next reader will find one call site and two callers.
Minor
-
[correctness]
color paletteandicon setare the same class one size down:accessible color palette WCAG 2.2 contrast ratio 4.5:1andmatplotlib colorblind safe color palette tab10 vs viridisboth flag on pattern 2, andlucide icon set tree shaking bundle size viteflags on pattern 4. Design vocabulary and front-end engineering vocabulary overlap heavily, and these two tokens sit in the overlap. -
[agent-usability] a suppressed query leaves no trace anywhere on the machine: no entry, no counter, no line, and the searchId the server minted is unreachable locally, so a false positive is undiscoverable after the fact and unreportable even once noticed. Given the whole design rests on the claim that false positives are rare, the cheapest way to keep that claim honest is to make them countable: either record the entry with a marker that the Stop hook and
outcome --all-openboth skip, or keep a counter besidehook-nags.json.
Nits (2), none blocking
- [hygiene] the 6.2% is one machine's corpus: 1094 queries from one developer's transcripts bounds what the rate can tell you, and the population most at risk from pattern 1 is whoever integrates against those platforms. Worth stating as a limit on the number rather than leaving it to be read as a general rate.
- [hygiene] no lever for someone the gate is wrong about:
hooks.searchMode offdisarms the whole hook, and the gate only changes on a re-install because it is baked into the script. A user who hits a false positive has no smaller move available.
Verified, not issues
- The regexes serialize into the generated script cleanly:
toString()preserves theiflag and none of the four sources contains a backtick or${, so nothing escapes the template. - The gate runs after the response boundary and only on
MISS, so a validation failure still exits quiet and a HIT is untouched, which is what the override test pins. - The local ledger and the server row are deliberately allowed to diverge, and the PR says so rather than implying the query is dropped.
Heads up on merge order, not a review finding: #177 and #113 both still touch hook-scripts.ts, and #177 rewrites the Stop hook's source branch that decides what a recorded MISS turns into.
Verdict: comments-only. The gate's architecture is the right one and the rejected-rule analysis is the strongest thing in the PR, but pattern 1 as written suppresses the exact question class the marketplace exists to collect, and the test set cannot see it. Ran locally: head checkout, the four regexes executed against eleven constructed technical queries and against your six must-flag cases with the platform tokens removed, plus the awesome- digit case.
|
Closing unmerged. @A1igator's review is why, and both majors were verified against head rather than taken on faith. The eleven-of-eleven result is the finding that ends this PR, not just pattern 1. Once bare platform tokens go, and The deeper point the review surfaced: a regex over query text is a syntactic guess at a semantic property. Both rule families we tried failed in the same direction, toward suppressing precise technical questions, which is the one class the ledger exists to collect. Two corrections to the record on the way out:
All of it now lives on BackTrackCo/tenjin#704: the operator-soup rejection with its examples, the variant sweep showing the family sits in one band, the eleven suppressed integration queries, and the argument that query-shape rules belong in a scored classifier as an input, never as a boolean that drops a row. The server side is where that 1.5% costs a column instead of a release. Branch left in place for the sweep data. Thanks for running the regexes instead of reading them; that is what made this decidable. |
The CLI-side v0 of the junk gate in BackTrackCo/tenjin#704. The server-side eligibility column stays that issue's scope; this is only the half that decides what lands in the local ledger.
The problem
The
PreToolUse/WebSearchhook records every query it sees into~/.tenjin/searches.json. A recorded MISS is not a note, it is an obligation: the Stop hook nags the agent to publish it back or close it, and the server row feeds/trending's questions tier. Most web searches are not marketplace questions at all (Cluely launch video who made it,land-book.com website inspiration 2026), so the ledger fills with fake demand and the nag loses its meaning.The change
In the generated WebSearch hook only:
recordSearchis skipped entirely: no local entry, therefore no obligation and no Stop nag./api/agent/searchis unchanged, so the server row still exists for demand telemetry and for tenjin#704's server-side classifier.HOOK_SCRIPT_VERSION17 to 18.tenjin config set hooks.searchMode offalready disarms the whole hook.The measurement
Against 1094 unique real WebSearch queries extracted from this machine's transcripts (1617
.jsonlfiles under~/.claude/projects), the four shipped patterns flag 68 queries, 6.2%.Deliberately rejected: the "operator soup" pattern from tenjin#704's handoff comment (bare
OR, quoted fragments,site:). Reconstructed as1+ bare OR | 2+ quoted fragments | site:(the narrowest variant that still flags all three false positives cited in the handoff), it flags 124 of the same 1094, 11.3% — and the flagged set is full of genuine durable questions:iCloud Private Relay Sec-Fetch headers stripped OR service worker Sec-Fetch-Dest empty prefetch Sec-Purpose"dvh" caniuse baseline "widely available" 2023 date OR "2024" OR "2025"execCommand copy "not allowed" OR "SecurityError" async setTimeout requires user gestureEvery variant of that rule sits in the same band:
bare ORalone flags 6.4%,2+ quoted7.9%,2+OR | 2+quoted | site:9.6%. The handoff's original run measured 10%; my reconstruction lands at 11.3% on a corpus that has grown since. The conclusion is not sensitive to which variant you pick — the rule is syntactic, and technical questions are the ones that use search operators.Also rejected: a bare
best ... 20XXrule, because maintained comparisons are in-scope demand per thetenjin-searchskill.best article extraction API 2026 Firecrawl Jina Reader Exaandllms.txt best practices 2026 examplesare real questions, and both survive the shipped patterns.The asymmetry is deliberate and conservative-first: when unsure, the query is real. Missing junk costs one stale line in a batch the agent already skims; suppressing a real question costs the finding it would have become.
Tests
14 new cases in
src/lib/hook-scripts.test.ts, all executing the real generated script as a child process in the existing style. Both directions are pinned with real queries out of the transcripts.hits() === 1so the POST still happened, exit 0 and silent.source: 'websearch-hook'.Each gate was reverted separately to prove the tests bite: dropping the junk gate fails exactly the 6 must-flag cases; extending it to CANDIDATES fails exactly the override case.
Ordering
Buyers need to re-run
tenjin installto pick up hook script v18.tenjin updatereplaces the binary and nothing else, and the self-heal does not reach hook scripts, so an existing install keeps the v17 record-everything behavior until someone re-runsinstall.Checks
pnpm typecheck,pnpm lint,pnpm format:check,pnpm buildall clean.pnpm test: 2152 passed, 10 skipped, 2 failed (2164). The 2 failures are pre-existing onorigin/main—doctor.test.ts"names the rule on auto" / "on full-auto" fail identically at0c92f5bwith none of this branch's changes. Baseline atorigin/mainis 2138 passed / 10 skipped / 2 failed (2150), so this branch is exactly +14 passed with skips and file counts unchanged.pack-smokeruns outside vitest and was not run locally (it rewrites~/.claude/skillson the host), so a green local suite is not a green CI here — CI is the check that matters for it.Comment density on added lines: 12/81, 14.8%.
🤖 Generated with Claude Code