<?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: Shoban Ravichandran</title>
    <description>The latest articles on DEV Community by Shoban Ravichandran (@lunatic_minion).</description>
    <link>https://dev.to/lunatic_minion</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%2F4192598%2Fae4c72ba-d57e-4dcc-9413-b9f9bc9cef95.jpg</url>
      <title>DEV Community: Shoban Ravichandran</title>
      <link>https://dev.to/lunatic_minion</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lunatic_minion"/>
    <language>en</language>
    <item>
      <title>Ninaivu: one memory for a team of AI support agents, built on FalkorDB</title>
      <dc:creator>Shoban Ravichandran</dc:creator>
      <pubDate>Sun, 18 Oct 2026 18:25:57 +0000</pubDate>
      <link>https://dev.to/lunatic_minion/ninaivu-one-memory-for-a-team-of-ai-support-agents-built-on-falkordb-1f15</link>
      <guid>https://dev.to/lunatic_minion/ninaivu-one-memory-for-a-team-of-ai-support-agents-built-on-falkordb-1f15</guid>
      <description>&lt;h1&gt;
  
  
  Ninaivu: one memory for a team of AI support agents
&lt;/h1&gt;

&lt;p&gt;Oct 18, 2026 · @Shoban · posted for dev.to&lt;/p&gt;

&lt;h2&gt;
  
  
  Support bots forget, and that is the whole problem
&lt;/h2&gt;

&lt;p&gt;You message support about a broken order. The bot asks for your order number. A human takes over and asks again. A week later you follow up, and a third person asks for it a third time. Nobody can tell you who promised the refund, or why.&lt;/p&gt;

&lt;p&gt;The model is rarely the weak link here. Memory and coordination are. Each agent sees a slice of the conversation, nothing survives the session, and a handoff is a copy-paste of chat history that the next agent has to reread.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Graph Hacks: Context for AI Agents&lt;/strong&gt; (WeMakeDevs × FalkorDB), I built &lt;strong&gt;Ninaivu&lt;/strong&gt; (நினைவு, Tamil for "memory") for Track 02, Agent Memory and Coordination. It gives a team of support agents one shared memory, and that memory is a graph.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://ninaivu-ai.vercel.app" rel="noopener noreferrer"&gt;ninaivu-ai.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/shobanravichandran/ninaivu" rel="noopener noreferrer"&gt;github.com/shobanravichandran/ninaivu&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demo video:&lt;/strong&gt; &lt;a href="https://youtu.be/T6a-N7P-nZ0?si=8H-J5XOh3kJc9FEp" rel="noopener noreferrer"&gt;https://youtu.be/T6a-N7P-nZ0?si=8H-J5XOh3kJc9FEp&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The rule: agents never share a chat
&lt;/h2&gt;

&lt;p&gt;Ninaivu runs three support agents, &lt;strong&gt;Triage&lt;/strong&gt;, &lt;strong&gt;Billing&lt;/strong&gt; and &lt;strong&gt;Tech&lt;/strong&gt;, plus a &lt;strong&gt;Memory Curator&lt;/strong&gt; that tidies up after each session. The design rests on one rule: the agents never see each other's conversations. Everything they know about a customer, and everything they hand to each other, goes through FalkorDB.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Who owns an issue&lt;/strong&gt; is an &lt;code&gt;ASSIGNED_TO&lt;/code&gt; edge. On every customer message the backend reads it to decide which agent replies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A handoff&lt;/strong&gt; is a &lt;code&gt;Handoff&lt;/code&gt; node with a reason and a context note, written in the same query that moves ownership.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What we know about the customer&lt;/strong&gt; is a set of &lt;code&gt;Fact&lt;/code&gt; nodes with embeddings, validity dates and a &lt;code&gt;SUPERSEDES&lt;/code&gt; history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which fix works&lt;/strong&gt; is learned: every playbook attempt becomes a &lt;code&gt;TRIED {success}&lt;/code&gt; edge that feeds a success rate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The FastAPI backend keeps no state of its own. Routing, history, memory and handoffs are read from the graph on every turn, so if you take FalkorDB away, no agent can say a word. That was my test for the hackathon rule that the graph must do real work, not decorate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory as a graph
&lt;/h2&gt;

