DEV Community

Cover image for Waking up to the context, not just the alert
Ayan
Ayan

Posted on Originally published at banana-coder.hashnode.dev

Waking up to the context, not just the alert

Building Lore on FalkorDB, and what I learned when I switched the graph off. Originally published on Hashnode.

TL;DR

  • Lore loads a company into one FalkorDB graph: people, services, PRs, deploys, Slack threads, tickets, decisions, and who may see what.
  • When an alert fires, four Gemini agents walk the graph with read-only Cypher tools and write a root-cause report where every claim cites a source the viewer is allowed to see.
  • Permissions are enforced inside the Cypher query, not after retrieval. 4 personas x 63 probe questions leaked 0 restricted sources.
  • With the graph switched off, the agents still found the bad PR, but they couldn't rule out the red herring, set a severity or say who to page. Retrieval recall dropped from 75% to 38%.

The pager goes off

My roommate is an SDE who sometimes stays on call, and he is a deep sleeper. When his pager fires, I am the one who says "bhai uth jaa, page hogaya hai". Then he wakes up and sits through the incident until everything is back in place.

One time it was a P2 that pulled in the whole team, including principal architects and leads. Afterwards the diagnosis was that the right context could have resolved it faster. In my view, getting that context is what takes on-call engineers the most time. And the first person paged is often an SDE-1 who joined a couple of months ago and has little history with the system that broke.

I am also curious about neuroscience, and it is hard to stay calm during a P2 after being woken up. Under acute stress, the brain releases stress chemicals that quickly weaken the prefrontal cortex, the part that handles working memory, planning and careful reasoning (Arnsten, 2009). Losing sleep makes it worse. The amygdala, which reacts to threats, becomes more reactive and less connected to the prefrontal regions that calm it down (Yoo et al., 2007). People call this an amygdala hijack. So in that panic anyone can stop thinking properly, and it happens to the best of us. Lore does the careful part. It runs the queries, checks every suspect against the evidence and hands over a clean report with sources, so the person on call can review it with a calmer head.

I am new to graphs. For the past 10 months I have been building context management systems at work, so I know where vector search breaks. It cannot infer that A caused B and B caused C. Graph databases are built for that. I wanted to learn how FalkorDB does it, and whether it really helps a context engine, so I built Lore for Graph Hacks (WeMakeDevs x FalkorDB), alone.

Lore loads a fictional food-delivery company called Kettle into one FalkorDB graph. All the data is synthetic. When an alert fires, four Gemini agents walk the graph with read-only Cypher tools and write a report where every claim cites a source the viewer may see. The goal is a shorter time to root cause, with a human approving anything that leaves the system.

What the incident asks for

In the demo data, alert A-3001 fires at 3 AM on checkout because p99 latency has passed 3 seconds.

  • 12 minutes earlier, deploy 990 shipped PR #479 to checkout. It only changes button copy, and it is the obvious suspect.
  • 90 minutes earlier, deploy 991 shipped PR #482 to pricing, lowering its cache TTL from 300 to 5 seconds. Checkout depends on promo-engine, which depends on pricing.
  • ADR-011 says that TTL must stay at 300 seconds or more. A past incident, INC-0307, was the same mistake.
  • The old runbook says to page Marco. Marco moved to the Data team.

A good answer needs the chain from alert to PR #482, the current owner of pricing, and a rule for who may read the private Slack thread about the vendor limits. Those are links and access rules, and no document states them.

The architecture

Lore architecture: browser, CloudFront and S3, FastAPI with a supervisor and four agents, read-only Cypher tools, FalkorDB, Gemini, and Slack and GitHub behind human approval

FalkorDB holds the company, who may see what, the embeddings and the agents' memory.

Three rules carry the design.

  1. Every tool returns the part of the graph it touched: node ids, the Cypher and the milliseconds.
  2. Permissions are enforced inside the Cypher query. The viewer comes from the login session and never from the model.
  3. Facts are computed in code and the model narrates. Severity, blast radius and who to page are rules.

This is the core of the graph schema.

