<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bhuvaneswari</title>
    <description>The latest articles on DEV Community by Bhuvaneswari (@bhuvaneswari10).</description>
    <link>https://dev.to/bhuvaneswari10</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4148717%2F745761d7-595f-401d-9d77-fb2f231aa851.png</url>
      <title>DEV Community: Bhuvaneswari</title>
      <link>https://dev.to/bhuvaneswari10</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bhuvaneswari10"/>
    <language>en</language>
    <item>
      <title>The Most Dangerous Line in a Sales Handoff Is the Promise Nobody Can Find</title>
      <dc:creator>Bhuvaneswari</dc:creator>
      <pubDate>Tue, 29 Sep 2026 08:13:01 +0000</pubDate>
      <link>https://dev.to/bhuvaneswari10/the-most-dangerous-line-in-a-sales-handoff-is-the-promise-nobody-can-find-111m</link>
      <guid>https://dev.to/bhuvaneswari10/the-most-dangerous-line-in-a-sales-handoff-is-the-promise-nobody-can-find-111m</guid>
      <description>&lt;p&gt;I started Waada with a simple observation: the most expensive piece of account history is often not a major decision. It is a small promise buried somewhere in the record.&lt;/p&gt;

&lt;p&gt;A date someone committed to. A document someone said they would send. A pricing issue that was already resolved. A change in the go-live plan that never made it into the CRM.&lt;/p&gt;

&lt;p&gt;I built the system around recovering that context without asking the incoming rep to reread an entire account.&lt;/p&gt;

&lt;p&gt;Why promises disappear&lt;br&gt;
Sales systems are optimized around records.&lt;/p&gt;

&lt;p&gt;People are not.&lt;br&gt;
An account might have CRM fields, dozens of emails, Slack conversations, meeting transcripts, and call notes. The CRM might say that an opportunity is in a particular stage. The messages contain the details that explain why.&lt;/p&gt;

&lt;p&gt;That creates a strange information problem.&lt;br&gt;
The data exists.&lt;br&gt;
The problem is retrieval.&lt;br&gt;
A new account owner doesn't usually ask:&lt;br&gt;
Give me a complete summary of everything that happened.&lt;br&gt;
They ask:&lt;br&gt;
What are we still waiting on?&lt;br&gt;
Or:&lt;br&gt;
Did we already resolve pricing?&lt;br&gt;
Or:&lt;br&gt;
What changed after the July discussion?&lt;br&gt;
Those questions require different pieces of history.&lt;br&gt;
That is the problem Waada tries to solve.&lt;br&gt;
The pipeline is intentionally explicit&lt;br&gt;
The repository is a TypeScript pnpm workspace with packages/core and apps/web.&lt;/p&gt;

&lt;p&gt;The core package owns the data model, parsers, ingestion, Hindsight adapter, LLM layer, and agent functions. The web application exposes import, briefing, commitments, Ask, comparison, settings, and pipeline views.&lt;/p&gt;

&lt;p&gt;The ingestion path is:&lt;br&gt;
files&lt;br&gt;
  -&amp;gt; parseFiles()&lt;br&gt;
  -&amp;gt; Interaction[]&lt;br&gt;
  -&amp;gt; ingest()&lt;br&gt;
  -&amp;gt; local persistence&lt;br&gt;
  -&amp;gt; Hindsight retain()&lt;br&gt;
