Skip to content

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: none

Read the Resolver: section — it names every slot and its outcome. Each slot has one of these statuses:

StatusMeaning
resolvedEntity matched a single database row
ambiguousMultiple rows matched — VERA needed to ask
not_foundNo row matched
would_auto_createNo row, but the verb implies creating one alongside the main record
pronoun_resolvedA pronoun (it, them, etc.) matched a recent entity from this conversation
pronoun_unresolvedA pronoun had no plausible referent in conversation history
raw_passthroughNon-entity value (hours, money, date) — carried through unchanged
mention_errorA 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 last

VERA 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 !debug without last (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_found

Find all pronoun fall-throughs:

pronoun_unresolved

Log 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_budget

For 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.