&lt;p&gt;Each kind of memory the track asks for maps onto nodes and edges that one Cypher query can walk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwj7t0ny7c8g0ycxngp20.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwj7t0ny7c8g0ycxngp20.png" alt="Ninaivu graph model: customers, sessions, messages, issues, facts, playbooks, agents, handoffs and orders, one graph per tenant" width="799" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read it from the customer outward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Episodic memory:&lt;/strong&gt; &lt;code&gt;Session&lt;/code&gt; and &lt;code&gt;Message&lt;/code&gt; nodes, chained with &lt;code&gt;NEXT&lt;/code&gt;, plus a two-sentence summary the curator writes when a chat ends.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic memory:&lt;/strong&gt; &lt;code&gt;Fact&lt;/code&gt; nodes, each embedded for vector search, linked to the session it was learned in (&lt;code&gt;LEARNED_IN&lt;/code&gt;), with old versions kept behind a &lt;code&gt;SUPERSEDES&lt;/code&gt; edge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Procedural memory:&lt;/strong&gt; &lt;code&gt;Playbook&lt;/code&gt; nodes whose ranking comes from every &lt;code&gt;TRIED {success}&lt;/code&gt; attempt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared state:&lt;/strong&gt; the &lt;code&gt;Issue&lt;/code&gt; in the middle, with exactly one &lt;code&gt;ASSIGNED_TO&lt;/code&gt; owner, which every agent coordinates on.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Four queries that do the real work
&lt;/h2&gt;

&lt;p&gt;Every query lives in its own &lt;code&gt;.cypher&lt;/code&gt; file and is loaded by name, so the code, the README and the app's Cypher panel all show the same text. There are 31 of them. Four carry the design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Routing.&lt;/strong&gt; Before any model runs, one query decides who answers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Customer&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$cid&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:RAISED&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;i:&lt;/span&gt;&lt;span class="n"&gt;Issue&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:ASSIGNED_TO&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;a:&lt;/span&gt;&lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;i.status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'resolved'&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;i.id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;issue_id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a.name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i.opened_at&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;opened_at&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;opened_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No router model and no session state in the backend. If nobody owns an open issue, Triage answers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Handoff.&lt;/strong&gt; Ownership moves and the note is written in one atomic query. Only the current owner can hand off: anyone else matches zero rows, so a stale agent can't steal an issue.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Customer&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$cid&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:RAISED&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;i:&lt;/span&gt;&lt;span class="n"&gt;Issue&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$iid&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;r:&lt;/span&gt;&lt;span class="n"&gt;ASSIGNED_TO&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;from:&lt;/span&gt;&lt;span class="n"&gt;Agent&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;$from_agent&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;to:&lt;/span&gt;&lt;span class="n"&gt;Agent&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;$to_agent&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:ASSIGNED_TO&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;since:&lt;/span&gt; &lt;span class="n"&gt;$now&lt;/span&gt;&lt;span class="ss"&gt;}]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;h:&lt;/span&gt;&lt;span class="n"&gt;Handoff&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$hid&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;reason:&lt;/span&gt; &lt;span class="n"&gt;$reason&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;context:&lt;/span&gt; &lt;span class="n"&gt;$context&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;ts:&lt;/span&gt; &lt;span class="n"&gt;$now&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:FROM&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:TO&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="ss"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:FOR&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;h.id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;handoff_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claiming a new issue works the same way: assign it only &lt;code&gt;WHERE NOT (i)-[:ASSIGNED_TO]-&amp;gt;()&lt;/code&gt;. In a test, ten threads claim the same issue at the same moment, and exactly one wins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Recall by meaning, kept honest by the graph.&lt;/strong&gt; A vector search finds facts close to what the customer just said. The graph then keeps only this customer's facts, and only the current version of each.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;db.idx.vector.queryNodes&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Fact'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'embedding'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vecf32&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;$qvec&lt;/span&gt;&lt;span class="ss"&gt;))&lt;/span&gt; &lt;span class="k"&gt;YIELD&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Customer&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$cid&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:HAS_FACT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;node.valid_to&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;node.key&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node.text&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The landing page has a live box ("Ask Arjun's memory") that runs exactly this query, so you can try questions like "how should we contact him" and see the answer and the milliseconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Learned playbooks.&lt;/strong&gt; The same idea ranks fixes: find the playbooks that match the symptom by meaning, keep the near-equal matches, then pick the one with the best track record. Every attempt writes a &lt;code&gt;TRIED {success}&lt;/code&gt; edge, so the ranking shifts as the team learns.&lt;/p&gt;

