Lead session follow-ups (same chat, same lead)
Question: "Rerun Hunt Forge and resynthesize with our current info."
One lead per chat
Each demo/API chat binds one CRM lead (contact_id + user-pasted fields).
Follow-up hunts must reuse that bound record — not a legal entity name
discovered during enrichment (e.g. user pasted "Hoops & Gears" but the graph
found HG AutoTech LLC).
| Do | Don't |
|---|---|
Reuse first enrich_lead_session lead (contact_id intact) |
Rebuild lead from summary_markdown / entities[] |
Pass prompts: ["<user request verbatim>"] |
Pass refresh: true for partial retries |
Let the server load prior_backfill from the chat |
Drop chat_session_id on follow-up |
Tool
POST /api/v1/enrich/lead-session or MCP enrich_lead_session
{
"lead": {
"contact_id": 1737992709,
"company": "Hoops & Gears and Clark",
"domain": "hgautotech.com",
"source_system": "demo_chat"
},
"chat_session_id": "ab62eb36cd134d70a58bc174d6b09185",
"prompts": [
"Rerun the Hunt Forge dig-deeper pass and resynthesize the summary with the current graph."
]
}
refresh vs follow-up
| User intent | Args |
|---|---|
| "From scratch" / "ignore cache" | refresh: true + same bound lead |
| "Rerun Hunt Forge" / "resynthesize" / "dig deeper on X" | prompts: [...] only — no refresh |
| Gap button from UI | prompts: [<button text>] |
refresh: true bypasses agent warm-seed and re-runs all hunt slots from zero.
It does not mean "retry one model."
Agent warm-seed (cross-chat reuse)
When you open a new chat (or a batch row chat) for a lead this agent already
enriched recently, the server loads the latest persisted graph and skips vendor tools
already in tool_trace — unless you pass refresh: true.
Lookup order (same agent):
| Step | Signal | Notes |
|---|---|---|
| 1 | chat_title |
Matches demo_chat_session.title from a prior batch row or enrich chat |
| 2 | Exact input | Canonical CRM JSON equals prior LEB input_snapshot |
| 3 | Batch input | Same canonical JSON on a prior matched batch row (batch_input_exact; debug or LEB graph) |
| 4 | contact_id |
User-supplied CRM int, or external string (e.g. docket id) hashed for LEB lookup |
| 5 | Phone | Shared US last-10 |
| 6 | Business or person name + address | Company or dm_name/name + zip, city+state, or street |
| 7 | Global cache | Cross-agent phone / company+geo / person+geo index |
Check agent_cache.hit, cache_scope (agent vs global), and cache_match
(chat_title, input_exact, batch_input_exact, contact_id, company, phone, …) on the hunt response.
Disable batch-row warm-seed only: AGENT_LEAD_BATCH_CACHE_ENABLED=0. TTL: AGENT_LEAD_CACHE_TTL_HOURS (default 168h).
Canonical lead_record_id (shared graph across chats)
When LEAD_RECORD_ENABLED=1, each resolved lead gets one lead_record_id
and one canonical leb_id per agent. All demo/API sessions bound to that
lead read and write the same persisted graph — person hunts, slot-person,
and full lead-session runs from any chat update one row.
| Field | Meaning |
|---|---|
demo_chat_session.lead_record_id |
Session bound to business identity |
lead_enrichment_backfill.leb_id |
Canonical graph (unique per agent_id + lead_record_id) |
backfill.lead_ref.lead_record_id |
Stamped in every persisted backfill JSON |
backfill.lead_ref.canonical_leb_id |
Same as persist.leb_id after write |
Dup resolution order (when LEAD_RECORD_ENABLED=1): session bind → external
CRM contact_id → input_fingerprint → lookup index (company+geo before
chat_title for company-shaped leads) → create new record.
Person at company: person-shaped sessions may link to the company parent's
lead_record_id; person-hunt writes land on the parent canonical leb_id.
Migration: python3 scripts/migrate_lead_records.py --agent-id N --commit
collapses duplicate LEB clusters (entity index + CRM contact_id) and backfills
session bindings.
Rollout (ordered cutover):
- Deploy SQL (
python3 db/deploy_sql.py) — includesX076filtered legacy index +leb_redirect. - Run migration per agent:
python3 scripts/migrate_lead_records.py --agent-id N --commit. - Set
LEAD_RECORD_ENABLED=1in prod.env(api-worker + jobs viastack-411data.yml). - Verify duplicate sessions share one
leb_id(e.g. re-open follow-up chats, check reconcile).
Cross-agent cache: global warm-seed (Resolve_LeadEnrichmentCache) is unchanged —
agent B can still seed from agent A's indexed graph. Merge/migration tombstone absorbed
leb_ids via leb_redirect; lookup index is re-unrolled on the surviving canonical row.
Hunt Forge / Swift / Deep (dig-deeper)
Middle-pass parallel slots (UI labels Hunt Forge, Hunt Swift, Hunt Deep)
map to internal dig-deeper models. They are not research_model picker values.
- During an active hunt: use the hunt board ↻ (retry one slot) or × (cancel).
- After a hunt completes: use partial routes below — not
refresh: truefull session. - Batch pending/error: hosted UI Run pending & failed (N) reruns all
pending+errorslots in one async job (parallel vendor hunts, one delta synth). API:enrich/lead-session/slots-rerunviaPOST /api/v1/enrich/jobs(see below).
There is no public research_model: data411-hunt-forge arg on enrich_lead_session.
Person hunt (per named contact)
Lead-session may run whitepages_person_hunt for owners/contacts without phones, or when
the user names someone in follow-up prompts. Person-hunt phones carry vendor scores
in backfill.phones[].score and cite as
[person hunt](https://411data.io/person-hunt) in summary_markdown — never
Whitepages-branded URLs in customer-facing prose.
Hosted UI — Hunt controls
On the last hunt turn, Hunt controls lists named contacts under Person hunt (below SIC when present):
| Control | When | Action |
|---|---|---|
| Contact name | Person hunt not yet run for that contact | Chat follow-up → full enrich/lead-session with a person-hunt prompt (same bound lead + chat_session_id) |
| Contact name | Person hunt already run (match or no-match) | Disabled — no new session |
| Intermediate search | Any time (unless that slot is busy) | Partial route slot-person (data411-hunt-person) via async job |
[person hunt] link in summary |
No graph phone and hunt not yet run | Same as clicking the contact name |
[person hunt] link |
Hunt already complete | Non-clickable citation (tooltip: “Person hunt complete”) |
Person-hunt detail may live in backfill.person_hunt_sections[] before merge. When prose
is orphaned, use Merge into summary (synthesize-patch) — not another hunt.
Detecting “already ran”
Authoritative signals (hosted UI and API partial routes use the same rules):
| Signal | Notes |
|---|---|
backfill.person_hunt_sections[] |
Row for contact_id or matching target_name |
tool_trace |
tool: whitepages_person_hunt with matching target_contact_id / target_name, or structured person name match |
backfill.phones[] |
Phone linked to contact with person-hunt source |
Match and no-match both count as “done” — do not re-run person hunt for the same contact.
Chat follow-up vs slot-person
| User intent | Route | Notes |
|---|---|---|
| “Run person hunt for Jane Doe…” | POST /api/v1/enrich/lead-session |
Pass bound lead, chat_session_id, prompts: [<verbatim user text>] — no refresh |
| Intermediate / targeted web research for one contact | POST /api/v1/enrich/lead-session/slot-person |
Per-contact LLM slot; optional chained person hunt |
| Person hunt already done | — | Skip hunt; use Resynthesize if summary is stale |
Slot-person (one contact)
POST /api/v1/enrich/lead-session/slot-person or async job
route: enrich/lead-session/slot-person.
Runs catalog slot data411-hunt-person: person-focused intermediate websearch for
one row in backfill.contacts, delta-merges into prior_backfill, then optionally
queues whitepages_person_hunt when include_person_hunt: true (default) and person
hunt has not already run for that contact.
{
"lead": { "contact_id": 1737992709, "company": "Hoops & Gears and Clark" },
"chat_session_id": "ab62eb36cd134d70a58bc174d6b09185",
"target_contact_id": "c_abc123",
"target_name": "Jane Doe",
"prior_backfill": { "...": "current graph" },
"include_person_hunt": true,
"prompts": ["Focus on direct cell or mobile for this owner."]
}
| Field | Notes |
|---|---|
target_contact_id |
Required — id from backfill.contacts[] |
target_name |
Optional display override; resolved from contacts when omitted |
include_person_hunt |
Default true; set false to run intermediate search only |
refresh_summary |
Default false; set true for full summary rewrite after merge |
Trace rows use hunt_pass: "person" and slot_action: "slot_person" on the intermediate
slot; person-hunt hops append whitepages_person_hunt with target_contact_id /
target_name.
Hosted UI: Intermediate search on the contact row (async job only — same as other partial hunt ops).
{
"route": "enrich/lead-session/slot-person",
"payload": {
"lead": { "contact_id": 1737992709, "company": "Hoops & Gears and Clark" },
"chat_session_id": "ab62eb36cd134d70a58bc174d6b09185",
"target_contact_id": "c_abc123",
"prior_backfill": { "...": "current graph" }
}
}
Submit at POST /api/v1/enrich/jobs — default precharge $0.35 (bill-on-match refund
when no usable rows). Sync POST /api/v1/enrich/lead-session/slot-person remains for
one-shot callers.
Slot rerun (one model)
POST /api/v1/enrich/lead-session/slot-rerun or MCP rerun_hunt_slot
{
"lead": { "contact_id": 1737992709, "company": "Hoops & Gears and Clark" },
"chat_session_id": "ab62eb36cd134d70a58bc174d6b09185",
"model_id": "data411-hunt-forge",
"prior_backfill": { "...": "from last enrich_lead_session backfill" },
"refresh_summary": true
}
model_id accepts public ids (data411-hunt-forge, data411-hunt-swift) or provider
slugs (x-ai/grok-build-0.1). Runs one slot with refresh=true, delta-merges into
prior_backfill, updates tool_trace.parallel_results for that model.
Hosted UI: Hunt controls panel on the last hunt turn → ↻ per slot row.
Prefer async jobs for slot reruns (hosted UI and headless agents):
{
"route": "enrich/lead-session/slot-rerun",
"payload": {
"lead": { "contact_id": 1737992709, "company": "Hoops & Gears and Clark" },
"chat_session_id": "ab62eb36cd134d70a58bc174d6b09185",
"model_id": "data411-hunt-forge",
"prior_backfill": { "...": "from last enrich_lead_session backfill" }
}
}
Submit at POST /api/v1/enrich/jobs — default precharge $0.35 per slot (bill-on-match refund on no usable rows). Sync POST /api/v1/enrich/lead-session/slot-rerun remains for one-shot callers.
Batch slot rerun (pending + error)
POST /api/v1/enrich/jobs with route: enrich/lead-session/slots-rerun (no sync HTTP route).
Runs eligible parallel slots in one job: vendor hunts in parallel, trace upsert per slot, one delta synthesis at the end (not N synth passes).
{
"route": "enrich/lead-session/slots-rerun",
"payload": {
"lead": { "contact_id": 1737992709, "company": "Hoops & Gears and Clark" },
"chat_session_id": "ab62eb36cd134d70a58bc174d6b09185",
"model_ids": ["data411-hunt-ground", "data411-hunt-sonar"],
"prior_backfill": { "...": "current graph" },
"progress_key": "job:slots:optional"
},
"bill_up_to_usd": 0.70
}
| Field | Notes |
|---|---|
model_ids |
Public ids (data411-hunt-forge, …) or provider slugs. Omit to run all trace-eligible slots (see eligibility below). |
bill_up_to_usd |
Default N × $0.35 when omitted (N = count of ids the server will run); override up to job max |
progress_key |
Optional live board key for poll progress |
Server eligibility (authoritative — derived from prior_backfill.tool_trace, not the hunt board alone):
| Trace state | Batch behavior |
|---|---|
pending or error in latest parallel trace |
Runs |
| Never appeared in trace (e.g. first-pass catalog slot after dig-deeper-only hunt) | Runs (first hunt for that slot) |
done, cancelled, repetition, or running |
Skipped (other ids in the same request still run) |
| No runnable ids after filtering | Job refused: status: refused, code: SLOTS_RERUN_REFUSED, error: no_eligible_slots |
When the client sends model_ids, the server filters the list — it does not fail the whole job because one id is already done. Settled slots (including aliases like minimax/minimax-m2.7 → data411-crimson-span) are skipped silently.
Hunt board vs trace: First-pass catalog rows are not injected when that model already has a settled trace row in any pass (e.g. dig-deeper done). Remaining not run rows are truly never-run slots (use per-slot ↻ or batch when listed). Run pending & failed (N) counts only trace-eligible targets (huntSlotsEligibleForBatchRerun — pending/error in trace, or never-run first-pass catalog). Batch still filters against tool_trace on the server; already-done ids are skipped silently.
Excludes cancelled, repetition, and in-flight running slots. Does not auto-resynthesize the summary — use Resynthesize if prose is stale after batch completes.
Resynthesize only
POST /api/v1/enrich/lead-session/synthesize-patch or MCP resynthesize_lead_graph
{
"lead": { "contact_id": 1737992709, "company": "Hoops & Gears and Clark" },
"chat_session_id": "ab62eb36cd134d70a58bc174d6b09185",
"prior_backfill": { "...": "current graph" },
"refresh_summary": true,
"prompts": ["Emphasize owner phones and legal contacts in the summary."]
}
No vendor spend — LLM resynthesis from the existing graph only. Hosted UI: Resynthesize in the hunt controls panel.
Summary history (hosted UI)
Each time a follow-up session, slot rerun, batch slot rerun, or synthesize-patch changes
summary_markdown, the server archives the prior version on
backfill.summary_history[] (with graph_fingerprint for phones/contacts/emails).
- Chat shows only the latest summary; older enrich turns collapse to a one-line stub.
- Summary history on the latest enrich turn opens a diff modal:
- Graph changes — added/removed fingerprint keys (primary view).
- Markdown diff — line-level prose comparison between any two versions.
Partial routes return the updated backfill including summary_history; no extra API.
Investigate unmerged finding
POST /api/v1/enrich/lead-session/finding-rerun or MCP investigate_unmerged_finding
{
"lead": { "contact_id": 1737992709, "company": "Hoops & Gears and Clark" },
"chat_session_id": "ab62eb36cd134d70a58bc174d6b09185",
"candidate_id": "a1b2c3d4e5f6g7h8",
"prior_backfill": { "unmerged_candidates": [{ "id": "a1b2c3d4e5f6g7h8", "label": "…" }] }
}
candidate_id is on backfill.unmerged_candidates[].id (stable SHA1 at merge time).
Hosted UI: Investigate on each row in Findings not merged.
Agent behavior checklist
- Read the user's latest message into
promptsif the model omitted it. - Copy the first
enrich_lead_sessionleadobject for every follow-up. - Never set
lead.companyto a discovered DBA/legal name unless the user pasted a new CRM export. - Explain
MULTI_LEAD_REFUSEDas "same chat, different lead shape" — fix by reusing boundcontact_id. - Do not re-run person hunt for a contact when
person_hunt_sections,whitepages_person_hunttrace, or person-hunt phones already record that contact (match or no-match). Use slot-person for intermediate search only, or synthesize-patch to refresh prose.
Related
- enrich_websearch_when — when to escalate to websearch
- bill_on_match — refunds on empty hunts
- filter_discipline_meta — tool choice before spending