fix(question-log): parse native AskUserQuestion answers — every native answer logged as __unknown__

Current Claude Code returns AskUserQuestion results as an OBJECT map keyed
by question text ({answers: {question: label}}); the hook only handled the
legacy array shapes, so 86% of live records carried user_choice __unknown__
— and the bin then scored every one as followed_recommendation false,
silently poisoning plan-tune metrics. Adds the object-map extraction (exact
+ whitespace-normalized + single-question pairing, multiSelect joins,
annotations as free_text), strips the (Recommended) suffix from BOTH sides
of the comparison, skips the computation entirely on extraction failure,
and logs unrecognized shapes to hook-errors.log instead of embedding them
in the record.

Fixes #2336, #2206.

Based on the working patch in #2336 by @yijisoo; suffix comparison fix
contributed by @chuchu2781 (PR #2400).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-14 20:20:52 -07:00
co-authored by Claude Fable 5
parent 540847d038
commit 54612cb9e3
3 changed files with 151 additions and 11 deletions
+11 -3
View File
@@ -168,9 +168,17 @@ if (j.recommended !== undefined) {
if (j.recommended.length > 64) j.recommended = j.recommended.slice(0, 64);
}
// followed_recommendation — compute if both sides present.
if (j.recommended !== undefined && j.user_choice !== undefined) {
j.followed_recommendation = j.user_choice === j.recommended;
// followed_recommendation — compute if both sides present. An __unknown__
// choice means extraction failed, not that the user rejected the
// recommendation — leave the field absent so metrics can't be poisoned.
// Strip a trailing (Recommended) marker from BOTH sides before comparing:
// recommended usually arrives pre-stripped while user_choice is the raw
// option label, so a user who picked the recommended option was scored as
// NOT following it (#2400). NB: this JS lives inside a double-quoted
// bun -e string — never use double quotes in it.
if (j.recommended !== undefined && j.user_choice !== undefined && j.user_choice !== '__unknown__') {
const stripRec = (s) => String(s).replace(/\s*\(recommended\)\s*$/i, '').trim();
j.followed_recommendation = stripRec(j.user_choice) === stripRec(j.recommended);
}
// session_id — kebab-friendly; <=64 chars