Repository navigation
[feat] upvote: collect adoption without relying on AI self-declaration (follow-up to #369) #723
Description
Problem
Follow-up to #369 (closed by #371 "make upvote collection reliable for learning promotion", plus the #614 nudge-loop fix). #371 made the referenced-doc-ids nudge fire on every Stop instead of once per session and loosened the parser. Re-measured on current main: upvoted_count is still structurally zero in practice. #371 did not move the needle.
Evidence from a real team repo's votes/*.yaml (git provider):
- 1221 vote records total; 1083 have
recalled_count > 0(max 68 for a single doc). - Only 2 have
upvoted_count > 0, and both aretestusertest fixtures (api-retry-pattern,k8s-rolling-upgrade). Zero real upvotes across the whole team. recalled_countis healthy;upvoted_countis the only counter that is dead.
Why the persistent nudge from #371 didn't fix it:
- Upvote still depends on the main agent voluntarily writing
<!-- teamai:referenced-doc-ids: [...] -->(src/builtin-rules.ts:165-176, collected atsrc/hook-handlers.ts:360). This is a weak contract — the model has to remember and choose to emit a specific HTML comment at the end of its reply. - The nudge does not actually force the model to declare. The nudge returns
additionalContexton theStopevent (src/utils/hook-output.ts:13). Claude Code only blocks a stop and forces another turn ondecision: "block"(the contract is called out insrc/omp-hooks.ts:24);additionalContexton Stop does not continue the session. So the session ends and the declaration is never produced. Empirically, scanning 2387 local transcripts, 54 carried a recall signal but only ~2 carried a genuine (non-placeholder, non-subagent)referenced-doc-idsdeclaration — the rest were subagent noise or rule-template placeholders (doc-id-1,你实际用到的-doc-id). - Even a correct declaration can be silently dropped. One session declared 5 doc-ids that exactly matched its recalled set (re-running the parser confirms
verified = 5), yet that user's local and team votes files showupvoted_count: 0, noupvoted_delta, nolast_upvoted_atfor all 5. The whole handler is wrapped intry { … } catch {}with no logging (src/hook-handlers.ts:449), so any failure — 4.5s Stop timeout (src/hook-handlers.ts:58), path/username mismatch, a parse throw — vanishes without a trace.
Downstream effect: the upvoted term and the upvoted/recalled ratio in confidence collapse to 0 (src/maintenance/confidence.ts), and the promote gate upvoted >= 5 is unreachable. This issue is only about the collection mechanism, not the thresholds (see Alternatives).
Proposed Solution
Soliciting design input for a new collection mechanism that does not depend on the AI voluntarily declaring references. The design space, for discussion (not prescribing one):
- Background LLM-judge (detached, after session). When a session recalled docs, a
background: trueStop handler (mirroring theupdateHandlerdetached pattern) asks the local signed-in CLI whether the final reply substantively used each recalled doc, then writes upvotes. Pros: no AI cooperation needed, no extra in-session turn, no loop risk. Cons: one extra local-CLI LLM call per recall session; only helps upgraded users; ceiling limited by how often recall is genuinely adopted (adoption is structurally sparse). - Tool-use evidence. Count a recall as adopted when the agent opens the recalled doc's file via
Read/Grep/Globin the same session. High precision, zero cooperation needed. But near-zero recall for learnings, because the recommended recall path injects the subagent's summary as text and the model rarely opens the file itself — so this alone barely feeds promotion. - Subagent-side attribution. The
teamai-recallsubagent already reliably emitsrecalled-doc-ids(.claude/agents/teamai-recall.md). Explore attributing adoption from the subagent interaction rather than the main agent self-declaring.
Whatever mechanism is chosen, it should: (a) not require the AI to self-declare; (b) not force extra turns or risk loops (cf. #614); (c) log failures instead of swallowing them — the silent catch {} at src/hook-handlers.ts:449 must go regardless; (d) keep HTTP teams out of upvote collection (see Alternatives).
Alternatives Considered
- Switch the nudge to
decision: "block"with a per-session cap. Would force the model to declare, but is intrusive (an extra turn + cost on every recall session), the model can still declare empty[]→ 0 upvotes, and it is loop-prone (the exact failure fix: prevent infinite nudge loop when model declares empty referenced-doc-ids #614 patched). Rejected as the primary path; at most a cap-bounded supplement. - Lower the promote
upvoted >= 5threshold. Explicitly out of scope. This papers over the loss instead of fixing collection; fix(votes): make upvote collection reliable for learning promotion #371 already chose not to touch thresholds, and this issue keeps that line. The goal is for real upvote signal to flow. - Relax
gitOnly: trueonvotesSyncHandler(src/hook-handlers.ts:565) so HTTP-provider teams collect upvotes. Out of scope — HTTP teams do not need upvote collection by design;gitOnlystays.
Additional Context
Key code locations:
| Topic | Location |
|---|---|
| Upvote collection entry (Stop hook) | src/hook-handlers.ts:337 (votesSyncHandler), :565 (gitOnly registration), :58 (4.5s timeout) |
| Silent error swallow | src/hook-handlers.ts:449 (catch {}) |
| Declaration → upvote intersection | src/hook-handlers.ts:358-363 |
| Nudge output (additionalContext on Stop) | src/hook-handlers.ts:434-448, src/utils/hook-output.ts:13 |
| Stop continuation contract note | src/omp-hooks.ts:24 (decision: "block" forces a turn) |
| Transcript parsing | src/transcript-parser.ts (referenced-doc-ids at :143, recalled-doc-ids at :88) |
| Self-declaration rule given to the AI | src/builtin-rules.ts:165-176 |
| Upvote/recalled counters | src/votes.ts:95 (incrementUpvoted), :71 (incrementRecalled) |
| Confidence (upvoted term + ratio) | src/maintenance/confidence.ts |
Promote gate (upvoted >= 5) |
src/maintenance/promote.ts |
Related: #369 (original diagnosis), #371 (persistent-nudge fix that did not resolve it), #614 (nudge-loop fix), #368 (teamai viz surfaces these counts).
Scope guards (per discussion before filing): do not change promote thresholds; HTTP teams do not need upvote collection (gitOnly is intentional).
Activity
feat(votes): show adopted team knowledge and drop AI self-declaration
fix(votes): collect upvote adoption from tool-use evidence, not AI se…
feat(votes): add opt-in background LLM-judge for upvote adoption (Ten…
fix(votes): serialize incrementUpvoted against concurrent writers (Te…
fix(votes): harden per code review — stale-safe lock, suffix path mat…
docs(usage): document the opt-in TEAMAI_UPVOTE_JUDGE flag (Tencent#723)
feat(votes): show adopted team knowledge and drop AI self-declaration
fix(votes): address code-review findings on the Tencent#723 adoption …
fix(votes): second-pass review hardening — atomic dedup, locked write…
fix(votes): log why votes-sync failed instead of swallowing it
fix(votes): third-review hardening — atomic write, grounded judge, sa…
fix(votes): collect upvote adoption from tool-use evidence + opt-in L…
fix(votes): collect upvote adoption from tool-use evidence + opt-in L…
fix(votes): tighten adoption evidence in transcript parser (Tencent#723)
fix(votes): dedup the LLM-judge via the vote ledger, drop session mar…
fix(votes): collect upvote adoption from tool-use evidence + opt-in L…
fix(votes): address CI review (Tencent#787)
fix(votes): keep votes with the scope they were cast in (#787) (#793)
fix(votes): ground the upvote judge from the recall log on the subage…
fix(votes): ground the upvote judge from the recall log on the subage…
fix(votes): ground the upvote judge from the recall log on the subage…
fix(votes): ground the upvote judge from the recall log on the subage…