fix: fall back to hook-generated reactions when HTML comments are visible (#124) - #127
Open
TorranceTech wants to merge 1 commit into
Open
fix: fall back to hook-generated reactions when HTML comments are visible (#124)#127TorranceTech wants to merge 1 commit into
TorranceTech wants to merge 1 commit into
Conversation
…ible Claude Code v2.1.169+ renders <!-- buddy: --> HTML comments visibly in the chat output instead of hiding them, breaking the end-of-turn reaction system (issue ramarivera#124). This fix adds a two-path strategy to buddy-comment.sh: - Primary: extract the existing <!-- buddy: ... --> comment (works on older CC) - Fallback: call server/turn-reaction.ts to generate a reaction from the turn pool when no comment is found, so the status line always updates cleanly The prompt in server/index.ts is softened to tell Claude to omit the comment if it would appear visible, since the hook now covers that case automatically. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: TorranceTech <billy.anderson.it@gmail.com>
Owner
|
Hey @TorranceTech — CI is now approved to run on this PR, and it surfaced one typecheck failure: If the cast is intentional, 🤖 Created with the help of AI (Claude Fable 5). |
ramarivera
added a commit
that referenced
this pull request
Jul 26, 2026
#159) * fix(server+hooks+README): make buddy_react the primary end-of-turn channel The HTML-comment channel broke visibly in Claude Code v2.1.169+ (issue end-of-turn, steering the model to the broken channel while keeping the working MCP tool unused. Result: every assistant message leaked the raw <!-- buddy: ... --> line and the primary model-authored path never fired. Accept-with-changes verdict on PR #127: that PR keeps the HTML comment as primary and adds a canned-pool fallback for when the comment is missing. That trades a visible-artifact bug for the same canned-pool loss the user already complained about ('suspicious English placeholder shit'). The real fix is the in-process option: make buddy_react the primary channel (it renders nowhere in the transcript and is model-authored by construction), keep the HTML-comment extractor as a backward-compat fallback for older Claude Code versions, and only fall through to a canned-pool line as a last resort. Changes: * server/state.ts — ReactionSource = tool|comment|fallback|none; ReactionState.source is optional and loaded as 'none' for legacy files. saveReaction() takes an optional source argument (default 'tool' since every MCP-side caller is one). * server/index.ts — getInstructions() and buddy://prompt now direct the model to call buddy_react at the end of every turn, with explicit examples. The contradictory 'Do NOT use buddy_react for end-of-turn comments' line is gone, as is the 'do NOT append an invisible HTML comment' steer. buddy_react / buddy_pet / buddy_unmute tag their writes source=tool or source=fallback. buddy_stats now loads the reaction file and shows the last line + its source so users can prove the bubble is not canned. * hooks/buddy-comment.sh — header rewritten to describe the 3-source chain; bookkeeping (turn counter, XP, memory) always runs on assistant message; reaction write skipped when a fresh source= tool reaction is on disk; otherwise tries the legacy HTML comment (source=comment) then falls back to a pool pick via the new server/turn-reaction.ts script (source= fallback). Cooldown still rate-limits the reaction write, not the bookkeeping. * server/turn-reaction.ts — new canned-pool entry used only as the last-resort fallback. Mirrors what PR #127 added but with the source tag wired through. * README.md — features card and 'How It Works' updated to describe the new chain and provenance tag; the '<!-- buddy: ... -->' Stop Hook line is replaced with the actual chain (skip tool → comment → fallback). Tests: bun test (246 pass, 0 fail). bun run typecheck clean. Smoke tests confirmed all three reaction paths (comment writes source= comment, canned fallback writes source=fallback, fresh tool reaction is left alone). Refs #124, #154. * fix(server+SKILL): reactions are statusline-only (F6 — maintainer decision) The previous round argued for 'visible by design' based on the hypothesis that MCP tool results render in Claude Code's transcript. The maintainer observed from a live session with a screenshot: that is false in his environment. Tool results are NOT visible; the model was duplicating the reaction into the reply body, producing a duplicate-on-every-turn stream the user does not want. Correct model of the channel: * buddy_react is a real MCP tool call → its result is the bubble text on the statusline only. * The user never reads the reply text containing the tool call. * The instruction 'display the tool result verbatim' was the leak: it told the model to echo the reaction in prose, where Claude Code did not show it as a tool result and the user instead saw it duplicated into the reply. Scrubbed: * server/index.ts:101 (NAME REACTIONS block) * server/index.ts:120 (END-OF-TURN block) * server/index.ts:300 (buddy_react tool description) * server/index.ts:1360 (buddy://prompt duplicate block — deleted entirely; corrected the keeper at the fence close above) * skills/buddy/SKILL.md:89 (the only writer of the duplicate) Preserved: * skills/buddy/SKILL.md:95 (buddy_uninstall — different tool, the maintainer explicitly excluded that line). * The statusline-only framing ('renders nowhere in the user's transcript') — that is the accurate description in this environment. Tool doc / prompt now state the silent contract explicitly: the result only reaches the statusline, the call is a silent side effect, do not narrate / echo / quote / explain. Tests/typecheck unchanged (purely documentation and prompt wiring, no behaviour moved). * fix(hooks+state): F1-F5/F7 (per-turn sentinel, default fallback, source tagging) Lost the previous F1-F5 fixes when I used 'git reset --hard' to rebase F6 onto a clean origin/fix/react-channel. Reviewer (Opus 5) flagged the regression; this restores the fixes with TDD discipline: tests written FIRST, verified to fail against the prior code, then the fix. F1 — per-turn sentinel (CRITICAL). The 60 s wall-clock freshness gate lost tool reactions on any turn longer than 60 s, which is ordinary for agentic coding (90 s-5 min). Stop hook now writes its run time to '.last_stop_hook.<sid>' after each invocation; a tool reaction is treated as 'from this turn' iff its timestamp is more recent than the previous Stop hook run. F2 — clock skew guard. Tool timestamps implausibly in the future (beyond 60 s) are treated as clock-skew garbage and the hook falls through to the comment / pool branch. 'lastStopRun' on the writer side is clamped to min(lastStop, now) so a future-dated marker does not poison the comparison. ('react.ts' now distinguishes a future tool timestamp from a legitimate one instead of rejecting both as 'age < 0'.) F3 — saveReaction contract. Default source flipped from 'tool' to 'fallback'. writeStatusState no longer calls saveReaction implicitly — every hatch / pet / unmute call path now writes its own provenance explicitly, so /buddy stats can no longer lie about canonical 'tool' lines being canned. F4 — pool-write hooks tag their provenance. react.ts, mood-react.ts, file-type-react.ts, name-react.ts all now write 'source: fallback' alongside reaction / timestamp / reason. Legacy files without the field default to 'none' on load. F5 — bookkeeping respects the rate limit. 10 short turns inside the default 30 s cooldown window now produce 1 turn-counter increment and 2 detached bun boots, matching main's behavior. Bookkeeping still fires on the fresh-tool-skip path (so the stop marker is stamped) but does NOT increment turns or spawn XP / memory on the path where the tool already wrote the reaction. F7 — literal '{lines}' dropped. The line '*nervous laughter' {lines} lines changed.' in the large-diff pool would render literally as the hook only substitutes {files} and {branch}. Removed. A new test scans every DEFAULT_REACTION_POOLS entry for any unsubstituted {name} token and asserts none (catches future regressions of this class). F9 — atomic write pattern. saveReaction now uses tmp + rename on the reaction file (torn reads were a sub-bug in the freshness gate's read). The Stop hook does the same on reaction / cooldown / stop-marker writes. New test asserts no leftover 'reaction.*.tmp.*' files remain after a hook run. Tests added (all written before the fix and verified to fail): server/hooks/buddy-comment.test.ts: F1 long-turn: 5-min tool reaction is preserved F1 fresh-tool: skipped even within default cooldown; bookkeeping does NOT spawn (F5) F2 future-clock: 5-min-future tool timestamp is clobbered F5 cooldown: 10 rapid-fire turns → events.turns === 1 server/hooks/{react,mood-react,file-type-react,name-react}.test.ts: F4 each hook's reaction file has source: 'fallback' server/state.test.ts: F3 saveReaction without source defaults to 'fallback' F3 writeStatusState does NOT clobber an existing source='tool' reaction in reaction.$sid.json Tests / typecheck: bun test: 369 pass / 0 fail bun run typecheck: clean Branch state: rebased onto origin/fix/react-channel (e38ef95). F6 statusline-only framing from a5d2bd1 is preserved verbatim — the maintainer's directive (reactions are statusline-only, not echoed in the transcript) is intact across the changes in this commit. Refs #124, #154. --------- Co-authored-by: coding-buddy dev <dev@local>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Fixes #124.
Claude Code v2.1.169+ changed how markdown is rendered:
<!-- ... -->HTML comments that were previously invisible now appear visibly in the chat output. This broke the end-of-turn reaction system — every response ends with a raw<!-- buddy: ... -->comment polluting the conversation.Solution
Two-path strategy in
hooks/buddy-comment.sh:Primary path (unchanged, backward-compatible): if Claude writes
<!-- buddy: ... -->and the comment is hidden, the hook extracts and uses it as before.Fallback path (new): when no comment is found in the message, the hook calls
server/turn-reaction.ts— a new lightweight script that reads the companion state and picks a"turn"reaction from the existing pool. The status line still updates on every turn without any HTML leaking into the chat.The prompts in
server/index.tsare updated to tell Claude to omit the comment when it would appear visible, since the hook now covers that case automatically. Old installations (where comments are still hidden) continue to work unchanged.Changes
server/turn-reaction.ts— new script: reads companion state, callsgetReaction("turn", ...), prints to stdouthooks/buddy-comment.sh— adds fallback branch; updates header commentserver/index.ts— softens the HTML comment instruction in bothgetInstructions()andbuddy://promptTest plan
bun test)turn-reaction.tsuses the samegetReaction()path already covered byreactions.test.ts<!-- buddy: -->extraction still works🤖 Generated with Claude Code