&lt;p&gt;One more detail makes isolation hold by construction: the agents' tools are Python closures bound to the tenant and the customer on the server. The model never passes a customer id, so it has no way to address someone else's memory. Each company also gets its own graph (&lt;code&gt;tenant_acme&lt;/code&gt;, &lt;code&gt;tenant_zenfiber&lt;/code&gt;) inside one FalkorDB instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  One customer, two agents, a week apart
&lt;/h2&gt;

&lt;p&gt;The demo follows Meena, a fictional customer of a fictional store. All the data is synthetic.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Meena writes: &lt;em&gt;"My order A-1001 arrived today and the earbuds case is cracked."&lt;/em&gt; Nobody owns an issue for her, so &lt;strong&gt;Triage&lt;/strong&gt; answers. It opens an issue linked to the order and hands it to Billing with a note: damaged delivery, order already linked, she hasn't chosen refund or replacement yet.&lt;/li&gt;
&lt;li&gt;In the same request, the backend reads the graph again, sees Billing now owns the issue, and runs &lt;strong&gt;Billing&lt;/strong&gt; with that note. Billing checks the order (Aurora Wireless Earbuds, ₹3,499) and finds the playbook with the best record for damaged items.&lt;/li&gt;
&lt;li&gt;Meena describes the crack, accepts a refund and adds: &lt;em&gt;"Please send updates on WhatsApp, not email."&lt;/em&gt; Billing starts the refund, stores &lt;code&gt;contact_preference&lt;/code&gt; as a fact, records that the playbook worked, and resolves the issue.&lt;/li&gt;
&lt;li&gt;She ends the chat. The &lt;strong&gt;Memory Curator&lt;/strong&gt; writes a two-sentence session summary and keeps only the facts that will still be true next month.&lt;/li&gt;
&lt;li&gt;I move the demo clock forward seven days. Meena writes: &lt;em&gt;"Hi again, any update?"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triage&lt;/strong&gt; answers, an agent that never saw the first conversation. It tells her the ₹3,499 refund was started, gives the expected date, and confirms updates will come on WhatsApp. It doesn't ask for the order number.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  History from the first click
&lt;/h2&gt;

&lt;p&gt;A memory demo with empty memory isn't much of a demo, so the seed data writes 19 past sessions across 14 customers, through the same code paths the agents use. That gives the graph some real situations to show:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An issue that's still open.&lt;/strong&gt; Ayushman's refund is owned by Billing. When he asks "any update on my refund?", Triage is skipped entirely and Billing answers directly, because the graph says Billing owns it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customers who moved.&lt;/strong&gt; Arjun lived in Coimbatore and wanted phone calls; tell the agents he moved to Bengaluru and prefers WhatsApp, and the new facts supersede the old ones. The old versions stay, closed with an end date, so you can always see what was true and when.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A playbook that failed first.&lt;/strong&gt; One ZenFiber customer's Wi-Fi didn't come back after a restart, and a channel change fixed it. Both attempts are &lt;code&gt;TRIED&lt;/code&gt; edges, so the ranking reflects what actually worked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app shows all of this live: a chat with a marker wherever one agent hands over to the next, a memory panel with old facts crossed out, a graph view that grows as the agents write, a trace of every tool call, and every Cypher query the turn ran, with timings and source.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke, and what I learned
&lt;/h2&gt;