(:Person)-[:MEMBER_OF]->(:Team)
(:Team)-[:OWNS]->(:Service)
(:Service)-[:DEPENDS_ON]->(:Service)
(:Deploy)-[:DEPLOYED_TO]->(:Service)
(:Deploy)-[:INCLUDES]->(:PR)
(:Person)-[:AUTHORED]->(:PR)
(:Alert)-[:FIRED_ON]->(:Service)
(:Incident)-[:CAUSED_BY]->(:PR)
(:Decision)-[:DISCUSSED_IN]->(:Thread)
(:Thread)-[:VISIBLE_TO]->(:Team)
Enter fullscreen mode Exit fullscreen mode

Access is part of the graph. A restricted source has a VISIBLE_TO edge to the Team, Role or Channel allowed to see it. When Marco left Pricing, his MEMBER_OF edge became WAS_MEMBER_OF, a different relationship type that grants nothing. The question "can Marco still read this?" turns into "does a current path exist?".

How the agents are built

A plain Python supervisor runs four specialist agents in a fixed order: Triage, Historian, Investigator, Responder. Each is the same tool-calling loop with its own prompt, tools and model. Code decides the facts between steps. The agents never message each other. They share a blackboard, the Investigation node in the graph with its Steps and Findings. A human approves anything that goes out.

How Lore's agents are built: the supervisor runs Triage, Historian, Investigator and Responder in order, with code rules between them, read-only Cypher tools over FalkorDB below, and human approval before Slack and GitHub

Orange is code deciding, blue is the model deciding.

Inside one agent, Gemini either calls a tool or stops. Code runs the tool with the permission filter and feeds the result back. At the end, code checks that every cited id came from a tool in this run. Bad claims get one retry, then they are dropped and flagged.

The loop inside one agent: Gemini calls a tool or stops, code runs the tool with the permission filter, findings are checked for citations with one retry, then written to the blackboard

The same loop runs all four agents.

The closest design template is a workflow of specialist agents, which is prompt chaining with gates. It is not an autonomous orchestrator. No model decides which agent runs next, and no model writes its own Cypher.

Design patterns Lore uses: a sequential workflow with gates, a deterministic supervisor, a ReAct tool loop per agent, a blackboard, a fixed tool catalogue, an evaluator with one retry, facts in code, human in the loop and three kinds of memory

The patterns used, and what I left out.

FalkorDB in action

Every query lives in its own file. There are 42 .cypher files and 11 agent tools, so the agents get a fixed catalogue of parameterised queries instead of free-form Cypher. This one separates the red herring from the root cause.

// recent_changes: what changed lately in this service or in anything it depends on?
MATCH (viewer:Person {id: $viewer})
MATCH p = (:Service {id: $service})-[:DEPENDS_ON*0..3]->(dep:Service)
WITH viewer, dep, min(length(p)) AS hops
MATCH (dep)<-[:DEPLOYED_TO]-(d:Deploy)-[:INCLUDES]->(pr:PR)
WHERE d.at > $since AND d.at <= $until
  AND coalesce(d.access_level, 'internal') IN $open_levels
  AND (pr.access_level IN $open_levels
       OR (viewer)-[:MEMBER_OF|HAS_ROLE|IN_CHANNEL]->()<-[:VISIBLE_TO]-(pr))
OPTIONAL MATCH (pr)-[:MODIFIES]->(m:CodeModule)
RETURN dep.name AS service, hops, d.id AS deploy_id, pr.id AS pr_id,
       pr.title AS pr_title, collect(DISTINCT m.id) AS modules
// ... author and other columns cut
ORDER BY d.at DESC
Enter fullscreen mode Exit fullscreen mode

The variable-length DEPENDS_ON*0..3 finds the pricing deploy even though the alert fired on checkout. MODIFIES shows that PR #479 only touched checkout/i18n/en.json. The evidence path uses FalkorDB's built-in shortest path, limited to four relationship types so it follows a causal chain and not a shortcut through a shared team.

CALL algo.SPpaths({sourceNode: a, targetNode: pr,
                   relTypes: ['FIRED_ON', 'DEPENDS_ON', 'DEPLOYED_TO', 'INCLUDES'],
                   weightProp: 'w', relDirection: 'both', maxLen: 6})
YIELD path, pathWeight
Enter fullscreen mode Exit fullscreen mode

Tool calls took 1 to 7 ms in a live run, with a median of about 5 ms in a recorded run.

Permissions inside the query

