Skip to content

Say why every conversation went where it did - #149

Merged
davidmckayv merged 1 commit into
mainfrom
fix/the-audit-trail-says-why-every-conversation-went-where-it-did
Aug 22, 2026
Merged

Say why every conversation went where it did#149
davidmckayv merged 1 commit into
mainfrom
fix/the-audit-trail-says-why-every-conversation-went-where-it-did

Conversation

@davidmckayv

Copy link
Copy Markdown
Contributor

Closes #133.

What was missing

channel.routed carries a viaMention field. It was hardcoded false at the only place the row is written, so it could never be true.

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: true and the person as the reason.

  • No model is called. They already decided. Asking would be spend, latency, and a chance to disagree with them.
  • A name not on the roster is refused, not redirected. Quietly turning a name somebody typed by hand into somebody else sends their message to a coworker they did not choose and says nothing about it.
  • The client throws the response away. It starts the conversation with the coworker named regardless, so failing to write the audit row can never cost somebody their message.
  • One function writes the row for both branches. Two call sites writing the same event is two payloads that drift.

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:

Coworker Decision
named with @ Risk Analyst The person chose this coworkeryou chose them yourself
router matched Risk Analyst Sent to the coworker it is forGoogle Drive access and investigation of company-wide metrics
router gave up Knowledge Sent to the default coworkersent to your default; no specialist was a confident match

The reason sits at a fixed width so a sentence a model wrote wraps inside the table. A max-width does 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/audit after 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, answered 15, and:

chosen=risk-analyst  viaMention=true  reason="you chose them yourself"  candidates=["risk-analyst"]

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:

  • a named coworker is recorded as the person's own choice
  • naming a coworker never asks the model
  • an inferred choice is still recorded as inferred
  • a coworker not on the roster is refused, not redirected
  • a blank agentId is a message with no mention, not a broken one

`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
davidmckayv merged commit cc05c05 into main Aug 22, 2026
8 checks passed
@davidmckayv
davidmckayv deleted the fix/the-audit-trail-says-why-every-conversation-went-where-it-did branch August 22, 2026 04:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

viaMention can never be true, so a mentioned conversation leaves no routing row

1 participant