The transcript miner stores some references under a wrong owner: nacme/service#4242 instead of acme/service#4242. Found 2026-09-15 while checking whether issue URLs are indexed (they are, see #157); the mangled entries turned up in the cache itself.
Owner, repo and issue numbers in this issue are placeholders. The real ones come from a private work repository and are deliberately not reproduced here; the counts below are real.
Cause
assistantTextOfLine takes a tool call's input with JSON.stringify(b.input) (src/enrichment-cache.ts), and JSON.stringify re-encodes a newline inside a string value as the two characters \ and n. So a tool input that reads
…some text
acme/service#4242…
reaches the scanner as …some text\nacme/service#4242… with a literal n sitting immediately before the owner.
PR_REF_RE has three branches, and only the second one has no left boundary:
| branch |
form |
left boundary |
| 1 |
github.com/o/r/pull|issues/N |
(?<=^|:\/\/|[^a-z0-9_./-]) ✅ |
| 2 |
owner/repo#N |
none ❌ |
| 3 |
bare #N |
(?:^|[^A-Za-z0-9&#]) ✅ |
[A-Za-z0-9-]+ is greedy and starts one character early, so the n becomes part of the owner.
This is a two-implementations-of-one-rule drift: the query side already gets it right. prRefPatterns' hash pattern in src/session-search.ts is (?:^|[^0-9a-z_./-])((?:${REPO})?)#${n}(?![0-9a-z_]), and its comment even spells out the case ("so foo/bar#12 captures foo/bar, not o/bar"). The miner's own comment block claims both sides apply the same boundaries, which is currently false.
What it costs, measured on the live cache (2026-09-15)
~/.config/codev/enrichment-cache.json, version 2, 134 sessions:
|
|
| references stored |
6378 |
| stored with a junk owner |
139 (2.2%), all of them an n glued on the front |
| sessions affected |
32 of 134 |
| references whose clean form is absent from the same session |
5 |
The last row is the real damage. A repo-qualified query builds a pattern with a left boundary, so it cannot match a stored nacme/service#4242, and for those five the mangled string is the only form that session carries, so the session is unfindable by its correct qualified form. Bare #N and pr:N are unaffected: #N is repo-agnostic, and pr:N only needs some qualified mention, which a mangled one still is.
The junk prefix was n in 139 of 140 cases, across two unrelated owners, so the class is general rather than tied to any one repository.
Fix
Add the same left boundary to branch 2, mirroring the matcher:
(?<![A-Za-z0-9_./-])([A-Za-z0-9-]+\/[A-Za-z0-9_.-]+)#([1-9][0-9]*)(?![A-Za-z0-9_])
Then bump ENRICHMENT_CACHE_VERSION from 2 to 3 so the already-stored mangled entries are dropped and re-mined (readEnrichmentCacheFile discards the file on a version mismatch). A full re-mine of the corpus was measured at ~2.0s once, in the background pass, so the one-off cost is not user-visible.
Test to add: a fixture whose assistant record carries a tool input with a newline before an owner/repo#N mention, i.e. the real JSON.stringify shape rather than a hand-written string, asserting the owner comes out clean. The same fixture guards the general lesson: anything scanned after JSON.stringify sees escape sequences as literal letters, not whitespace.
Severity: Minor. It silently narrows the index rather than breaking anything visible, which is why it went unnoticed since #151.
🤖 On behalf of @grimmerk — generated with Claude Code
The transcript miner stores some references under a wrong owner:
nacme/service#4242instead ofacme/service#4242. Found 2026-09-15 while checking whether issue URLs are indexed (they are, see #157); the mangled entries turned up in the cache itself.Cause
assistantTextOfLinetakes a tool call's input withJSON.stringify(b.input)(src/enrichment-cache.ts), andJSON.stringifyre-encodes a newline inside a string value as the two characters\andn. So a tool input that readsreaches the scanner as
…some text\nacme/service#4242…with a literalnsitting immediately before the owner.PR_REF_REhas three branches, and only the second one has no left boundary:github.com/o/r/pull|issues/N(?<=^|:\/\/|[^a-z0-9_./-])✅owner/repo#N#N(?:^|[^A-Za-z0-9&#])✅[A-Za-z0-9-]+is greedy and starts one character early, so thenbecomes part of the owner.This is a two-implementations-of-one-rule drift: the query side already gets it right.
prRefPatterns'hashpattern insrc/session-search.tsis(?:^|[^0-9a-z_./-])((?:${REPO})?)#${n}(?![0-9a-z_]), and its comment even spells out the case ("sofoo/bar#12capturesfoo/bar, noto/bar"). The miner's own comment block claims both sides apply the same boundaries, which is currently false.What it costs, measured on the live cache (2026-09-15)
~/.config/codev/enrichment-cache.json, version 2, 134 sessions:nglued on the frontThe last row is the real damage. A repo-qualified query builds a pattern with a left boundary, so it cannot match a stored
nacme/service#4242, and for those five the mangled string is the only form that session carries, so the session is unfindable by its correct qualified form. Bare#Nandpr:Nare unaffected:#Nis repo-agnostic, andpr:Nonly needs some qualified mention, which a mangled one still is.The junk prefix was
nin 139 of 140 cases, across two unrelated owners, so the class is general rather than tied to any one repository.Fix
Add the same left boundary to branch 2, mirroring the matcher:
Then bump
ENRICHMENT_CACHE_VERSIONfrom 2 to 3 so the already-stored mangled entries are dropped and re-mined (readEnrichmentCacheFilediscards the file on a version mismatch). A full re-mine of the corpus was measured at ~2.0s once, in the background pass, so the one-off cost is not user-visible.Test to add: a fixture whose assistant record carries a tool input with a newline before an
owner/repo#Nmention, i.e. the realJSON.stringifyshape rather than a hand-written string, asserting the owner comes out clean. The same fixture guards the general lesson: anything scanned afterJSON.stringifysees escape sequences as literal letters, not whitespace.Severity: Minor. It silently narrows the index rather than breaking anything visible, which is why it went unnoticed since #151.
🤖 On behalf of @grimmerk — generated with Claude Code