Many RAG systems filter after retrieval. In Lore the filter sits in the same query as the vector search, so restricted text never leaves FalkorDB.

MATCH (viewer:Person {id: $viewer})
CALL db.idx.vector.queryNodes('Chunk', 'embedding', $fetch_k, vecf32($qvec)) YIELD node AS c, score
MATCH (c)-[:PART_OF]->(src)
WHERE src.access_level IN $open_levels
   OR (viewer)-[:MEMBER_OF|HAS_ROLE|IN_CHANNEL]->()<-[:VISIBLE_TO]-(src)
// ... keep the best chunk per source, order by score, LIMIT $k
Enter fullscreen mode Exit fullscreen mode

The index returns nearest neighbours before the WHERE runs, so I over-fetch. The leak test asks 63 probe questions as each of 4 personas and counts restricted ids in results and citations. The count is 0.

War stories

A FalkorDB error that only fires for restricted data. On FalkorDB 4.18.3, my permission pattern failed with Type mismatch: expected Null but was Pointer. It happened in two shapes: a WHERE attached to a WITH that also computes a new value, and a node taken from startNode(r) or endNode(r). Every test on open data passed, because the OR short-circuits and the pattern is never evaluated. It broke four queries when Dana, who may see restricted sources, first looked up a thread. For the graph view I collect the viewer's granted ids first and test with IN. The other three queries got a pass-through WITH viewer, n, level before the WHERE.

OPTIONAL MATCH (viewer)-[:MEMBER_OF|HAS_ROLE|IN_CHANNEL]->()<-[:VISIBLE_TO]-(g)
WITH viewer, collect(DISTINCT g.id) AS granted
// ...
WHERE (a_level IS NULL OR a_level IN $open_levels OR a.id IN granted)
Enter fullscreen mode Exit fullscreen mode

The regression tests now run those queries as a persona who can see restricted nodes. Tests that only use the least-privileged user cover the easy branch.

A leak I found in review. Investigations are stored as Finding nodes, and their text can come from a viewer with more access. My visibility check treated any node without an access_level as public, and memory nodes had none. Querying as Sam, the contractor, through a free-form debugging tool returned another persona's findings. Only organisation and structure labels count as open now, and the tool is deleted. Giving memory nodes their own access levels is still on my to-do list. A missing label must mean deny.

Smaller ones. The Ask page refused 9 of 12 simple lookups like "Who owns pricing?", because ownership lives in edges and no document says it. Returning the named node's direct, permission-filtered relationships as a citable source took it to 12 of 12. My own eval first said the graph made retrieval worse, 26% against 38%, because it had its own copy of the retrieval steps. Calling the product's function gave 75%. And the model once paged Meera, the lead of the service that alerted, instead of Rahul, who owns the service that broke. That is now a rule in code.

What happened when I switched the graph off

Lore has a Graph OFF switch. The same agents and models can then only search text by meaning, over the same FalkorDB vector index with the same permission filter. I expected Graph OFF to miss the root cause.

It did not. With Graph OFF the agents still found PR #482, because the PR's own text mentions the cache TTL, and similarity search matched it.

What vector search could not do was everything around that answer. It could not rule out the red herring PR #479, set a severity, give an evidence path or name who to page. It also took longer and cost more. This is one recorded run of each, as Arjun, the on-call persona.

Measure Graph ON Graph OFF (vector only)
Root cause PR #482 PR #482, after a second Investigator pass
Red herring PR #479 Examined and ruled out Never examined; five other PRs ruled out
Severity P1, set by rule from the blast radius None
Evidence path Alert to PR #482 via checkout, promo-engine, pricing, deploy 991 None
Who to page Rahul Nobody named
Tool calls 13 18
Time to root cause 54 s 102 s
Cost of the run $0.026 $0.091

Graph ON vs Graph OFF on the same alert: a table comparing root cause, suspects ruled out, severity, evidence path, who to page, tool calls, time to root cause and cost

The comparison screen, built from those recorded runs.

Graph ON has more live runs behind it. Across 6 live runs as Arjun, the root cause was PR #482 every time, the red herring was rejected every time, and a run took 50 to 60 s and cost $0.022 to $0.027.

