A release fails. An agent rolls it back. Someone retries the same build, and the system goes through the same failure again.
Now change the version number. The release looks new, but it still carries the dependency that caused trouble last time. Remembering a version number won’t catch that.
That is the problem I explored with Revora, my project for Graph Hacks: Context for AI Agents, in Track 2: Agent Memory and Coordination.
Revora stores release history, measurements, agent handoffs and component relationships in FalkorDB. Those records influence whether the next candidate is allowed to serve traffic.
Try the live app · Source code
Start with something that can actually fail
I wanted a visible consequence for a wrong decision. The demo uses an isolated HTTP gateway with a stable service and a candidate service. Introducing a fault sends requests to a candidate that returns HTTP 503. Recovery restores healthy routing and verifies HTTP 200.
The fault is intentional. The HTTP requests, status codes and recovery checks are real. This is a controlled demonstration on a project-owned service, not a production outage.
In the recorded demo, I introduce v9-demo with base@3 and checkout-sdk@1.0. After the failure and recovery, retrying that version is blocked from memory before candidate traffic begins.
The more interesting attempt is v10-demo. It has a different version number and the same components. Revora follows the relationships back to the earlier failure and blocks it too.
The query that made the graph useful
The relevant path is small:
new release → USES → component ← USES ← failed release There is an important exception: a shared component may also have appeared in a healthy release. Blaming every shared dependency would make memory too aggressive.
This is the component check used by the backend:
MATCH (c:Component) WHERE c.key IN $components MATCH (c)<-[:USES]-(bad:ReleaseIncident { service:$service, decision:'rollback', failed:true }) WHERE bad.version <> $version AND NOT (c)<-[:USES]-(:ReleaseIncident { service:$service, decision:'promote' }) RETURN c.key, bad.id, bad.version That exclusion matters in the demo. The base component has healthy history, so it is excluded from the blocking match. The SDK has failed history without a healthy appearance, so it triggers the gate.
This is still a heuristic. A component appearing in a failed release does not establish that it caused the failure. A healthy appearance does not prove universal safety either. The rule is deliberately conservative and inspectable; it needs richer evidence before being trusted with real deployments.
A relational database could represent these relationships too. FalkorDB let me express the path directly in Cypher and use the same connected records for the decision and its explanation.
Three roles, with a check between observation and action
The recovery workflow has three bounded roles.
The Release Analyst measures twelve HTTP requests and persists a handoff containing the raw statuses, latencies and reported metrics.
The Evidence Reviewer reads that handoff and recomputes the measurements. Missing evidence can produce a request for more information. Contradictory evidence can reject the handoff.
The Recovery Planner receives the reviewed evidence and graph context. When configured with the Groq API, an LLM proposes an allowlisted action and cites graph IDs. A deterministic guard checks that proposal against the evidence before recovery proceeds.
The analyst and reviewer are deterministic. The planner has an LLM path and a deterministic fallback. These are bounded roles in an orchestrated workflow; they are not three autonomous agents freely negotiating with each other.
FalkorDB holds the handoff and the records connecting it to the incident, review and decision. The receiving role reads persisted evidence, which also makes the handoff available to another process.
What building it taught me
Saving history is only the beginning. A useful memory needs an identity, scope and a retrieval rule. Then it needs a clear point where it can change an action. In Revora, that point is the gate before candidate traffic.
An explanation needs evidence behind it. A convincing paragraph from a model is easy to mistake for a verified result. Keeping raw samples lets the reviewer check the numbers independently. Persisting the proposed action and guardrail verdict makes disagreement visible too.
Retries deserve their own design. One audit found that replaying an old completed recovery could clear a newer active incident. The repair had to distinguish replaying a historical result from repairing the same interrupted incident. That became a regression case. “Idempotent” needs to describe exactly which state a retry may change.
Deployment is part of the demonstration. A documentation commit triggered a Vercel deployment that served a 404 because the project root was wrong. Setting the root to frontend and the framework to Next.js restored it. A green deployment status alone did not tell me the app was usable; opening the public page and checking its backend connection did.
What the project proves—and what comes next
The demo shows a remembered failure changing a later release decision, a component relationship extending that memory to a different version, and recovery decisions connected to their evidence.
It does not establish general autonomous learning or prove that memory beats every well-designed static policy. The hosted setup is one workspace and one backend worker. Tenant isolation and distributed coordination would need further work.
The next improvement I would make is better handling of uncertainty: distinguish a suspected component from a confirmed cause, and define when new measurements should override an older memory.
For now, the question I want Revora to answer is simple: what did the previous failure teach the next decision?
You can follow that path in the live workspace, watch the demo, or inspect the implementation and local setup.
