No path, no recommendation: an AI agent with graph memory on FalkorDB
Built for the WeMakeDevs × FalkorDB hackathon. Primary track: Agent Memory and Coordination. Secondary fit: Agents That Act on Connected Data. Most tools that read the internet for you end the same way: a feed of links, each with a confident sentence explaining why it matters to you. That sentence

Built for the WeMakeDevs × FalkorDB hackathon. Primary track: Agent Memory and Coordination. Secondary fit: Agents That Act on Connected Data.
Most tools that read the internet for you end the same way: a feed of links, each with a confident sentence explaining why it matters to you. That sentence is written after the fact, by a model, and nobody can check it.
NightShift does the opposite. A recommendation cannot exist unless FalkorDB returns a path from your project to the signal, and that path is the explanation.
The short version: NightShift is an always-on developer intern. It connects public technical signals to the project you are actually working on, remembers decisions across sessions, hands work between bounded agents, and explains every recommendation with the graph evidence that produced it. Nothing with a side effect runs without explicit approval.
Keyword matching can tell you an article mentions Redis. It cannot tell you whether your project needs Redis, or why.
What is missing is relationships. This project has this requirement. That requirement needs this technology. This item mentions that technology. This source published the item. Those are four hops, and a relational table with a keyword column flattens every one of them.
That is why the store had to be a graph, and why FalkorDB is the product store here, not a visualization layer bolted on at the end.
Every recommendation has to survive this query. It lives in the graph repository, not in an HTTP handler or an agent prompt:
MATCH path = ( p: Project { id: $project_id }) - [:HAS_REQUIREMENT ] -> ( r: Requirement ) - [:NEEDS ] -> ( tech: Technology ) <- [:MENTIONS ] - ( item: Item ) - [:FROM ] -> ( source: Source ) WHERE item.id = $item_id RETURN p.id AS project_id, r.id AS requirement_id, tech.id AS technology_id, item.id AS item_id, source.id AS source_id, nodes ( path ) AS path_nodes LIMIT 10 Enter fullscreen mode Exit fullscreen mode In one line: User → Project → Requirement → Technology ← Item → Source.
If the query returns nothing, the item is not a recommendation. It can still be shown as unconnected ingestion data, but it never gets a graph-backed score. When it does return a path, the recommendation stores the query name, ranking version, matched IDs, source, evidence and a readable path. The UI renders the path the graph returned. It does not ask a model to invent an explanation afterwards.
Pull FalkorDB out and you do not lose a chart. You lose the relationships that make the product work.
The first draft of our spec had SQLite as an operational store. We dropped it. FalkorDB is the only durable application state store: projects, graph paths, tasks, leases, idempotency records, memories, decisions, tool calls and approved actions.
Two stores means a question about "what did the user decide?" can be answered from the wrong one. So the rule is strict. If a crash-replay spool is ever needed, it can only be an at-least-once transport buffer, deleted after the graph acknowledges the write, and it can never answer a product query.
The practical payoff is simple: restart the daemon and the dashboard is rebuilt from FalkorDB, including the active project path and persisted tasks.
User graph names come from a stable hashed identifier. A raw email, display name or token never appears in a graph name, and every user-owned node also carries tenant_id and user_id for defense-in-depth filtering.
Memory is append-oriented and typed: episodic, semantic, procedural or operational. When something changes, the new memory points at the old one with a SUPERSEDES relationship. History stays queryable. Every memory that influences a recommendation keeps its DERIVED_FROM link back to evidence.
Retrieval has a fixed order: current task and session, active project, recent decisions and feedback, stable preferences, then older graph-connected memories. Each retrieval is bounded by rows and bytes, so memory can't quietly grow into a context dump.
The agent surface is MCP, but narrow and typed. It is not an arbitrary Cypher endpoint:
Every invocation becomes a ToolCall node linked to a task and a session. The model cannot choose a graph name, run arbitrary Cypher, run shell commands, fetch arbitrary URLs, read credentials or perform a direct external write. Scope comes from the installation, never from tool input.
Multiple daemons can share a user graph, so scheduler and task leases live in the graph too. A lease is keyed by source or task ID, with an instance heartbeat and a lease token. A worker stores its result only while its token is valid, so a stale worker cannot publish after another one recovers the lease. Abandoned work is converted to an explicit failed state by recovery routines rather than silently continuing.
Work moves between agents as Task nodes: Task -[:HANDED_OFF_TO]-> Task, Task -[:ASSIGNED_TO]-> Agent. The Relevance-to-Planner handoff is a graph relationship you can query, not a message that disappeared into a queue.
"Please ask before doing anything destructive" is a wish. We wanted it to be a property of the runtime.
NightShift can optionally hand a scoped coding task to OpenCode. You type something like Fix the authentication bug in a conversation, and then:
The guardrails matter more than the flow:
NightShift is one native Go daemon. It needs no Node.js, Python, Docker or SQLite at runtime. Static binaries build for old x86 PCs and ARM64 boards:
GOOS = linux GOARCH = arm64 CGO_ENABLED = 0 go build -trimpath -o dist/nightshift-linux-arm64./cmd/nightshift GOOS = linux GOARCH = amd64 CGO_ENABLED = 0 go build -trimpath -o dist/nightshift-linux-amd64./cmd/nightshift Enter fullscreen mode Exit fullscreen mode If a device lacks the memory for FalkorDB, the graph can stay remote. The same binary also runs under systemd on a VPS, or as a stateless service on Render, as long as every instance shares one FalkorDB endpoint.
One detail we like: /health/live never touches the graph, while /health/ready probes it and returns 503 when it is unreachable. A graph outage therefore stops traffic without sending a healthy process into a restart loop.
cp.env.example.env set -a; source.env; set +a docker compose -f deploy/docker/compose.yaml up -d falkordb go mod download go run./cmd/nightshift Enter fullscreen mode Exit fullscreen mode Then trigger the deterministic public-fixture flow and inspect the evidence:
curl -fsS -X POST http://127.0.0.1:8080/api/v1/runs curl -fsS http://127.0.0.1:8080/api/v1/recommendations curl -fsS http://127.0.0.1:8080/api/v1/graph/path/<recommendation-id> Enter fullscreen mode Exit fullscreen mode Record feedback and turn the recommendation into a proposed plan:
curl -fsS -X POST http://127.0.0.1:8080/api/v1/recommendations/<recommendation-id>/feedback \ -H 'Content-Type: application/json' \ -d '{"kind":"useful","note":"Connected to the active project."}' curl -fsS -X POST http://127.0.0.1:8080/api/v1/recommendations/<recommendation-id>/plan \ -H 'Content-Type: application/json' \ -d '{"goal":"Prepare the smallest implementation task."}' Enter fullscreen mode Exit fullscreen mode The plan stays proposed until a human approves it. Open http://127.0.0.1:8080/ for the embedded status page, which shows the active graph path.
Covered by automated checks: bounded source failures and recovery, approval-gated action retries, graph evidence persistence, exactly-once lease behaviour, and the auth and configuration boundaries.
We are not claiming numbers for memory, CPU, startup time or dashboard latency, and not for any particular ARM board. Those targets stay unmeasured until the workload is run on the target hardware and the output is recorded. Deterministic mode is the baseline. Live Gmail, Calendar and search provider credentials are deployment-specific.
Make the invariant a query, not a prompt. "Only recommend what's relevant" is an instruction. "No path, no recommendation" is a MATCH that returns rows or doesn't.
One durable store removes a whole class of bugs. Leases, memory, decisions and tasks in the same graph means there is no second place for the truth to drift.
Narrow, typed tools beat clever prompts. Five fixed tools with scope taken from the installation are easier to reason about, and to audit, than one flexible one.
State what you haven't measured. A hackathon project that says "this number is unmeasured" is more believable than one that guesses.