Sitemap

Using Hindsight, My Agent Called Back Customers After the Fix

5 min readSep 30, 2026

--

Three weeks ago a customer told our support agent that CSV exports “take forever.” Last Tuesday the same agent phoned him, said the problem was fixed in release 2.3, and asked if he wanted a walkthrough of the upgrade. Nobody scheduled that call. A release event and a memory did.

That behavior is the part of LoopCall I care about most, so this post is about how it works and what broke while I built it.

Press enter or click to view image in full size

What the system does

LoopCall is one voice agent that runs in three modes against one shared memory:

  • Support: remembers each customer’s technical setup and how frustrated they’ve been across calls.
  • Sales: remembers which objections came up and which rebuttals moved a deal forward.
  • Follow-up: calls people back when something they reported has been fixed.

Memory lives in Hindsight, an open source agent memory system. I use three kinds of banks:

  • cust_{id} for one customer's technical facts, emotional history, and promises.
  • org_playbook for sales tactics and their outcomes.
  • org_product for feedback themes and releases.

Every call follows the same lifecycle. Before the call, I build a short brief from recalled memories. During the call, the agent mostly relies on that brief. After the call, an extraction job turns the transcript into structured facts and retains them. The Hindsight documentation covers the retain, recall, and reflect operations I’m building on, so I didn’t write a retrieval layer of my own.

The through-line: a release is a memory event

Most support tooling has a blind spot. A ticket closes when an engineer merges a fix, and the customer who complained finds out only if they happen to read the changelog.

I wanted the opposite. A shipped fix should reach the people who asked for it, with the context they gave us.

Three pieces make that work:

  1. After every call, extraction tags feedback with a canonical theme such as export_slowness. Mentions, sentiment, and affected customers roll up per theme.
  2. When we publish a release, we record which themes it fixes and retain that fact into org_product.
  3. Publishing also joins the release against open issues and creates a prioritized callback queue.

Here is the release handler, trimmed:

python

def publish_release(version: str, fixed_themes: list[str]) -> list[CallbackTask]:
db.save_release(version, fixed_themes)
memory.retain(
bank="org_product",
content=f"Release {version} addressed themes: {', '.join(fixed_themes)}.",
tags=["release"],
)
    tasks = []
for theme in fixed_themes:
for issue in db.open_issues(theme=theme):
tasks.append(CallbackTask(
customer_id=issue.customer_id,
release=version,
theme=theme,
priority=issue.frustration * issue.account_arr,
))
return sorted(tasks, key=lambda t: t.priority, reverse=True)

Priority is frustration times account value, so the most upset high-value customer gets the first call. It’s a blunt heuristic. I’d rather have a blunt heuristic I can explain than a clever one I can’t.

Get Praseedhabairy’s stories in your inbox

Join Medium for free to get updates from this writer.

Clicking “Call now” on a task starts a call in follow-up mode. The release, the theme, and the customer’s original complaint all go into the brief.

Building the pre-call brief

The brief is where memory either shows up or doesn’t. I run a handful of focused recalls in parallel, each with a hard timeout, then compress the results into five to eight lines.

python

async def build_brief(customer_id: str, memory_on: bool) -> Brief:
if not memory_on:
return Brief.empty()
    bank = f"cust_{customer_id}"
queries = {
"tech": "technical environment and fixes that worked or failed",
"emotion": "frustration trajectory and communication preferences",
"commitments": "open promises made by us or the customer",
}
results = await asyncio.gather(
*(memory.recall(bank, q, timeout=2.0) for q in queries.values())
)
return Brief.compose(dict(zip(queries, results)))

I do recall before the call instead of letting the model search during it. A live conversation has no room for a two-second retrieval stall, and a short brief is easier to audit. The UI shows the raw recalled items next to the transcript, so I can see exactly which memory shaped which sentence.

Before and after

Here is the same customer, a synthetic account from our seeded data, with memory off:

Agent: Thanks for calling. What can I help you with today? Daniel: Exports. Again. Agent: I’m sorry to hear that. Which version are you on, and what error do you see?

And with memory on, in follow-up mode:

Agent: Hi Daniel, quick note that this call is recorded to improve our service. On the 4th you told us CSV exports were timing out on your v2.1 setup. Release 2.3 includes a fix for that. Want me to walk you through the upgrade?

The second agent doesn’t ask Daniel to repeat anything. It also knows the last restart attempt didn’t help, so it doesn’t suggest one.

Press enter or click to view image in full size

What was painful

Theme clustering drifts. Early on, export_slowness, slow_csv_download, and report_timeout became three separate themes, which split the signal and delayed the callback queue. I fixed it by having extraction choose from a canonical label list and only propose a new label when nothing fits. A human approves new labels.

Structured output fails on fast models. The Groq-hosted models I use are quick, but function calling and JSON output occasionally break. Extraction now validates against a Pydantic model, tries one repair pass, falls back to a second model, and finally stores a degraded record rather than dropping the call.

Contradictions happen. A customer will say the problem is back after we told them it was fixed. The brief builder adds an explicit CONFLICT line when new statements disagree with recalled ones, and the agent has to acknowledge both facts instead of picking one.

Lessons

  1. Treat external events as memory inputs. Conversations aren’t the only thing worth remembering. A release is an event, and joining it with past conversations is what made the callback possible.
  2. Build the brief ahead of time. Pre-call recall keeps latency predictable and makes behavior easier to debug.
  3. Write memories as standalone sentences. “Daniel’s v2.1 export timed out; batch size 500 fixed it” recalls far better than a field dump.
  4. Keep an off switch. A per-session memory toggle let me compare the same scenario with and without memory, which beat any explanation I could write.

The agent memory overview from Vectorize is a good primer if you’re deciding what belongs in memory and what belongs in a database. My rule ended up simple: narrative context goes in Hindsight, and anything I need to count goes in SQL.

The repository is at https://github.com/PraseedhaB/loopcall.git . If you try the same idea, I’d start with the release-to-callback join. It’s the smallest piece that makes memory visibly useful.

--

--