fix(recall): resolve 7 knowledge retrieval bugs (#79-#85) - #91
Conversation
…ut, dead code - Add `teamai import --dir <path>` to README command tables (EN + CN) - Fix callClaude JSDoc: timeout default is 600000 (10min), not 120000 - Remove redundant dynamic import of autoPushTeamRepo (already static) - Replace dead 'workspace' union member with 'dir' in ImportSession.mode
jeff-r2026
left a comment
There was a problem hiding this comment.
Automated review pass (medium effort). Three items — one functional contradiction with #79, plus two ranking-consistency issues in the new codebase tokenizer path. Left inline; none are blocking.
| if (entry.snippet) { | ||
| const maxLen = entry.snippet.length > 500 ? 500 : entry.snippet.length; | ||
| const snippet = entry.snippet.slice(0, maxLen); | ||
| lines.push(`Snippet: ${snippet}${entry.snippet.length > maxLen ? '...' : ''}`); | ||
| } |
There was a problem hiding this comment.
--depth lookup output is silently truncated here. This 500-char cap applies to every snippet, including codebase results. But queryCodeKnowledge gives lookup a 20000-token budget and slices up to ~16k chars per page (code-knowledge-recall.ts:300), so the #79 behavior ("full content per result") never reaches the user — all depths render ≤500 chars. Either make the cap depth-aware (raise/skip it for lookup), or drop the per-page budget in queryCodeKnowledge if 500 chars is the intended ceiling; right now the two disagree.
Separately, the maxLen juggling is redundant and can be simplified:
| if (entry.snippet) { | |
| const maxLen = entry.snippet.length > 500 ? 500 : entry.snippet.length; | |
| const snippet = entry.snippet.slice(0, maxLen); | |
| lines.push(`Snippet: ${snippet}${entry.snippet.length > maxLen ? '...' : ''}`); | |
| } | |
| if (entry.snippet) { | |
| const snippet = entry.snippet.slice(0, 500); | |
| lines.push(`Snippet: ${snippet}${entry.snippet.length > 500 ? '...' : ''}`); | |
| } |
| path: `evidence/code/${relativePath}`, | ||
| title, | ||
| content, | ||
| uniqueTokens: new Set(fastTokens(content)), |
There was a problem hiding this comment.
Docs and queries are tokenized by two different tokenizers. Pages are indexed with fastTokens (regex split) here, which builds df, but the query is tokenized with the shared tokenize (Intl.Segmenter + camelCase) at line 267. A query term like getuserid (Segmenter keeps it whole) is absent from df — docs only stored get/user/id — so df.get(token)=0 yields max idf, while countOccurrences still matches it in raw content (tf>0). Net effect: whole-word identifiers and CJK whole-words get inflated idf and skewed BM25 ranking.
This also cuts against the PR's stated goal of a unified tokenizer — for this module it introduces a second, divergent one. If fastTokens exists only to dodge the Segmenter OOM, prefer having the shared tokenize produce the same token surface (it already truncates to 100K chars) so index-time and query-time terms line up.
| function fastTokens(text: string): string[] { | ||
| const prepared = text.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase(); | ||
| const words = prepared.split(/[^a-z0-9一-鿿]+/).filter((w) => w.length >= 2); | ||
| const tokens: string[] = words.slice(0, 5000); |
There was a problem hiding this comment.
fastTokens caps at 5000 words, but rawTokenCount (used as BM25 dl at line 97) counts the whole document. For an evidence/code page over 5000 words, df/uniqueTokens only reflect the first 5000 words while length-normalization uses the full dl — query terms in the tail are missing from df (idf inflated, same failure mode as the tokenizer mismatch), and dl vs the token set are computed inconsistently. If the cap is just an OOM safeguard, apply the same truncation to rawTokenCount (or drop the cap) so the two stay in sync.
Tencent#79 — --depth parameter now produces visibly different output: - route (1500 token budget): title + path only - context (5000, default): 300-char snippet - lookup (20000): full content per result - Snippet field shown in formatResults (up to 500 chars) Tencent#80 — codebase score normalization: replace min-max (always=10) with log-dampening: Math.min(10, Math.log2(score+1) * 2) Tencent#81 — hasWiki path: use resolved wikiRoot instead of cwd/teamwiki Tencent#82 — buildConfidence: change from arithmetic mean (dilutes with more evidence) to max(weights) (strongest evidence determines confidence) Tencent#83 — search-index shrink protection: relax from 50% threshold to only guard against empty index overwriting non-empty Tencent#84 — codebase graph retrieval: - 84-1: loadWikiPages now recurses into subdirectories via collectMdFiles - 84-2/4: unified tokenizer (src/utils/tokenizer.ts) shared by search-index and code-knowledge-recall; adds camelCase splitting; truncates input to 100K chars to prevent Intl.Segmenter OOM - OOM fix: PageDoc.tokens replaced with uniqueTokens Set built via fastTokens (regex-only, no Segmenter) to avoid 216-file memory spike Tencent#85-2 — auto-recall upvote: use autoDetectInit() instead of requireInit() so votes go to the correct scope Additional: - SEARCH_INDEX_VERSION bumped 4→5 (triggers rebuild for camelCase tokenizer) - README --dir entry added (EN + CN) - callClaude JSDoc timeout updated to 600000 - Redundant dynamic import of autoPushTeamRepo removed Closes Tencent#79, Tencent#80, Tencent#81, Tencent#82, Tencent#83, Tencent#84, Tencent#85
a53e514 to
020a453
Compare
#73/#77 fixed recall's dual-scope merge and #91 fixed auto-recall's upvote scope, but issue #85 flagged four more spots where project vs user scope still leaked into each other: - hooks-cmd.ts: `hooks inject`/`hooks remove` reconciled the user-home copy of team hooks using the PROJECT's manifest instead of the user's own, diverging from pull.ts's per-scope reconcile and risking duplicate injection / wrongful cleanup of shared tool settings files. - tags.ts: subscribe/unsubscribe (and list/add/remove) always read and wrote ~/.teamai/config.yaml, ignoring project-scope installs. - contribute.ts: wrote new learnings to the team repo but never rebuilt the local search index, so `recall` couldn't see a contribution until the next `pull`. - types.ts/config.ts: getTeamaiHome()/resolveBaseDir() silently fell back to the user home directory when a project LocalConfig was missing `projectRoot` (e.g. a pre-migration config.yaml). Now detectProjectConfig()/loadLocalConfigForScope() backfill projectRoot from the directory the config was actually loaded from, and the scope helpers throw instead of silently degrading if it's ever still missing. Added an e2e suite (scope-isolation-e2e-issue85.test.ts) that drives the built CLI against real git fixtures to cover all four fixes end-to-end, plus unit coverage in hooks-cmd.test.ts/scope.test.ts. Closes #85 Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Summary
Fixes 7 recall/knowledge-graph bugs reported in #79-#85, plus reviewer cleanup.
#79 —
--depthparameter now effectiveSnippet field added to
formatResultsoutput — no truncation applied (length controlled by depth budget inqueryCodeKnowledge).#80 — codebase score normalization
Replace min-max (top result always = 10) with log-dampening:
Math.min(10, Math.log2(score + 1) * 2)— preserves relative ordering without pinning.#81 —
hasWikipathUse resolved
wikiRoot(from team-repo config) instead of hardcodedcwd/teamwiki.#82 —
buildConfidencesemanticsChange from arithmetic mean (more evidence → lower score) to
max(weights)— strongest evidence determines confidence. This is a deliberate design change: multiple supporting factors should not dilute confidence.#83 — search-index shrink protection
Relax from "skip if new < 50% of old" to "skip only if new is empty". Allows legitimate file deletions to be reflected in the index.
#84 — codebase graph retrieval
loadWikiPagesnow recurses into subdirectories viacollectMdFilessrc/utils/tokenizer.ts) shared by search-index and code-knowledge-recall. Adds camelCase splitting to search-index. Truncates input to 100K chars to preventIntl.SegmenterOOM.PageDoc.tokensreplaced withuniqueTokens: Set<string>built via the sharedtokenizefunction (same tokenizer for index-time and query-time)#85 — auto-recall upvote scope
Use
autoDetectInit()instead ofrequireInit()so votes are written to the correct scope (project or user).Additional cleanup
SEARCH_INDEX_VERSIONbumped 4→5 (triggers rebuild for camelCase tokenizer change)--direntry added (EN + CN)callClaudeJSDoc timeout corrected to 600000autoPushTeamReporemovedReview feedback addressed
formatResultstruncation — snippet length now fully controlled by depth budgetfastTokensregex) vs query-time (tokenizeSegmenter) produce different tokens → skewed BM25fastTokens; index-time now uses the same sharedtokenizeas query-timerawTokenCountcounts full doc while token Set was capped at 5000 → inconsistent dlrawTokenCountnow truncates to 100K chars (same limit astokenize)E2E verification
Test plan
npx tsc --noEmit— zero errorsnpx vitest run— 1676 passed (130 files)Closes #79, #80, #81, #82, #83, #84, #85