feat(assistant): make the thumbs actually report something (#1236)

* feat(assistant): make the thumbs actually report something

The thumbs up/down under an assistant answer shipped wired to nothing. They lit
up, the vote died in component state, and the code said so in a comment nobody
reading the UI could see. An affordance that looks like it reports something and
does not is worse than no affordance: it spends the user's goodwill once, and
silently.

They now post to a new /api/agent/feedback, which emits the SAME agent.feedback
event the gnubok_feedback MCP tool emits, with actorType 'user'. The product
team already queries event_log for that type, so chat votes land in the backlog
they read rather than in a second place someone has to remember to look at.
event_log takes the payload as jsonb and already treats agent.* as telemetry, so
there is no migration.

The conversation id is caller-supplied, so it gets the same ownership check
/api/agent/invoke got: without it a member could file feedback against a
colleague's thread and the backlog would carry conversations the reporter never
saw. Mutation-checked, three tests fail when the guard is removed.

The pressed state is set only after the server accepts the vote, so the button
never claims a report that never arrived, and a vote does not toggle off: it is
append-only telemetry, and offering an undo we cannot honour would be a control
that lies. Changing your mind sends the other sentiment instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(assistant): drop the unused free-text field from the feedback route

compliance-swarm flagged the `comment` field as an undocumented PII exposure:
free text embedded verbatim into the agent.feedback payload and written to
event_log under telemetry retention, with no data-classification decision, next
to a PostHog policy that treats the same class of data differently.

The finding is right, and the field was worse than it looked: no caller ever
sent one. The UI posts sentiment and a turn index. So this is dead API surface
whose only effect was to accept whatever a user might type, in an accounting
product, into a 180-day log: client names, personnummer, case details.

Removed rather than documented. A comment box is a reasonable thing to want,
but it needs its own classification and redaction decision made with the UI in
front of it, not inherited from an unused parameter.

The test now asserts the property instead of the field's absence: a caller that
posts a comment anyway must not get it stored anywhere in the payload.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-27 15:46:08 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 2da96c0be2
commit 1dce9227a4
6 changed files with 445 additions and 9 deletions
+3
View File
@@ -600,3 +600,6 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-07-27] Removing Recapt made lib/support/submit-feedback.ts single-channel: a failing /api/support/contact now surfaces as a real error instead of being masked by Recapt reporting success on its own channel. That is the correct behaviour (silently "succeeding" while the message reached nobody was worse) and the Resend path is solid. A non-blocking posthog.capture('support_feedback_submitted') replaces the useful half of the Recapt channel by putting the submission on the user's timeline next to the session replay; it deliberately carries no message body, since free text is user content and would be PII in an event property.
[2026-07-27] vitest.config.ts now aliases 'server-only' to tests/stubs/server-only.ts. Its real entry point throws unconditionally (Next.js swaps it out at bundle time; Vitest cannot), so the moment a server-only module entered the test graph it broke 48 test files at import. app/(dashboard)/request-context.ts was already carrying the same latent trap and had simply never been imported by a test.
[2026-07-27] Self-hosted the dicebear Notionists avatars under public/agent-avatars instead of loading api.dicebear.com per render: the licence turned out to be CC0 1.0 (verified on dicebear.com/licenses AND in each file's own RDF metadata), so there was no licence decision to escalate, and the CDN was sending every authenticated page view's IP and referer to a third party while breaking firewalled/self-hosted installs entirely.
[2026-07-27] Chat thumbs emit the existing agent.feedback event with actorType 'user' rather than a new table or event type: the product team already queries event_log for that type from the MCP tool, so a second store would be a second place someone has to remember to read, and event_log's jsonb payload needs no migration.
[2026-07-27] A cast vote does not toggle off: it emits append-only telemetry and there is no un-emitting one, so an undo control would be a button that lies. Changing your mind sends the opposite sentiment, which the backlog can actually see.
[2026-07-27] Dropped the free-text `comment` field from /api/agent/feedback instead of documenting a retention policy for it (compliance-swarm V14.1): no caller ever sent it, and an unused free-text parameter in an accounting product is a PII sink into event_log's telemetry retention (client names, personnummer). A comment box needs its own classification and redaction decision made with the UI in front of it, not as dead API surface.