Switching AI coding tools was supposed to help me keep building. Quite often, it meant starting another explanation.
What am I building? Why did I choose this approach? What did I already try? Which detail about that API must the next agent know?
I would explain most of it, forget one particular, and watch the next session redo work or continue from a decision I had already changed. Sometimes that turned into a bug. Then I was explaining the project and the bug.
This kept happening while I built projects, joined hackathons, and moved between tools. I wanted the useful things I had already worked out to come with me.
That is why I started building LockIn: an open-source notebook that my AI tools can write to and read from.
For Graph Hacks, I took on the part that saving notes alone does not solve: keeping the work connected when those notes change.
I wanted the context to travel with the project
I tried keeping project instructions in CLAUDE.md. Those files are useful, and I still see a place for them. But maintaining one was another task. Moving to another machine meant remembering to move or sync the right version too, whether through Git or some other route.
I wanted to open a different tool on a different machine and connect it to the same maintained notebook. I also wanted to choose where that notebook lived.
LockIn connects through MCP, the protocol that lets a compatible AI tool call its save, search and handoff tools. The agent can record a useful finding while it works, then retrieve the relevant pages in a later session. I can read and edit those pages in the app, organised by project and subject.
Workflow illustration. Each device connects to the same reachable LockIn instance; the notebook is shared across those connections.
This is the machine independence I wanted. A particular chat window no longer has to be the only place holding the explanation.
I wanted to review what my agents remembered
A saved note shows who recorded it. I can open it, check the reasoning, and witness it. LockIn's name for recording that review.
That needs one distinction: a saved note can be searched before I witness it. Witnessing is not a filter that magically makes every searchable sentence correct.
It does matter when a note replaces an earlier decision. An agent can propose the correction, but the older page is retired only after I witness the replacement. The earlier page stays linked to the new one, so I can follow how the decision changed.
I also added a Claude Code plugin that captures summaries when a conversation compacts or ends. It helps keep useful session knowledge from disappearing at precisely the moment I am most likely to need it again.
The code changes. Its explanation should be checkable too.
A note can carry the repository, commit and files it describes.
That gives the next agent something concrete to check. With access to the repository, it can compare the current code with the saved reference, confirm the explanation, flag uncertainty, or propose a correction.
The commit link is a reference point, not a promise that the note is still right. I wanted to make checking possible without quietly overwriting the reasoning that came before.
That led to a harder problem.
What if the next agent already has the old decision?
Here is the payment example I use to explain it. It is a demonstration scenario, not a claim that LockIn handled a real customer's payment.
An agent records a decision: retry the payment request when the server returns an error. I review it. A task depends on that decision, and the agent leaves a handoff with the goal, progress, what it tried, and the next step.
Another agent picks it up and starts implementing retries.
Then I learn that a failed response does not necessarily mean the payment failed. Retrying could charge the customer twice. I approve a replacement decision: check the charge before retrying.
The notebook now has the correction. But the second agent is already working.
How do I tell which work is still using the earlier decision?
Searching for the new page is useful when an agent asks. It does not, by itself, tell me what an agent received before the change.
That is the question I built the FalkorDB part of LockIn around.
I gave the handoff a context receipt
A context receipt records the pages and revisions delivered when an agent picks up work. It also keeps the paths that explain why those pages were included.
The graph connects the handoff to its task, the task to the decisions it depends on, and each approved replacement to the page it replaces. LockIn can follow those connections to prepare current context, then record what it actually delivered.
Workflow illustration. Updating a decision does not rewrite the record of what an agent received earlier.
Three queries do the core work. Here is the first, with a shorter return list than the application uses:
MATCH p = (h:Handoff {id: $h})
-[:PINS|ABOUT|FOR|DEPENDS_ON*1..4]->(n:Note)
WHERE n.deleted = false
RETURN n.id AS id,
[x IN nodes(p) | coalesce(x.title, x.goal, x.id)] AS why,
length(p) AS hops
One pattern follows four kinds of connection at once: the pages the handoff pins, the note it is about, its task, and whatever those depend on, up to four steps away. why is the route itself. When LockIn tells an agent "you have this page because this handoff → task Add retry → page 9", it is printing nodes(p). I did not have to rebuild the explanation after finding the answer. The answer arrived with it.
The second query decides which version of each page stands now:
MATCH (n:Note {id: $id})
OPTIONAL MATCH p = (n)<-[:SUPERSEDES*1..8 {accepted: true}]-(cur:Note)
WHERE NOT (cur)<-[:SUPERSEDES {accepted: true}]-(:Note)
RETURN cur.id, [x IN nodes(p) | x.page] AS chain
The important part is {accepted: true} on the variable-length relationship. A replacement only counts once I have witnessed it, so an agent's unreviewed proposal can never be handed over as the current decision. It comes back separately, as a warning. WHERE NOT … keeps only the end of each chain. If two witnessed pages both replace the same decision, there are two ends, and LockIn reports a conflict instead of quietly picking one.
The third writes the receipt:
MATCH (h:Handoff {id: $h})
MERGE (a:Agent {name: $agent})
CREATE (a)-[:RECEIVED]->(r:Receipt {id: $id, at: $at})-[:FOR]->(h)
WITH r
UNWIND $items AS it
MATCH (n:Note {id: it.id})
CREATE (r)-[:DELIVERED {hash: it.hash, why: it.why}]->(n)
Each DELIVERED relationship stores the revision hash and the route. Later, check_context runs the first two queries again for the same handoff and compares the result with these relationships. Every difference has a name: replaced, edited, newly witnessed, withdrawn, no longer linked, or added.
SQL can store these rows. What made me choose a graph is the question itself: handoff → task → decision → whatever replaced it, with a different kind of connection at each step and no fixed length. In tables, that is a recursive query per question, and the route has to be reassembled afterwards to explain the result. In Cypher, the route is the result, and the route is exactly what the agent needs to read. The full queries are in the repository.
Sometimes the useful answer is “refresh your context first”
Before I approve a replacement, LockIn can show the affected work and agents. After approval, check_context compares an earlier receipt with the context available now.
If the agent tries to complete the connected task through LockIn using an outdated receipt, the task update asks it to refresh first. The agent gets a concrete change to inspect instead of a vague reminder to be careful.
There is a boundary here that I want to keep clear. This check governs task completion recorded through LockIn. It cannot stop arbitrary code edits, and receiving a new page does not prove the agent implemented it correctly. I still need to review and test the work.
Finished tasks keep a connection to the receipt used at completion. A later decision change can therefore flag completed work for review too. I can also inspect which page version each agent received.
I found that a more useful question than simply asking whether the notebook contains the latest information.
An old failure can need another look
I wanted a handoff to remember what had already been tried. Repeating a failed approach was part of the frustration that started this project.
But “we tried that” needs a reason beside it.
Suppose automatic retries were ruled out because the payment API could create duplicate charges. Later, the API adds support for idempotency keys, and the relevant decision is replaced.
The earlier attempt is still a failure under its original conditions. Its reason may no longer apply to the current task.
LockIn stores that attempt against its task and the pages it assumed when the handoff was saved. The next pickup includes the earlier attempts and highlights changed premises. It does not automatically rerun the idea; it gives the next agent a better reason to reconsider it.
In the graph, that check is one more step along a relationship LockIn already had:
(:Handoff)-[:TRIED]->(:Attempt)-[:ASSUMED]->(old:Note)<-[:SUPERSEDES {accepted: true}]-(newer:Note)
An attempt is linked to its assumptions once, when the handoff is saved. Those links therefore keep pointing at the pages it was ruled out under, even after the notebook moves on.
A saved AI answer can get old as well
Inside LockIn, Ask answers questions using my notes, saved links and PDFs. Answers include page references I can open and check.
Those citations now become graph relationships. If an answer cited a note that was later replaced, or a saved document that was deleted, the earlier answer is flagged. The “Answers to ask again” view gives me somewhere to revisit those questions.
It is the same replacement relationship again, approached from a different starting point:
MATCH (a:Answer)-[:CITES]->(n:Note)<-[:SUPERSEDES {accepted: true}]-(:Note)
RETURN DISTINCT a.id, n.page
Receipts, earlier attempts and saved answers all ask one question of the graph: does something I relied on have a witnessed replacement now?
This became a useful test of the design during the final feature review. Deleted-document citations were not being flagged because that check handled notes but missed documents. The fix now covers both. I also fixed graph excerpts left behind by deleted conversations and a pagination boundary that could skip answers with matching timestamps. The fix commit and regression cases are public.
A source trail only helps if changes to the source are followed all the way through.
What FalkorDB taught me
My first load test was slow everywhere. I ran simulated agents against a graph with 5,000 notes. With one agent it managed about 25 operations a second. With 100 agents at once, it still managed about 25. A fixed ceiling like that usually means something is being serialized.
There were two causes:
- Every query used one connection. A Redis-protocol server answers commands on a connection in order, so each agent waited behind every other agent. A small pool of connections fixed that. FalkorDB can run read queries from different connections in parallel, and a write only replies after it has committed, so a read sent afterwards on another connection still sees the change.
-
The planner started from the wrong side.
GRAPH.PROFILEshowed this pattern scanning every note in the graph instead of starting from the indexed id:
MATCH (n:Note {id: $id})<-[:SUPERSEDES {accepted: false}]-(p:Note)
Splitting it with WITH makes the starting point explicit:
MATCH (n:Note {id: $id})
WITH n
MATCH (n)<-[s:SUPERSEDES]-(p:Note)
WHERE s.accepted = false
That query went from 20 ms to 0.7 ms. Two others had the same shape.
| Agents at once | Operations / second | p50 | p95 |
|---|---|---|---|
| 1 | 130 | 6 ms | 16 ms |
| 10 | 432 | 15 ms | 69 ms |
| 100 | 450 | 157 ms | 615 ms |
4 cores shared by the test and FalkorDB. Each operation is one MCP tool's complete graph work. Method and full results.
exists(pattern) was not the test I expected. In FalkorDB 4.22, using it in a RETURN gave true for a pattern with no match. That made every dependency look like a loop. size([(t)-[:DEPENDS_ON*1..8]->(f) | 1]) > 0 gives the correct result.
One graph per account is only a name, so the name must be trusted. LockIn derives the graph name from the signed-in account, never from anything an agent sends. A test gives two accounts identical page numbers and titles, then checks that neither can see the other's graph.
The lesson I keep: Cypher lets you write one elegant pattern, and the planner may still start at the wrong end. GRAPH.PROFILE shows you where it starts, and WITH tells it where to start instead.
How I put the parts together
The app and connected agents use the same LockIn server.
Postgres stores notebook records, tasks, handoffs and retrieval passages with their embeddings. FalkorDB stores the connections used for dependencies, replacements, receipts, earlier attempts and cited answers, with a separate graph for each account. Changed notebook records are projected into that graph before graph operations.
These stores have different jobs. Passage retrieval helps find material related to a question by meaning. The graph follows the explicit connections between that material and work. I use both.
For small notebooks, Ask can use the available digest. For larger context, when embeddings are available, it retrieves relevant passages before asking the model to answer. That is the RAG part. In this implementation, the passage embeddings are in Postgres.
The graph also answers questions about the team's knowledge, not just a single handoff. Who knows about payment retries? FalkorDB's full-text index finds the notes; each one is resolved to the page that stands now; then the graph checks which agents received which version:
CALL db.idx.fulltext.queryNodes('Note', $topic) YIELD node, score
The answer reads like: page 12 stands, Claude Code wrote it, Codex received page 11, which has since been replaced. Which decisions carry the most work? PageRank over the dependency relationships ranks them:
CALL algo.pageRank(NULL, 'DEPENDS_ON') YIELD node, score
Those are the decisions to change most carefully, and the ones LockIn previews the impact of before I witness a replacement. The technical reference has the full graph model and every query.
I wanted to choose where the notebook and the model run
Self-hosting matters to me. I can run LockIn on infrastructure I control and connect my devices to it, instead of handing the notebook to another hosted memory service.
For Ask, I can choose a browser model, a local server such as Ollama or LM Studio, or a supported provider using my own API key. The browser sends the prepared question and context directly to the selected model.
With a local model setup, I can keep that processing on my own infrastructure too. If I choose a cloud model, the supplied question and context go to that provider. Self-hosting the notebook does not change that choice.
That control is part of what I wanted from LockIn: a place for project knowledge that I can reach from different tools, with a clear say in where it lives and where it is sent.
Next, I want the project context to work for the whole team
The next direction I want to build is team organisations: members sharing context for selected projects, with clear boundaries around what belongs to the team.
That is future work. Today's implementation uses account-scoped notebooks and graphs; I am not presenting a finished organisation-permissions system here.
I started with the frustration of explaining the same project again. The part I care about now is continuity: keeping the finding, its reason, the work that used it, and the changes that follow connected enough to inspect.
If you want to try it, the repository has the Docker setup, MCP connection instructions and a reproducible payment scenario. The original notebook existed before the event; the FalkorDB coordination work was built for Graph Hacks by WeMakeDevs and FalkorDB, entering Agent Memory and Coordination and Company Brain.
I used Claude Code and OpenAI Codex as coding assistants, including implementation, review, tests and production material. The motion graphics are made with Remotion. I remain responsible for checking the behaviour described here and explaining the design.
What is the detail you most often have to explain again when you start a new AI session?






Top comments (0)