&lt;p&gt;The first end-to-end run worked, which mostly meant the bugs got more interesting.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Relevance has to come before track record.&lt;/strong&gt; My first playbook query took the ten nearest playbooks, nearly all of them, and sorted purely by success rate. "My earbuds won't connect" came back with the app-login playbook because it had a better record. Printing the cosine distances showed near-duplicate playbooks sit within about 0.04 of each other, while unrelated ones sit far away. Now the query keeps matches within 0.05 of the best one, then ranks by success.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents follow playbooks literally.&lt;/strong&gt; The damaged-item playbook says "ask for a photo", but the chat can't receive files, so Billing waited for a photo forever. One prompt line fixed it: ask the customer to describe it instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A curator stores too much.&lt;/strong&gt; On its first run it saved the customer's name and an "open refund request" as facts. A fact is now defined as something still true next month, and issue state stays on issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anchor every write on the customer.&lt;/strong&gt; My first claim and handoff queries matched an issue by id alone. If the model had guessed another customer's issue id, the query would have run. Both now start from &lt;code&gt;(:Customer {id: $cid})&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The smallest model that works is the right one.&lt;/strong&gt; I started on Claude Opus 5.5. But once the graph does the remembering, the agents' reasoning is light: read the context, call a tool, reply. Switching to Claude Haiku 5.5 cut the cost of a turn by about 100× (from about $0.05 to about $0.001) and made it faster. Its first run sent a damaged item to Tech instead of Billing; one clearer routing line in the Triage prompt fixed that. The model is one environment variable, so going back is trivial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency is geography.&lt;/strong&gt; From a laptop in India, each query took about 230 ms to reach us-east-1, so one agent turn spent around 7 seconds just waiting on the network. Deployed on Vercel next to FalkorDB Cloud, the same 32 queries take about 40 ms in total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure the database, not your connection pool.&lt;/strong&gt; My first load test reported a 12-second median for starting a session. That was 50 coroutines queuing for 16 connections. FalkorDB reports its own execution time on every result, and that is the number I publish now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Even the cost cap belongs in the graph.&lt;/strong&gt; The demo is public and every message calls a paid API, so there's a daily cap on agent turns. The backend is stateless and serverless, so the counter lives in FalkorDB as one atomic &lt;code&gt;MERGE ... SET&lt;/code&gt; query that every instance shares.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;FalkorDB answers the memory queries in well under a millisecond, and a full agent turn costs about a tenth of a cent.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Load test: 50 concurrent customers, 1,912 queries&lt;/td&gt;
&lt;td&gt;FalkorDB time p50 0.18 ms, p99 0.59 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector recall (vector search + graph filter)&lt;/td&gt;
&lt;td&gt;p50 0.26 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Race: 10 agents claim one issue at the same moment&lt;/td&gt;
&lt;td&gt;exactly 1 winner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolation: same customer id in two tenant graphs&lt;/td&gt;
&lt;td&gt;no shared memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One production agent turn&lt;/td&gt;
&lt;td&gt;about 32 queries, about 40 ms in FalkorDB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude cost per customer turn&lt;/td&gt;
&lt;td&gt;about $0.001 (Haiku 5.5)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The load test makes no model calls: it runs the same named queries the agents use, from 50 simulated customers at once. There are also 25 automated tests, 7 of which run every query against a live FalkorDB instance. Every Claude call logs its token cost, and the app shows the cost of each turn in its top bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built in under two days, with an AI pair programmer
&lt;/h2&gt;

&lt;p&gt;I built Ninaivu with Claude Code as a pair programmer, and I say so up front because the rules ask for it. The split worked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;I decided.&lt;/strong&gt; I picked the track and the idea, reviewed and approved a written plan before any real code, switched the frontend from Streamlit to React on Vercel, and set up FalkorDB Cloud, the API keys, GitHub and Vercel myself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude Code built.&lt;/strong&gt; Under that plan it wrote most of the code, the Cypher, the tests and the docs, then ran the tests, the seeding, the load test and the deploy checks, and reported what broke.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I kept it honest.&lt;/strong&gt; Every API run reported its cost, nothing was committed without my request, and secrets stayed in git-ignored files that I pasted into Vercel myself.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What surprised me was how much of the real work was verification. Most of the bugs above only showed up because each step was run against the live database instead of being assumed to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://ninaivu-ai.vercel.app" rel="noopener noreferrer"&gt;ninaivu-ai.vercel.app&lt;/a&gt;. Pick a customer (the picker shows who has past chats), send a message, then open the Graph, Cypher queries and Agent trace tabs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code, queries and data model:&lt;/strong&gt; &lt;a href="https://github.com/shobanravichandran/ninaivu" rel="noopener noreferrer"&gt;github.com/shobanravichandran/ninaivu&lt;/a&gt;. Every Cypher query is in &lt;code&gt;server/queries/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; FalkorDB Cloud (free tier, AWS us-east-1) · FastAPI on Vercel · React + Vite on Vercel · Claude Haiku 5.5 through the Anthropic tool runner · Voyage AI &lt;code&gt;voyage-4-lite&lt;/code&gt; embeddings.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The demo runs on FalkorDB's free tier, which pauses after a day without traffic and keeps no data across restarts. If the graph looks empty, the seed script in the repo restores the demo data in under a minute.&lt;/p&gt;

&lt;p&gt;The idea I'd take to any multi-agent system: &lt;strong&gt;don't make agents share a transcript.&lt;/strong&gt; Give them a shared, queryable memory, and let ownership and handoffs be data they can read in one query.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>falkordb</category>
      <category>hackathon</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
