Say why every conversation went where it did - #149
Merged
davidmckayv merged 1 commit intoAug 22, 2026
Merged
Conversation
`channel.routed` carries a `viaMention` field that was hardcoded false at the only place the row was written, so it could never be true. The reason was structural rather than a typo: naming a coworker with `@` short-circuited the router in the composer, so that code never ran and no row was written at all. The trail therefore answered "why did this go to Risk Analyst" for conversations the router placed and said nothing whatever for ones the person chose by hand, which reads exactly like a row that failed to write. So a mention is recorded too. The endpoint takes the named coworker, checks it against the same roster the router picks from, and records it with `viaMention` true and the person as the reason. No model is called: they already decided, and asking would be spend, latency and a chance to disagree. A name that is not on the roster is refused rather than quietly turned into somebody else, because silently redirecting a message somebody addressed by hand is the worst answer available. The client throws the response away and starts the conversation with the coworker named regardless, so failing to write the row can never cost somebody their message. Both branches now write the row through one function, because two call sites writing the same event is two payloads that drift. The audit page had to change with it or none of this would be visible. A routing row rendered with no subject, no Bot and a decision of "Allowed", which is the same nothing the missing row was. It now names the coworker it went to and separates the three cases that matter: the person chose them, the router matched them, or the router gave up and used the default. The reason sits underneath, at a fixed width so a sentence a model wrote wraps inside the table instead of running off the edge. Closes #133.
davidmckayv
requested review from
MikeRyanDev,
guidovizoso and
tylerslaton
as code owners
August 22, 2026 04:16
davidmckayv
deleted the
fix/the-audit-trail-says-why-every-conversation-went-where-it-did
branch
August 22, 2026 04:18
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.
Closes #133.
What was missing
channel.routedcarries aviaMentionfield. It was hardcodedfalseat the only place the row is written, so it could never betrue.The reason was structural, not a typo: naming a coworker with
@short-circuits the router in the composer, so that code never ran and no row was written at all.So the trail answered "why did this go to Risk Analyst" for conversations the router placed, and said nothing whatever for ones the person chose by hand. A missing row is indistinguishable from one that failed to write.
The issue offered two ways out. This takes the first: record the mention, so the field earns its place and the trail answers the question for every conversation.
What it does now
The endpoint accepts the named coworker, checks it against the same roster the router picks from, and records it with
viaMention: trueand the person as the reason.The page had to change too
Adding the row alone would have achieved nothing visible. A routing row rendered with no subject, no Bot, and a decision of "Allowed" — the same nothing the missing row was.
Now it names the coworker, and separates the three cases:
@The reason sits at a fixed width so a sentence a model wrote wraps inside the table. A
max-widthdoes not work here: the table lays itself out from its content and ignores it, and the end of the sentence went off the right edge where nobody scrolls to find it.Driven in Chrome
All three rows above are real, from a running deployment, read off
/admin/auditafter driving each case through the composer.The
@case: picked Risk Analyst from the@menu, asked "What is 9 plus 6?" — a question the router would never send to a compliance specialist, so the row cannot be a coincidence. Went to Risk Analyst, answered15, and:Worth noting what the intermediate state caught: with the client updated and the server not yet restarted, the mention reached the inferred branch and recorded
chosen=knowledge, fallback=true. The conversation still went to Risk Analyst, because the client discards that answer. That is the safety net above, working, before it was ever needed.Tests
server/tests/routing-routes.test.ts, 5 tests, covering the endpoint that had none:agentIdis a message with no mention, not a broken one