Troubleshooting entity resolutions
What this page is for
When VERA's entity resolution does the wrong thing — picks the wrong project, ignores a pronoun, fails to auto-create a client — this is how an operator (or the affected user) finds out why.
The resolution pipeline is now live on every inbound DM. It runs before every Claude call, tokenises the user's message, identifies the intended action (verb), and resolves each referenced entity (project, client, person, task) against the database. Every message that matches a verb produces a !debug last record. The result is also logged to stdout so you can query it across time and users.
The !debug last command
DM VERA: !debug last
VERA returns the most recent resolution it ran for your Slack user. The output looks like:
Last resolution — 2026-05-16 22:14 UTC
Message: "create project Foo for Acme"
Extractor: matched verb create_project (typed as "create")
project_name: "Foo" (project)
client_name: "Acme" (client)
Resolver:
project_name → not_found
client_name → would_auto_create (proposed: "Acme")
Missing slots: noneRead the Resolver: section — it names every slot and its outcome. Each slot has one of these statuses:
| Status | Meaning |
|---|---|
resolved | Entity matched a single database row |
ambiguous | Multiple rows matched — VERA needed to ask |
not_found | No row matched |
would_auto_create | No row, but the verb implies creating one alongside the main record |
pronoun_resolved | A pronoun (it, them, etc.) matched a recent entity from this conversation |
pronoun_unresolved | A pronoun had no plausible referent in conversation history |
raw_passthrough | Non-entity value (hours, money, date) — carried through unchanged |
mention_error | A Slack @mention couldn't be parsed |
!debug last is read-only. Calling it never produces a new resolution record.
DM only. The command is ignored in channels — resolutions may contain entity names from other users' conversations.
Inline debug — append !debug last to any DM
Instead of sending !debug last as a separate follow-up, you can append the marker to any DM and VERA will return both the normal response and the resolution record in the same turn:
log 3 hours to CityCore on QA today !debug lastVERA processes the action normally, then appends the resolution record separated by a --- divider. The marker is stripped before the extractor and Claude see the message — they receive the clean text as if the marker were never there.
Works with:
- Any natural-language action (
create client Foo !debug last) - Digit confirmations (
1 !debug last) - The short alias
!debugwithoutlast(log 2h to Acme !debug)
Does not trigger for:
- Standalone
!debug last— that still runs the existing standalone command - Mid-message occurrences (
show !debug last week's entries) — the match requires end-of-string
This is most useful during smoke testing: each step produces both the action result and the resolution in one message, halving the round-trips needed to verify entity resolution.
The stdout log
Every resolution emits a single-line JSON object to stdout, captured by Railway. Use Railway's log search to query across users, tenants, and time ranges.
Filter to a specific user:
slack_user_id="U..."Find all not_found outcomes (entity the resolver couldn't match):
event="resolution" not_foundFind all pronoun fall-throughs:
pronoun_unresolvedLog line shape:
json
{
"event": "resolution",
"ts": "2026-05-16T22:14:33.012Z",
"tenant_id": "abc",
"slack_user_id": "U123",
"message_ts": "1747...",
"raw_message": "create project Foo for Acme",
"extractor": { "matched": true, "canonical_verb": "create_project", ... },
"resolver": { "resolutions": { ... }, "missing": [] }
}raw_message is truncated to 500 characters in the log. The in-memory record (accessible via !debug last) retains the full text.
Common failure modes
Wrong entity picked from conversation history (pronoun resolved to stale entity)
Check that entities is populated on the prior turn's conversation record. The pronoun scanner reads conversations.entities — if the column is empty for the turn where the entity was introduced, the pronoun will fall through to pronoun_unresolved. Look for extractTurnEntities in index.js — it runs after every tool call response and populates that column.
Pronoun came back unresolved even though the entity was mentioned recently
The entity must have been the result of a tool call (e.g. get_project_info returned it) for it to land in conversations.entities. A message that only mentioned the entity by name in prose, without a tool call confirming it, won't populate the column.
Client got auto-created when we wanted to use an existing one
The resolver matched would_auto_create because the client name wasn't an exact or near-exact match to any existing client. Use the exact stored name, or check for a typo.
Project resolved to the wrong one when names are similar
The resolver found an ambiguous match but VERA still proceeded — this means the wiring layer chose one. Check !debug last to see the candidates list. If the intended project isn't in the candidates, the name diverges more than expected.
Resolution looks wrong but !debug last shows no record
Every inbound DM now produces a !debug last record — including confirmation replies (yes, 1, etc.) and messages whose verb the extractor doesn't cover (e.g. cancel, add user). If !debug last returns nothing, the message handler likely short-circuited before the entity pipeline (e.g. a !test command, a dialing-menu digit, or a CSV export request). Check the structured stdout log for event="resolution" entries.
For confirmation replies, the record shows Extractor: confirmation reply and the pending intent: line names the tool that was about to fire from the prior turn. For example:
Last resolution — 2026-05-22 22:00 UTC
Message: "1"
Extractor: confirmation reply
pending intent: set_project_budgetFor unmatched messages, the record shows reason: no_verb_match — confirming the extractor saw the text but found no known verb. If the verb is a common one that should be covered, consider adding it to lib/verb-context-table.js.
The legacy [Pre-resolved: …] annotation path (for non-wired user-bearing verbs) does not write to the ring buffer — check the outbound message for that prefix if !debug last shows only the extractor/resolver section without any tools.
What this page does not cover
Issues outside the entity-resolution pipeline: LLM hallucinations, tool execution failures, permission errors. Those have their own paths — check the error reporter GitHub issues (labelled auto-reported) and the status dashboard at /status.