---
title: "227 Unverified Cards on /flow and Two Causes Hiding Behind One Theory"
canonical: https://dxdev.com/blog/speaker-provenance-words-store/
datePublished: 2026-10-11
---
## The page that mixed us up

The /flow page showed 227 "speaker unverified" cards for the last 7 days, and some of them were not mine. A screenshot showed one reading "Manager to…", which is a hub telling a child session what to do. I asked why there were so many unverified cards, and whether we could build a flow that knows which messages come from agents and which come from me. Keeping my own messages stored properly is one of my core goals.

## The first explanation

The explanation on the table was that a relay looks exactly like something I typed once it arrives inside the child session. It was stated as a guess, with "I haven't confirmed that yet" attached. We acted on it anyway. An agent went off to fix it with this plan:

- Mark every message with its sender at send time, instead of guessing from the text.
- Relabel the stored messages with the same rules, with anything unclear staying "unverified" and never counted as mine.
- Show only my verified words under "what you said" and move agent messages somewhere separate.

The relay theory turned out to cover only part of it. The agent came back with two root causes.

First, every message that came in through a bridge session was unverified. Those sessions carry both my typing and hub relays, and nothing recorded which was which.

Second, messages I sent while a turn was still running were never read at all. [Claude Code](/blog/2026-09-26_hub-doorway-sub-sessions/) records those as `queued_command` attachments, and the word sync skipped that form. About 200 of my messages a week were sitting unclassified and never appeared on /flow.

So the theory covered the first cause and said nothing about the second.

## What replaced the text guessing

Every message the server puts into a session now gets written to a sender log under `.state/provenance`. That covers `/message`, the spawn seed, a pinned spawn and the front desk. The log records who sent it, and the sources are:

- **Declared senders.** Our own sending tools (session-send, subsessions, spawn, the coceo bridge, and the WhatsApp gateway, which sends as me) add a sender header to what they send. The server writes that to the log.
- **Web.** A message with no declaration that comes from a browser page counts as me typing on the web, decided from the browser's fetch metadata (`Sec-Fetch-Site`).
- **Anything else.** A script that doesn't identify itself is recorded as unknown, and never as me.

The speaker is then decided by `words.classify_with_basis` in a fixed order:

1. The sender log.
2. Fixed strings only our code writes.
3. `origin.kind`, which includes Claude Code's own "typed by a human" flag.
4. Text heuristics, last. They are labelled as guesses and can only ever say "agent".

The store itself changed too. It is schema 9, with `heard.speaker` and `heard.origin` columns and a `ben_words` view over them. Lines from before origin was recorded are stored with `speaker='unverified'` and kept out of `ben_words`. Whether those should ever count as my words downstream is still an open question.

## Checking it

A reclassify command backfilled 731 rows. Unverified for the last 7 days went from 227 to 26. A throwaway provenance check also ran three prompts, each asking for one exact reply: `PROVCHECK-SEED`, `PROVCHECK-WEB`, and a manager check for `PROVCHECK-AGENT`. Seed, web and agent sends were all stamped correctly.

The tests were `test_units_words*`, handoff_spawn, subsessions, whatsapp_session and units_cli, 203 passing. The full local gate ran 16052 passed and 1 failed. The failure is in the WhatsApp gate test, which fails on HEAD too.

Two bugs were fixed before the store landed, each with a regression test. A transcript line whose `message` is a string crashed classify and scan. Hook rows are now matched to transcript lines by globally nearest pair, each row once.

## What still isn't right

I signed off on /flow as "good enough for now". Among the follow-ups logged at that point was one on this same labelling: agent browser clicks are logged as me.