The Lore investigation screen: reasoning trail on the left, the permission-filtered graph with the evidence path lit in the middle, the cited root-cause report with who to page on the right

A live run, with who to page on the right.

The 59-question eval shows the gap more widely. Scoring checks key facts and source ids, with no AI judge.

Measure Graph ON Graph OFF
Expected sources in top 5 (recall@5) 75% 38%
Plain lookups ("who reviewed PR 482?") 93% 11%
Stale-source questions 76% 29%
Answers with the key facts 71% 43%
Claims with a valid source 67% 49%
Correct refusals 93% 66%
Leaks, 4 personas x 63 probes 0 0

Two honest notes. Claims with a valid source is 67% in the Ask page, below my 90% target. And on conflicting-source questions the two modes tied at 83% recall. The graph helps most where the answer is a relationship, such as an owner or an author.

Memory helps too. Once a human confirms a root cause, it becomes a playbook. On Story B the Investigator went from 8 steps to 4, and time to root cause from 50 s to 39 s.

What is not solved

  • I planted the stories in the data, so the agents are solving puzzles I designed. Real Slack and GitHub data would be messier.
  • The samples are small. The Graph ON vs OFF comparison is one recorded run each.
  • A contractor's report says "100 restricted sources matched". That is true, but it hits the search limit and reads oddly.

How I built it, and how I used AI

An AI coding assistant wrote most of the code and docs in the repo. Before the hackathon I told it what I understood and what I wanted to learn, and it proposed the project. I set the direction, guided the implementation and reviewed it. I reviewed models.py thoroughly and, for the rest, mostly the crux of the files and the infrastructure roles. I am a beginner with graph databases, Cypher and backend design, and the assistant wrote the Cypher in this post.

I made the deployment calls. I chose Fargate because it is cheaper and needs no ACM certificate, with the API and FalkorDB on Fargate and the front end on S3 and CloudFront. I picked the reasoning model by comparing two on cost, speed and accuracy.

My most useful work was using the app and trying to break it. Four fixes came from that. The Slack message was a wall of raw ids (F-16). A recorded run showed a failed tool step on every replay (F-17). The graph jittered and the camera jumped during a run (F-18). Graph OFF still showed the graph (F-19), which led to the vector-only view and the comparison screen. The UI screens and diagrams were also designed with the assistant. Gemini runs inside the product and wrote the synthetic Slack, ticket and ADR wording.

What I would steal from this

  • Put access in the graph as edges, and filter inside the same query that retrieves.
  • Treat a missing access label as deny.
  • Give agents a catalogue of parameterised queries, and inject identity on the server.
  • Move every decision with a correct answer, like severity and who to page, out of the model and into code.
  • Build the graph-off baseline early. It tells you what the graph is for.

Thank you to WeMakeDevs and FalkorDB for Graph Hacks. Try the live demo at https://d1cu7fzoa0v5wz.cloudfront.net. The code, eval reports and fix log are in the repo:

GitHub logo ayan2809 / Lore

Lore Company Brain for dedicated Incident Support RCA

Lore: the company brain that already knows what broke

Lore loads a fictional food-delivery company ("Kettle") into one FalkorDB graph: people, teams, services, PRs, deploys, Slack threads, tickets, design decisions and incidents, plus who may see what. When an alert fires, four Gemini agents walk the graph with read-only Cypher tools and write a root-cause report where every sentence cites a source the viewer is allowed to see. A human approves anything that leaves the system.

Built solo by Ayan for Graph Hacks (WeMakeDevs x FalkorDB), Oct 15-18 2026.

Live demo: https://d1cu7fzoa0v5wz.cloudfront.net ยท Tracks: all three ยท Video: https://youtu.be/-pN2LUVsix4 ยท Blog: https://banana-coder.hashnode.dev/waking-up-to-the-context-not-just-the-alert

The Lore investigation screen: the reasoning trail on the left, the permission-filtered company graph with the evidence path lit in the middle, and the cited root-cause report with who to page on the right

Every number in this README comes from a real run or from the eval reports in backend/eval/. Kettle is fictional and all data is synthetic; every date in the data is a date in Kettle's story, not a project date.

Jump to: Why I builtโ€ฆ




If you have been on call and have opinions on what a 3 AM report should contain, I would love to hear them in the comments.

Top comments (0)