The normalized model is the key:&lt;br&gt;
Interaction = {&lt;br&gt;
  account: string;&lt;br&gt;
  sourceId: string;&lt;br&gt;
  type: "call" | "email" | "slack" | "meeting" | "note";&lt;br&gt;
  date: string;&lt;br&gt;
  title: string;&lt;br&gt;
  participants: string[];&lt;br&gt;
  content: string;&lt;br&gt;
  source: "...";&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;I don't want the commitment logic to care whether the promise came from .eml, Slack JSON, or a transcript.&lt;br&gt;
That knowledge belongs in the parser.&lt;/p&gt;

&lt;p&gt;Once everything becomes an Interaction, the rest of the application can operate on one timeline.&lt;/p&gt;

&lt;p&gt;I used Hindsight because the history needed to survive the current prompt&lt;br&gt;
The memory layer is isolated under packages/core/src/memory/.&lt;br&gt;
Each account gets a Hindsight bank derived from its slug:&lt;br&gt;
const bankId = bankIdFor(account);&lt;br&gt;
An imported interaction is retained with its content, date, context, document ID, and metadata.&lt;/p&gt;

&lt;p&gt;The Hindsight adapter handles retain, recall, retries, and mapping external results into Waada's MemoryHit type.&lt;/p&gt;

&lt;p&gt;The Hindsight GitHub repository and Hindsight documentation describe the underlying memory system.&lt;/p&gt;

&lt;p&gt;For this application, the important distinction is between retention and retrieval.&lt;br&gt;
Retention says:&lt;br&gt;
This interaction belongs to this account's history.&lt;br&gt;
Recall says:&lt;br&gt;
Given what I'm trying to understand now, which pieces of that history should I see?&lt;br&gt;
That second operation is where the commitment ledger starts to become interesting.&lt;br&gt;
I made “open commitment” a retrieval problem&lt;br&gt;
A naïve implementation would store commitments as they are discovered:&lt;br&gt;
commitment -&amp;gt; status=open&lt;br&gt;
That looks convenient.&lt;br&gt;
It is also dangerous.&lt;br&gt;
The source history may later contain the evidence that the commitment was fulfilled.&lt;br&gt;
So I chose to derive commitment state from recalled evidence.&lt;br&gt;
commitmentLedger() searches for promise-oriented evidence, deduplicates it, extracts structured commitments, and sorts them.&lt;br&gt;
The conceptual flow is:&lt;br&gt;
const evidence = await recallPromiseEvidence(account);&lt;br&gt;
const uniqueEvidence = chunkAndDeduplicate(evidence);&lt;br&gt;
const commitments = await extractCommitments(uniqueEvidence);&lt;/p&gt;

&lt;p&gt;return sortLedger(commitments);&lt;br&gt;
The sorting logic puts open items ahead of unclear and delivered items. Open items are then prioritized around deadlines.&lt;br&gt;
That gives the incoming owner a practical view:&lt;br&gt;
OPEN&lt;br&gt;
  overdue promise&lt;br&gt;
  upcoming promise&lt;br&gt;
  undated promise&lt;/p&gt;

&lt;p&gt;UNCLEAR&lt;br&gt;
  ...&lt;/p&gt;

&lt;p&gt;DELIVERED&lt;br&gt;
  ...&lt;br&gt;
The important thing is that the list is reconstructed from history.&lt;/p&gt;

&lt;p&gt;If the source history changes, the interpretation can change.&lt;br&gt;
The September 2 promise is more useful than a generic summary&lt;br&gt;
The Acme scenario gives the system a specific expected behavior: an outstanding September 2 SOC 2 commitment should lead the brief.&lt;br&gt;
That is a useful test because it forces the system to preserve three things:&lt;br&gt;
what was promised,&lt;br&gt;
who was involved,&lt;br&gt;
when it was due.&lt;/p&gt;

&lt;p&gt;A generic summary could mention the SOC 2 discussion without making the outstanding obligation obvious.&lt;/p&gt;

&lt;p&gt;The ledger turns it into an operational item.&lt;br&gt;
This is why I think the phrase “AI summary” undersells what the system is doing.&lt;/p&gt;

&lt;p&gt;The useful output is not simply shorter text.&lt;br&gt;
It is structured interpretation of remembered evidence.&lt;br&gt;
The opposite problem is the resolved objection&lt;br&gt;
The second important object is a landmine.&lt;/p&gt;

&lt;p&gt;I wanted to preserve decisions, not just unfinished work.&lt;br&gt;
A resolved pricing objection can be more important than an open task if the incoming rep is about to reopen it without knowing what was already negotiated.&lt;/p&gt;

&lt;p&gt;The landmine flow recalls objections, sensitive topics, and accepted agreements. The LLM extracts the topic, previous discussion, resolution, date, guidance, and source.&lt;/p&gt;

&lt;p&gt;The result is a warning that says, in effect:&lt;br&gt;
This topic has history. Read it before you reopen it.&lt;br&gt;
The Acme scenario expects the resolved pricing issue to appear here.&lt;/p&gt;

&lt;p&gt;That gives the handoff two kinds of continuity:&lt;br&gt;
commitment -&amp;gt; remember what still needs doing&lt;br&gt;
landmine   -&amp;gt; remember what was already settled&lt;br&gt;
I found that distinction more useful than trying to make everything fit into a generic “important facts” section.&lt;br&gt;
The brief combines several memory questions&lt;br&gt;
brief() runs the commitment ledger and landmine extraction, then recalls stakeholder and recent-change evidence before sending bounded context to the LLM.&lt;/p&gt;

&lt;p&gt;The architecture is closer to a small query planner than to a single summarization call:&lt;br&gt;
account memory&lt;br&gt;
                       |&lt;br&gt;
       +---------------+---------------+&lt;br&gt;
       |               |               |&lt;br&gt;
       v               v               v&lt;br&gt;
    promises       objections       timeline&lt;br&gt;
       |               |               |&lt;br&gt;
       v               v               v&lt;br&gt;
    ledger          landmines      recent changes&lt;br&gt;
       \               |               /&lt;br&gt;
        +--------------+--------------+&lt;br&gt;
                       |&lt;br&gt;
                       v&lt;br&gt;
                     brief&lt;br&gt;
This is also where Hindsight's role becomes concrete. The memory system is not responsible for writing the final brief. It supplies relevant historical evidence.&lt;/p&gt;

&lt;p&gt;The LLM is responsible for turning that evidence into a readable result.&lt;br&gt;
I kept those responsibilities separate.&lt;/p&gt;

&lt;p&gt;Question answering is retrieval with a user-supplied query&lt;br&gt;
The Ask path makes the design even simpler.&lt;br&gt;
Suppose the rep asks:&lt;br&gt;
What changed since July?&lt;br&gt;
The system uses that question as the retrieval query, bounds the recalled excerpts, and gives the resulting evidence to the model.&lt;br&gt;
const hits = await memory.search(account, question);&lt;br&gt;
const evidence = capEvidence(hits);&lt;/p&gt;

&lt;p&gt;return llm.chat({&lt;br&gt;
  question,&lt;br&gt;
  evidence,&lt;br&gt;
});&lt;br&gt;
The answer includes source contexts and document IDs.&lt;br&gt;
A successful live evaluation recorded the expected Q3-to-Q4 go-live change for the Acme question.&lt;/p&gt;

&lt;p&gt;That interaction is important because it demonstrates why I didn't want a single static summary.&lt;/p&gt;

&lt;p&gt;The question itself changes the required context.&lt;br&gt;
I had to make memory cheaper than “read everything”&lt;br&gt;
Persistent memory creates a second problem: retrieval can return more evidence than the model should consume.&lt;/p&gt;

&lt;p&gt;The LLM layer therefore uses a shared prompt budget of 5,000 tokens, represented through a conservative character limit. Evidence is bounded and chunked.&lt;br&gt;
The practical flow becomes:&lt;br&gt;
recall&lt;br&gt;
  -&amp;gt; deduplicate&lt;br&gt;
  -&amp;gt; select&lt;br&gt;
  -&amp;gt; chunk&lt;br&gt;
  -&amp;gt; budget&lt;br&gt;
  -&amp;gt; generate&lt;br&gt;
I also made the brief legs sequential because the provider environment has shared rate limits.&lt;/p&gt;

&lt;p&gt;That was painful at first. Parallel requests look cleaner in code.&lt;/p&gt;

&lt;p&gt;But if several LLM calls share a tight token-per-minute limit, “more parallel” can simply mean “hit the limit faster.”&lt;br&gt;
The implementation chooses controlled sequencing where necessary.&lt;/p&gt;

&lt;p&gt;The LLM cannot be trusted with application state&lt;br&gt;
I treat structured model output as external input.&lt;/p&gt;

&lt;p&gt;The extraction layer attempts structured output, validates through Zod, retries with repair guidance, then attempts plain JSON parsing. Invalid results can safely become null.&lt;br&gt;
const parsed = schema.safeParse(modelOutput);&lt;/p&gt;

&lt;p&gt;if (parsed.success) {&lt;br&gt;
  return parsed.data;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;const repaired = await repair(modelOutput);&lt;/p&gt;

&lt;p&gt;return schema.safeParse(repaired).success&lt;br&gt;
  ? schema.parse(repaired)&lt;br&gt;
  : null;&lt;br&gt;
That matters because a model-generated commitment object eventually influences the user interface.&lt;/p&gt;

&lt;p&gt;I don't want malformed JSON becoming a fake promise.&lt;br&gt;
The same boundary discipline appears in the ingestion layer and server functions.&lt;br&gt;
I kept comparison paths independent&lt;br&gt;
Waada has a comparison view with CRM-only, raw-summary-only, and memory-aware outputs.&lt;br&gt;
The CRM baseline reads only .waada/crm/.json.&lt;br&gt;
The summary baseline reads normalized interactions chronologically and caps them at 12,000 characters. It does not call Hindsight.&lt;/p&gt;

&lt;p&gt;The memory-aware path can add account-specific recalled evidence.&lt;br&gt;
I intentionally made the baselines inspectable instead of presenting an opaque “AI versus old system” claim.&lt;/p&gt;

&lt;p&gt;The evaluation record also contains degraded runs: structured-output variability, provider rate-limit pressure, intermittent Hindsight Cloud failures, and a run where the summary-only baseline scored higher on its checks.&lt;/p&gt;

&lt;p&gt;That is exactly why I don't treat the comparison as a benchmark.&lt;br&gt;
It is a context comparison.&lt;br&gt;
The real lesson was about information shape&lt;br&gt;
Building Waada changed how I think about sales history.&lt;/p&gt;

&lt;p&gt;The problem isn't that there isn't enough data.&lt;br&gt;
There is usually too much.&lt;br&gt;
The hard part is shaping that history around the question the next person needs answered.&lt;br&gt;
That led to several rules I would reuse elsewhere:&lt;br&gt;
Normalize first&lt;br&gt;
Every source should become one canonical interaction model before memory or reasoning starts.&lt;/p&gt;

&lt;p&gt;Make memory boundaries explicit&lt;br&gt;
An account should map to its own memory bank.&lt;br&gt;
Preserve time&lt;br&gt;
Sales questions are frequently temporal. “What changed?” is impossible to answer well without trustworthy dates.&lt;/p&gt;

&lt;p&gt;Keep source history authoritative&lt;br&gt;
Derived states such as commitment status should be reconstructable from evidence.&lt;br&gt;
Budget retrieval&lt;br&gt;
More memory is not automatically better context.&lt;br&gt;
Treat models as untrusted&lt;br&gt;
Validate every structured result before using it.&lt;/p&gt;

&lt;p&gt;The architecture I would keep&lt;br&gt;
The final shape is straightforward:&lt;br&gt;
TanStack Start&lt;br&gt;
      |&lt;br&gt;
      v&lt;br&gt;
server functions&lt;br&gt;
      |&lt;br&gt;
      v&lt;br&gt;
@waada/core&lt;br&gt;
  |       |       |&lt;br&gt;
  v       v       v&lt;br&gt;
ingest   memory   LLM&lt;br&gt;
           |&lt;br&gt;
           v&lt;br&gt;
       Hindsight&lt;br&gt;
That simplicity is intentional.&lt;br&gt;
The web layer doesn't know how Hindsight works.&lt;br&gt;
The agent layer doesn't know how email parsing works.&lt;br&gt;
The memory adapter doesn't know how the brief is rendered.&lt;br&gt;
Each layer has one job.&lt;/p&gt;

&lt;p&gt;And the result is a system that can preserve something a normal summary struggles with: not just what happened, but enough of what happened to answer the next question correctly.&lt;br&gt;
That is the reason I built it this way.&lt;/p&gt;

&lt;p&gt;The hardest part of a handoff isn't writing the summary.&lt;br&gt;
It's finding the promise.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aiagents</category>
      <category>softwaredevelopment</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
