Debugging Context Loss in Law Firm Client Handoffs With Hindsight
Suits Agent exists because paralegals kept apologizing to clients for something that was never their fault: forgetting.
Not forgetting because anyone on staff is careless. Forgetting because the information was never in one place to begin with. A law firm case isn't a single document — it's a scattered trail of intake calls, email threads, PDFs, court filings, and handoffs between an attorney and a paralegal who may no longer be on the matter by the time a client calls back. When that call comes in after a long gap, whoever picks up the phone is reconstructing a case from memory, and memory is exactly the thing that fails under pressure.
Suits Agent closes that gap by giving a law firm a persistent, queryable memory layer for every matter it handles.
Frontend Core Agent Engine Hindsight Engine
┌────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐
│ Dashboard │ │ Ingestion pipeline │ │ Retain │
│ Matter detail page │ ───▶ │ Handoff brief │ ───▶ │ (namespaced per │
│ Memory timeline │ │ synthesizer │ │ matter) │
│ Ingest / Ask chat │ ◀─── │ Recall orchestration │ ◀─── │ Recall │
└────────────────────┘ └───────────────────────┘ └───────────────────────┘
Requests from the UI hit the core agent engine, which either writes a new event into Hindsight (retain) or asks Hindsight for the relevant slice of case memory (recall). The engine never touches raw storage directly — every read and write to case memory goes through this boundary, which is what keeps matter data isolated and auditable.
What the System Does
Suits Agent is not an AI lawyer. It doesn't draft motions, doesn't predict case outcomes, and doesn't give legal advice. What it does is narrower and, in daily practice, more valuable: it remembers.
Every event tied to a case — an intake transcript, an attorney's note, a filing summary, a deposition recap — is written into memory as it happens. When someone on the team needs context, they don't dig through folders or wait on a colleague who might be out. They ask the system a question in plain language and get an answer grounded in everything retained about that specific matter.
The application is organized around a few core surfaces:
- A dashboard showing every active matter, with status flags for what's urgent — an upcoming hearing, a missing document.
- A matter detail page consolidating the client profile, case summary, timeline, deadlines, and open questions into one view.
- A memory timeline showing the chronological trail of everything the system has stored for that case.
- An ingest flow where staff submit call transcripts, email threads, or notes, committing them to memory in a single action.
- A handoff brief generator that synthesizes case state into a single-page summary for anyone picking up the matter cold.
- A chat interface for direct questions about a case — "what did we tell this client last time?" — answered strictly from what's on record.
None of it works without a memory layer that does two things reliably: absorb heterogeneous, messy case information as it arrives, and retrieve the relevant slice of it later — not a keyword match, a relevant one. That's the part the system doesn't build from scratch; it delegates to Hindsight.
The Core Technical Story: Retain and Recall
Suits Agent runs its memory layer on the Hindsight GitHub engine, and the architecture is organized around two operations: retain and recall.
Retain is the write path. Every time something case-relevant happens — a client sends an email, an attorney logs a note, a filing gets summarized — that content is pushed into Hindsight as a memory bound to the matter. Rather than maintaining a rigid schema for every input type (an intake transcript looks nothing like a one-line attorney note), retain acts as a normalization boundary: heterogeneous case data goes in without requiring a relational schema migration for every new source type.
Here's the retain path as a Next.js API route, backed by a typed Hindsight client:
// src/lib/hindsight.ts
import { HindsightClient } from "@vectorize-io/hindsight-client";
let realClient: HindsightClient | null = null;
const apiKey = process.env.HINDSIGHT_API_KEY;
const baseUrl =
process.env.HINDSIGHT_BASE_URL ||
"https://api.hindsight.vectorize.io";
if (apiKey) {
realClient = new HindsightClient({
apiKey,
baseUrl,
});
}
export async function retainMemory(
matterId: string,
content: string,
metadata: MemoryMetadata
): Promise<{ success: boolean; id: string; mock: boolean }> {
const dateStr =
metadata.date || new Date().toISOString().split("T")[0];
if (realClient) {
try {
await realClient.retain(
matterId,
`${content} [Source: ${metadata.sourceType}, Date: ${dateStr}]`
);
return {
success: true,
id: `real_${Date.now()}`,
mock: false,
};
} catch (error) {
console.error(
"Error writing memory to Hindsight API, falling back:",
error
);
}
}
// Local mock-mode fallback
const id = `mock_${Date.now()}`;
return {
success: true,
id,
mock: true,
};
}
// src/app/api/retain/route.ts
import { NextResponse } from "next/server";
import { retainMemory } from "@/lib/hindsight";
export async function POST(request: Request) {
try {
const body = await request.json();
const { matterId, content, metadata } = body;
if (!matterId || !content || !metadata) {
return NextResponse.json(
{
success: false,
error: "Missing required fields: matterId, content, or metadata",
},
{ status: 400 }
);
}
const result = await retainMemory(
matterId,
content,
metadata
);
return NextResponse.json({
success: true,
id: result.id,
mock: result.mock,
message: "Memory successfully retained in Hindsight memory bank.",
});
} catch (error: any) {
return NextResponse.json(
{
success: false,
error: error.message || "Failed to retain memory",
},
{ status: 500 }
);
}
}
Recall is the read path, and it's where the product delivers its value. When a paralegal opens a matter or submits a question through the chat interface, Suits Agent doesn't run a keyword search against a notes table — it issues a recall query against Hindsight, scoped to that matter's namespace, and gets back the memories relevant to the question being asked, per the retrieval model described in the Hindsight documentation.
// src/lib/hindsight.ts
export async function recallMemories(
matterId: string,
query: string
): Promise<MemoryItem[]> {
if (realClient) {
try {
const response = await realClient.recall(
matterId,
query
);
if (response && response.results) {
return response.results.map((r: any, idx: number) => ({
id: r.id || `recalled_${idx}`,
text: r.text || r.content,
score: r.score || 0.9,
metadata: {
matterId,
clientName:
matterId === "sarah-williams"
? "Sarah Williams"
: "Daniel Kim",
matterType:
matterId === "sarah-williams"
? "Employment Dispute"
: "Tenant Eviction Defense",
sourceType:
r.metadata?.sourceType || "System Seed",
date:
r.metadata?.date ||
new Date().toISOString().split("T")[0],
},
}));
}
} catch (error) {
console.error(
"Error recalling memories from Hindsight API, falling back:",
error
);
}
}
// Mock-mode retrieval remains scoped to the requested matter.
const memories = readMockDb();
return memories
.filter((item) => item.metadata.matterId === matterId)
.sort((a, b) => b.score - a.score);
}
The separation between retain as a normalization boundary and recall as a relevance boundary is what keeps Suits Agent from needing a custom retrieval layer bolted onto a plain vector store — indexing, chunking, and re-ranking are handled beneath the retain/recall interface rather than in application code.
Turning Recall into a Handoff Brief
A raw list of fifteen relevant memories isn't a handoff brief — it's still homework. The handoff brief generator takes recall output and structures it into the sections staff need before a client call: case summary, key dates, client concerns, prior advice given, missing documents, open questions, and a recommended next internal action.
// src/app/api/recall/route.ts
import { NextResponse } from "next/server";
import { recallMemories } from "@/lib/hindsight";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const matterId = searchParams.get("matterId");
const query = searchParams.get("query");
if (!matterId || !query) {
return NextResponse.json(
{
success: false,
error: "Missing matterId or query parameter",
},
{ status: 400 }
);
}
try {
const memories = await recallMemories(matterId, query);
return NextResponse.json({
success: true,
memories,
});
} catch (error: any) {
return NextResponse.json(
{
success: false,
error: error.message || "Failed to recall memories",
},
{ status: 500 }
);
}
}
Every generated brief keeps its source memories attached rather than hidden behind the summary. If an attorney reads "client advised not to contact former manager," they can trace it to the exact note, timestamp, and author behind it. Case memory that can't be traced to its source isn't trustworthy enough to hand to someone before a client call.
What This Looks Like in Practice
Consider a matter where a client is terminated in March, files an EEOC charge in April, raises a non-compete concern in May, and then goes quiet for six months. When she calls back in August, the attorney who ran her intake is unavailable, and a paralegal who has never touched the file picks up the phone.
Without a memory layer, that call starts with "can you remind me what happened?" — which, from the client's side, reads as being forgotten.
With Suits Agent, the paralegal opens the matter and generates a handoff brief. The response coming back from the API looks like this:
// src/app/api/recall/route.ts
import { NextResponse } from "next/server";
import { recallMemories } from "@/lib/hindsight";
import { generateBrief } from "@/lib/llm";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const matterId = searchParams.get("matterId");
if (!matterId) {
return NextResponse.json(
{ success: false, error: "Missing matterId parameter" },
{ status: 400 }
);
}
try {
// 1. Recall all relevant memories from Hindsight
const memories = await recallMemories(
matterId,
"Summarize case events, deadlines, timeline facts, instructions, concerns, and questions"
);
// 2. Synthesize memories into the structured HandoffBrief object
const brief = await generateBrief(matterId, memories);
return NextResponse.json({
success: true,
brief,
memories, // Source memories for UI citation display
});
} catch (error: any) {
console.error("Error generating handoff brief:", error);
return NextResponse.json(
{
success: false,
error: error.message || "Failed to generate brief",
},
{ status: 500 }
);
}
}
The frontend renders that payload as a single-page brief, and the call starts with "I see your mediation is coming up on September 9th, and the main thing we're waiting on is your signed employment agreement" instead of a blank stare.
That's the product. It's not flashy. It's the difference between a client who feels tracked across months and a client who feels like a stranger every time they call.
Lessons Learned
Separate the write path from the read path early. Retain and recall as distinct operations — instead of one generic "save this" call — forces a decision, at write time, about what each piece of content needs to be retrievable for later. That decision shapes almost every other part of the system.
Don't let the summary hide its sources. Every brief carries a visible link back to the raw memory it was built from. Skipping that traceability is faster to ship but means the output can't be trusted for something as consequential as a legal matter.
Scope memory per matter, not globally. A single memory pool across the firm is the wrong default. Every retain and recall call is namespaced to matter-${matterId}, which means a query about Sarah Williams' case can never surface a memory from Daniel Kim's eviction defense, even if the two cases share vocabulary or a common attorney. This matters for retrieval precision — a smaller, matter-scoped index returns cleaner results than one big pool — but it matters more for the obvious reason: firms cannot risk one client's confidential case history leaking into another client's brief, even accidentally. Namespace boundaries are the enforcement mechanism for that, not an afterthought bolted on with access-control checks later.
Resist scope creep toward legal advice. It's tempting to have the model suggest strategy once it has full case context — it has the dates, the client's stated concerns, and the attorney's prior instructions, so a plausible-sounding next step is one prompt away. Suits Agent draws that line explicitly, both in the system prompt passed to the brief synthesizer and in the product's own framing: it summarizes case memory, it does not advise. That constraint is enforced at the prompt layer, not left to hope, because the moment a memory tool starts opining on strategy, it stops being a memory tool and becomes something with a very different risk profile.
Build the chat interface last, not first. The "ask a question about this case" surface looks like the centerpiece, but it's a thin layer over recall that only becomes useful once retain, matter-scoping, and the brief generator are solid. Building it too early masks problems in the memory layer underneath it.
Any system that needs to remember unstructured, time-sensitive information and answer specific questions about it later — rather than just search it — runs into this same retain/recall split. It's worth understanding how stateful agent memory differs from a plain retrieval-augmented setup before building that layer from scratch.
Top comments (0)