{"slug": "i-accidentally-turned-llm-memory-into-program-analysis", "title": "I accidentally turned LLM memory into program analysis", "summary": "A security researcher has developed a Datalog engine for LLM agents to maintain knowledge during vulnerability research, addressing the problem of models losing track of established facts. The system treats observations as facts and rules, automatically invalidating conclusions when inputs change, similar to incremental program analysis.", "body_md": "# I accidentally turned LLM memory into program analysis\n\nOver the past few months I have been playing around quite a bit with LLM agents, particularly for vulnerability research.\n\nThey are becoming surprisingly good at navigating large codebases, explaining unfamiliar subsystems and helping explore potential attack surfaces. However, once an investigation starts taking a few hours, I kept running into the same problem: the model would slowly lose track of what we had actually established.\n\nIt might suggest an approach that we had already ruled out, forget that an assumption turned out to be false, or confidently continue reasoning from an observation that was no longer valid. Obviously, telling an LLM that something is wrong does not necessarily mean that it will stop believing all of the things that depended on it :)\n\nI initially started looking into memory systems because I wanted to make LLMs more useful for complex vulnerability research and reduce this type of hallucination.\n\nThere are of course already plenty of solutions for giving LLMs memory. Usually this involves storing old conversations or observations somewhere, embedding them, and then retrieving the most relevant pieces whenever the model needs them again.\n\nThis works reasonably well, but there was something about it that bothered me.\n\nDuring a vulnerability research sesh, I don’t just want the model to remember what we said.\n\nI want it to **maintain what we currently know**.\n\nImagine that during an investigation we establish the following:\n\n```\nattacker controls object_a\nobject_a points to object_b\nobject_b is a kernel object\n```\n\nFrom this, we may conclude that the attacker can control a kernel object.\n\nA normal memory system could store all of these observations and retrieve them again whenever we ask about the exploitability of the bug. The LLM then figures out the same conclusion.\n\n*Great!*\n\nHowever, suppose that two hours later we discover in LLDB that `object_a`\n\ndoes not actually point to `object_b`\n\n, and that our previous observation was based on a wrong assumption.\n\nAt that point our memory may contain something like:\n\n```\nobject_a points to object_b\nattacker can control object_b\nobject_a does not actually point to object_b\n```\n\nNow we retrieve some subset of these memories and hope that the LLM correctly figures out which conclusions are still valid.\n\nThis started to feel a *little* familiar to me.\n\n## This looks like program analysis\n\nA lot of the work I normally do involves program analysis.\n\nWhen analysing a program, we usually have a bunch of facts about the program and some rules that derive additional facts from them.\n\nFor example, imagine we know:\n\n```\ncalls(foo, bar)\ncalls(bar, baz)\n```\n\nWe could define a rule stating that if one function calls another function, which itself can reach a third function, then the first function can reach the third function as well.\n\nEventually we calculate a fixed point containing everything we can derive from the program. More importantly, if one of our input facts changes, there are plenty of techniques for updating only the affected results instead of rerunning everything from scratch.\n\nThis is also exactly what I wanted from an LLM during vulnerability research.\n\nIf an observation changes, I don’t want the model to reconstruct the entire investigation from a transcript and hopefully notice all of the consequences. I want the affected conclusions to become invalid automatically.\n\nWhen looking at the problem from this perspective, I started wondering why we were making the LLM reconstruct its entire state over and over again.\n\n*What if we just maintained it?*\n\nAnd this is how I somehow ended up writing a Datalog engine for LLMs :)\n\n## Datalog\n\nBefore we continue, it is probably useful to briefly explain what Datalog actually is.\n\nDatalog is a declarative logic programming language. Instead of writing instructions describing how something should be calculated, we describe facts and rules from which new facts can be derived.\n\nFor example, we could store the following facts:\n\n```\ncontrols(attacker, object_a).\npoints_to(object_a, object_b).\nkernel_object(object_b).\n```\n\nAnd then define the following rule:\n\n```\ncontrols_kernel_object(Attacker) :-\ncontrols(Attacker, ObjectA),\npoints_to(ObjectA, ObjectB),\nkernel_object(ObjectB).\n```\n\nFrom our existing facts, the engine can therefore derive:\n\n```\ncontrols_kernel_object(attacker).\n```\n\nNothing particularly exciting yet.\n\nHowever, suppose we later discover that:\n\n```\npoints_to(object_a, object_b).\n```\n\nwas incorrect.\n\nIf `controls_kernel_object(attacker)`\n\nwas derived from that fact, we know exactly which conclusion depends on the observation that just changed, and we can automatically invalidate it.\n\nThis is considerably nicer than putting all of the old information into a prompt and asking an LLM to hopefully notice the same thing.\n\n## Lemmalog\n\nThis eventually turned into [Lemmalog](https://github.com/JordyZomer/lemmalog).\n\nThe basic idea is that an LLM should not necessarily be responsible for maintaining its own knowledge. Instead, I split the problem into two parts.\n\nThe LLM handles the fuzzy part:\n\n```\n\"LLDB shows that the freed object is later reused\nas the destination of the write.\"\n|\nv\nfreed(object_a)\nreused_as(object_a, write_target)\n```\n\nAnd Lemmalog handles the deterministic part:\n\n```\nfacts\n|\nv\nrules\n|\nv\nderived facts\n```\n\nThis means that the LLM is still responsible for understanding natural language, source code, debugger output and all the other messy information that appears during an investigation.\n\nLLMs happen to be quite good at this.\n\nBut once that information has been converted into structured facts, we no longer need the model to repeatedly determine all of its consequences. The database can do that instead.\n\n## Retractions\n\nOne of the first interesting problems I ran into was removing facts.\n\nAdding facts to a Datalog database is relatively straightforward: add the new fact and evaluate any rules which may now produce additional results.\n\n**Removing** something is a little more annoying.\n\nTake the following example:\n\n```\na.\nb.\nc :- a.\nc :- b.\n```\n\nHere `c`\n\nhas two separate reasons for being true.\n\nIf we remove `a`\n\n, we cannot simply remove `c`\n\n, because `b`\n\nstill provides another derivation for it. However, if we remove both `a`\n\nand `b`\n\n, `c`\n\nshould disappear as well.\n\nThis turns out to be quite important during vulnerability research, because a conclusion may be supported by multiple observations.\n\nFor example:\n\n```\ncandidate_3_is_exploitable\n```\n\nmay remain true even if one particular exploit primitive turns out not to work, because there is another independent path to the same result.\n\nSo Lemmalog has to keep track of how facts were derived and update their support when something changes.\n\nConveniently, this also gives us another useful property:\n\n*we can ask why something is true.*\n\n## Why?\n\nImagine we have been running an agent for a few hours while investigating something and it eventually concludes:\n\n```\ncandidate_3_is_exploitable\n```\n\nThat is nice, but I would also quite like to know why.\n\nBecause Lemmalog already tracks the dependencies of derived facts, we can ask it for the provenance of a conclusion. For example, we may get something that conceptually looks like this:\n\n```\ncandidate_3_is_exploitable\n|\n+-- attacker_controls_pointer\n| |\n| +-- observation_41\n|\n+-- pointer_reaches_target\n|\n+-- observation_57\n+-- rule_12\n```\n\nIf `observation_41`\n\nlater turns out to be incorrect, we know that this conclusion may no longer be valid, and because the database knows this as well, it can remove the affected conclusions automatically.\n\nThis was originally mostly necessary to make incremental evaluation work correctly, but it turns out that being able to ask an AI agent why it believes something is quite useful as well :)\n\nIt also addresses one of the more annoying failure modes I encountered with LLM-assisted research. Sometimes a model will confidently say something like:\n\n```\nwe already established that this pointer is attacker-controlled\n```\n\nwhen that is not actually true.\n\nIf a conclusion exists in Lemmalog, I can ask where it came from. If there is no provenance supporting it, then it is not part of the maintained state.\n\nThis obviously does not prevent an LLM from hallucinating during extraction, but it does make it much harder for unsupported conclusions to silently become part of the investigation.\n\n## Facts also change over time\n\nAnother issue is that replacing old facts is not always the same as deleting them.\n\nSuppose we originally believe:\n\n```\nprimitive_a is viable\n```\n\nand later discover:\n\n```\nprimitive_a is not viable\n```\n\nFor most current queries, we probably only care about the second statement. However, if we want to understand why we previously explored a particular exploit strategy, the old state is still useful.\n\nFor this reason Lemmalog can associate facts with validity intervals.\n\nConceptually, we can represent the state as something like:\n\n```\nviable(primitive_a) [10:14, 12:37)\nnot_viable(primitive_a) [12:37, ...)\n```\n\nThis allows us to answer both:\n\n```\nIs primitive_a viable now?\n```\n\nand:\n\n```\nWhy did we think primitive_a was viable earlier?\n```\n\nwithout keeping two apparently contradictory facts around and asking the LLM to decide which one we meant.\n\nAgain, this is not really a language model problem.\n\nIt is mostly a database problem.\n\n## Why not just use a vector database?\n\nVector databases are very useful.\n\nIf I ask:\n\n```\nWhat did we find earlier about this allocation path?\n```\n\nsemantic search is probably exactly what I want.\n\nBut cosine vibe similarity and truth are not quite the same thing.\n\nA vector database can retrieve:\n\n```\nobject_a points to object_b\n```\n\nbecause it is relevant to my question. It does not inherently know that the statement was disproven two hours later, or that five other conclusions depended on it and should therefore no longer be considered valid.\n\nThis made me realise that there are really two different problems hiding under the term “memory”.\n\nThe first is:\n\n```\nWhat information from the past is relevant to this question?\n```\n\nThe second is:\n\n```\nGiven everything we have learned so far, what is currently true?\n```\n\nRetrieval is very good at the first problem.\n\nLemmalog is mostly an experiment in solving the second one.\n\nThe two can also be combined, which is what I currently do.\n\n## A vulnerability investigation is basically an analysis state\n\nThe more I worked on this, the more similarities with program analysis started appearing.\n\nDuring a vulnerability investigation we have observations:\n\n```\nthis field is attacker-controlled\n```\n\nassumptions:\n\n```\nthis object survives until the second callback\n```\n\nrelationships:\n\n```\nprimitive_b depends on primitive_a\n```\n\nhypotheses:\n\n```\nthis could become an arbitrary write\n```\n\nand conclusions:\n\n```\ncandidate_3 is exploitable\n```\n\nThis maps surprisingly well to the things we already do in program analysis.\n\nWe have input facts:\n\n```\nobservations\n```\n\nrules:\n\n```\nrelationships between observations\n```\n\nderived facts:\n\n```\nconclusions\n```\n\na fixed point:\n\n```\neverything currently known\n```\n\nand when an input changes, we perform incremental evaluation:\n\n```\nupdate affected conclusions\n```\n\nBecause we track dependencies, we can also explain where results came from:\n\n```\nprovenance\n```\n\nAt some point it became fairly obvious that I had approached the problem like a static analysis engine without intentionally meaning to.\n\nThis also changed how I thought about the role of the LLM itself.\n\nYou can almost think of the whole system as a slightly strange compiler.\n\nThe LLM acts as the front-end:\n\n```\nsource code,\ndebugger output,\nnatural language notes\n|\nv\nstructured facts\n```\n\nLemmalog is the intermediate representation and analysis engine:\n\n```\nstructured facts\n|\nv\ndeductive rules\n|\nv\nmaintained state\n```\n\nAnother LLM invocation can eventually turn that state back into natural language, suggest the next experiment, or use it to perform some action.\n\nThe amusing part is that our parser is probabilistic, while everything after it does not necessarily have to be.\n\n## Does it actually make LLMs better?\n\nThis is of course the important question.\n\nThe engine itself now supports incremental evaluation, retractions, provenance, temporal facts, aggregations, entity reconciliation, hybrid retrieval, demand-driven queries and a bunch of other things that I probably added because implementing Datalog features is more fun than I expected.\n\nThere is also an MCP server which allows agents to use Lemmalog directly.\n\nBut none of that matters very much if giving an LLM this memory does not actually improve anything.\n\nSo I plugged it into [MemEval](https://github.com/ProsusAI/MemEval) and tested it on both LongMemEval and LoCoMo using their standardized reader models and evaluation setup. Extraction during ingestion is Claude Sonnet 4.6 (chunked and file-cached, so it is paid once per conversation); everything after extraction uses the benchmark’s own standardized readers and judges.\n\nThe results were a little better than I expected.\n\n## LongMemEval\n\nLongMemEval tests whether an LLM can answer questions about information spread across long conversation histories. The split I used contains 102 questions, divided equally between user facts, assistant facts, preferences, multi-session questions, temporal reasoning and knowledge updates.\n\nBecause 17 questions per category is not exactly a massive sample size, I ran Lemmalog three times rather than getting excited about whichever run happened to score highest.\n\nThe result was:\n\n```\nLemmalog\nF1: 0.463 +/- 0.010\nAccuracy: 0.575 +/- 0.004\n```\n\nFor comparison, the published memory-system results are:\n\n```\nPropMem 0.550\nSimpleMem 0.480\nLemmalog 0.463 +/- 0.010\nOpenClaw 0.244\nFull Context 0.222\n```\n\nMy own full-context GPT-4.1 run scored `0.197`\n\nF1.\n\nSo Lemmalog is not beating PropMem yet, and it is still slightly behind SimpleMem, but it gets more than twice the F1 of giving GPT-4.1 the entire conversation.\n\nMore amusingly, the context passed to the answering model is roughly **38 times smaller**.\n\n```\nFull context: ~104,000 tokens/question\nLemmalog: ~2,700 tokens/question\n```\n\nApparently maintaining state instead of repeatedly rereading the entire history is useful :)\n\nThe category results from one representative run looked like this:\n\n| System | SS-User | SS-Asst | Preference | Multi-Session | Temporal | K-Update |\n|---|---|---|---|---|---|---|\n| PropMem | 0.851 |\n0.767 |\n0.147 | 0.582 |\n0.424 | 0.528 |\n| SimpleMem | 0.752 | 0.566 | 0.126 | 0.382 | 0.578 |\n0.475 |\nLemmalog |\n0.790 | 0.672 | 0.128 | 0.211 | 0.416 | 0.579 |\n| OpenClaw | 0.401 | 0.432 | 0.127 | 0.082 | 0.185 | 0.234 |\n| Full Context | 0.265 | 0.415 | 0.177 |\n0.062 | 0.212 | 0.202 |\n\nThe result I found most interesting was Knowledge Update.\n\nLemmalog scored `0.579`\n\n, compared with `0.528`\n\nfor PropMem and `0.202`\n\nfor full context.\n\nKnowledge Update is basically the situation I originally cared about:\n\n```\nwe believed A\n|\nlater we learn that A is no longer true\n|\nwhat should we believe now?\n```\n\nSo seeing Lemmalog top the published field on the category that most closely resembles maintained program state was rather satisfying.\n\nSingle-session factual memory also worked surprisingly well. Lemmalog reached `0.790`\n\non user facts and `0.672`\n\non assistant facts, while temporal reasoning reached `0.416`\n\n, almost identical to PropMem’s `0.424`\n\nin that run.\n\nThe obvious remaining problem is multi-session reasoning:\n\n```\nPropMem 0.582\nSimpleMem 0.382\nLemmalog 0.211\n```\n\nDiagnosing those failures was interesting: the information usually was not mis-connected, it was simply never extracted. If the extractor never emits a fact for the Airbnb booking, no amount of derivation is going to answer a question about it.\n\nWhich brings us to one of the more amusing parts of running benchmarks.\n\n## I accidentally taught it not to answer questions\n\nAt one point LongMemEval suddenly dropped to `0.371`\n\nF1.\n\nAfter going through the failures, I discovered that **32 of the 102 questions were being refused**.\n\nAll 32 were answerable.\n\nQuestions such as:\n\n```\nWhich airline did I fly most?\n```\n\nor:\n\n```\nHow many magazine subscriptions do I have?\n```\n\nwere returning:\n\n```\nNot mentioned.\n```\n\nThe problem was an instruction I had added to reduce hallucinations. I told the reader to make sure that the answer was actually supported by the retrieved facts before answering.\n\nUnfortunately, the model interpreted this as:\n\nIf no single fact literally contains the final answer, refuse.\n\nThere is obviously no fact saying:\n\n```\nmost_flown_airline(user, swiss)\n```\n\nif the memory instead contains:\n\n```\nflew(user, swiss, trip_1)\nflew(user, swiss, trip_2)\nflew(user, lufthansa, trip_3)\n```\n\nThe answer exists. It just requires counting.\n\nThe fix was to separate two cases:\n\n-\nIf the premise is absent or misattributed, refuse.\n\n-\nIf the evidence exists but requires counting, comparing, combining or ordering facts, actually reason over it.\n\nAfter fixing that, F1 recovered to `0.429`\n\n.\n\nThe rest of the gap turned out to be sneakier: the counting path had been silently dead the entire time. Count lines were passed through a relevance filter before being shown to the reader, and the plural stemmer used by that filter only folded words longer than four characters. So `owns`\n\nnever matched `own`\n\n, every count line was dropped, and counting questions quietly received no counts at all.\n\nFixing the stemmer, rendering counts together with the facts they count, and precomputing date arithmetic instead of hoping the model would correctly subtract two dates brought F1 to `0.463`\n\n.\n\nThis distinction also turns out to matter quite a bit on another benchmark.\n\n## LoCoMo\n\nI also ran Lemmalog against the full LoCoMo benchmark.\n\nLoCoMo is considerably larger: 10 long conversations containing **1,986 questions** covering factual recall, temporal reasoning, multi-hop questions, inference and adversarial false-premise questions.\n\nThis one was particularly useful because 1,986 questions makes it considerably harder to accidentally get excited about a lucky seed.\n\nAgain, I ran the entire benchmark three times.\n\n```\nLemmalog LoCoMo:\n0.533 +/- 0.001 F1\n```\n\nThe published comparison looks like this:\n\n| System | F1 |\n|---|---|\n| PropMem | 0.605 |\n| OpenClaw | 0.557 |\n| Full Context | 0.542 |\nLemmalog |\n0.533 ± 0.001 |\n| Hindsight | 0.489 |\n| Graphiti | 0.416 |\n| Memory-R1 | 0.389 |\n| SimpleMem | 0.358 |\n\nSo Lemmalog currently sits third among the dedicated memory systems in this comparison, behind PropMem and OpenClaw.\n\nIf we count throwing the entire conversation into the prompt as a memory system, it is fourth.\n\nWhich I think is fair :)\n\nMore importantly, the three runs were almost identical, so `~0.53`\n\nseems to be a real result rather than benchmark noise.\n\nThe per-category results from the final configuration look like this:\n\n| Category | Lemmalog | PropMem | Full Context |\n|---|---|---|---|\n| Factual | 0.399 | 0.431 | 0.517 |\n| Temporal | 0.454 |\n0.615 |\n0.369 |\n| Multi-hop | 0.545 | 0.599 | 0.674 |\n| Inferential | 0.164 | 0.289 |\n0.197 |\n| Adversarial | 0.707 |\n0.794 |\n0.509 |\n\nThere are two results here that I particularly like.\n\nThe first is temporal reasoning.\n\nThe initial version of Lemmalog scored:\n\n```\n0.257\n```\n\nAfter fixing temporal normalization and retrieval:\n\n```\n0.454\n```\n\nThe bug was actually quite funny.\n\nAt one point I was comparing date-like values as interned Datalog symbols.\n\nThe engine’s `<`\n\noperator on symbols compares their internal ids.\n\nInternal ids are obviously not dates :)\n\nAfter normalising extracted dates into comparable integers and deriving `happened_before`\n\nfrom actual timestamps, temporal performance jumped by almost twenty F1 points.\n\nThe second result I like is adversarial questions.\n\nLemmalog scores:\n\n```\n0.707\n```\n\nwhile full context scores:\n\n```\n0.509\n```\n\nThese questions deliberately contain false or misattributed premises.\n\nFor example, the conversation may contain a story about somebody receiving a gift, followed by a question which attributes the same gift to somebody else.\n\nA language model with a giant transcript is rather tempted to find the semantically similar story and answer anyway. A structured memory can instead notice that there is simply no supporting fact about the person in the question.\n\nIn other words:\n\n```\nno\n```\n\nturns out to be quite a useful answer.\n\n## The front-end matters a lot\n\nThe first LoCoMo implementation scored `0.483`\n\n.\n\nThe current one scores about `0.533`\n\n.\n\nThe Datalog evaluator did not suddenly become 10% smarter.\n\nMost of the improvement came from fixing how information gets into and out of the analysis state.\n\nEntity resolution, for example, turned out to matter quite a lot.\n\nImagine the following sessions:\n\n```\nSession 1:\n\"I bought a Honda Civic.\"\n\nSession 3:\n\"My car broke down.\"\n\nSession 7:\n\"The Civic is finally fixed.\"\n```\n\nIf extraction produces:\n\n```\nbought(user, honda_civic).\nbroke_down(car).\nfixed(civic).\n```\n\nthen the Datalog engine is doing exactly what we asked it to do.\n\nUnfortunately, we asked it to reason about three different objects.\n\nSo Lemmalog now has a reconciliation pass which connects episode-local mentions to canonical entities.\n\nPure lexical retrieval also caused some funny failures. A question referring to a:\n\n```\n\"kitchen gadget\"\n```\n\nwould not necessarily retrieve a fact about an:\n\n```\n\"Instant Pot\"\n```\n\neven though the relationship is obvious to us.\n\nRetrieval now combines BM25, graph/entity boosts and embeddings, while the final context contains both the structured facts and the original source snippets they came from.\n\nThis was another useful reminder that the difficult part of this architecture is not necessarily computing the fixed point.\n\nIt is building a good IR from natural language.\n\nWhich, again, feels suspiciously like program analysis.\n\n## Some things should probably stay fuzzy\n\nThere is also one area where Lemmalog remains rather bad: inference.\n\nOn LoCoMo:\n\n```\nPropMem 0.289\nLemmalog 0.164\n```\n\nThis makes sense.\n\nSuppose somebody says:\n\n```\nI usually prefer quiet restaurants, except when I'm travelling\nwith friends, when I quite like somewhere lively.\n```\n\nFlattening that into:\n\n```\nprefers(user, quiet_restaurants).\n```\n\nhas thrown away half of the useful information before Datalog has even seen it.\n\nThe obvious direction is not to abandon structured memory, but to stop pretending that every memory is an unconditional tuple.\n\nConditional knowledge can remain conditional:\n\n```\nprefers(User, lively_restaurants) :-\n    prefers_when(User, lively_restaurants, with_friends),\n    with_friends(User).\n```\n\nAnd the original episode text can remain available for situations where the structured representation loses useful nuance.\n\nThe useful architecture therefore looks less like:\n\n```\nvector memory\nOR\nsymbolic memory\n```\n\nand more like:\n\n```\nagent memory\n|\n+---------+---------+\n| |\ndeductive state episodic memory\n| |\nfacts / rules / time fuzzy context\nprovenance semantic retrieval\nretractions source text\n```\n\nWhich is fortunately pretty close to what Lemmalog has become anyway.\n\n## The token thing\n\nThere is one other part of the result which I did not originally expect to be quite as large.\n\nFor LongMemEval, the answering model sees roughly:\n\n```\nFull context: ~104,000 tokens/question\nLemmalog: ~2,700 tokens/question\n```\n\nAround **38x less context**.\n\nFor LoCoMo:\n\n```\nFull context: ~18,900 tokens/question\nLemmalog: ~3,400 tokens/question\n```\n\nAround **6x less**.\n\nThere is of course an extraction cost.\n\nThe conversation has to be read once and turned into facts, so saying that the whole system is simply 38 times cheaper would be dishonest.\n\nThe important distinction is that extraction happens once.\n\nFull-context prompting pays for the entire history again on every query.\n\nWith a persistent agent, the difference therefore grows over time.\n\nConceptually:\n\n```\nfull context  - Lemmalog\nturn 50 100K/query - ~2.5K/query\nturn 100 200K/query -  ~2.5K/query\nturn 500 1M/query - ~2.5K/query\n```\n\nAt some point the full-context version doesn’t merely become expensive.\n\nIt stops fitting in the context window.\n\nLemmalog’s query context does not grow with the entire transcript because it retrieves the relevant maintained state instead.\n\nWhich was kind of the original point.\n\n## Does this prove anything?\n\nNot quite yet.\n\nLongMemEval is 102 questions, and LoCoMo is still a conversational-memory benchmark rather than a vulnerability investigation.\n\nPropMem also still beats Lemmalog overall on both standardized comparisons.\n\nSo I am not going to claim that Datalog has solved LLM memory :)\n\nBut I do think the results are enough to show that the idea is not completely stupid.\n\nAcross three LongMemEval runs, Lemmalog scores:\n\n```\n0.463 +/- 0.010 F1\n0.575 +/- 0.004 accuracy\n```\n\nAnd on LoCoMo:\n\n```\n0.533 +/- 0.001 F1\n```\n\nIt is particularly competitive when the task rewards the things the architecture was designed for: knowledge updates, temporal state, multi-hop relationships and rejecting unsupported premises.\n\nPerhaps the most interesting result to me, though, is not the final number.\n\nThe first standardized LongMemEval configuration scored:\n\n```\n0.226\n```\n\nThe current one scores:\n\n```\n0.463\n```\n\nMore than twice as high.\n\nMost of that improvement came from looking at individual failures and discovering fairly concrete computer science problems:\n\n- entity identity was disconnected\n- dates were represented incorrectly\n- retrieval missed semantic aliases\n- aggregation existed but wasn’t surfaced\n- a plural stemmer didn’t think “owns” matched “own”\n- the reader had accidentally been taught to refuse synthesis\n\nNone of those required making the language model larger.\n\nThey required maintaining better state around it.\n\nWhich is a result I find rather funny given why I started this project.\n\nThe next experiment is therefore the one I actually care about.\n\nGive an agent a complicated vulnerability investigation, let it run for a long time, and see whether maintaining its analysis state stops it from resurrecting dead hypotheses and hallucinating relationships between observations.\n\nThat will probably be more interesting than remembering where Alice works :)\n\n## Conclusion\n\nI didn’t really want to give the LLM a better memory.\n\nI wanted it to stop forgetting why we believed things.\n\nIf an agent has already discovered that:\n\n```\nA implies B\nB implies C\n```\n\nand later learns that `A`\n\nis no longer true, we shouldn’t need to give it fifty old messages and ask it to figure out whether `C`\n\nshould still be trusted.\n\nLikewise, if an exploit strategy depends on an assumption that we have just disproven in a debugger, I don’t want the model to suggest the same strategy again two hours later because an old conversation happened to be semantically relevant.\n\nWe already know how to solve problems involving facts, dependencies, invalidation and fixed points. We’ve been solving them in databases and program analyses for decades.\n\nThe benchmark results at least suggest that this isn’t only a nice idea in theory.\n\nLemmalog is already competitive with dedicated LLM memory systems, substantially outperforms full context on some of the tasks it was designed for, and does so while giving the reader a tiny fraction of the original history.\n\nThere is still **plenty** that it is bad at.\n\nBut perhaps we don’t need a bigger context window every time an agent forgets something.\n\nSometimes we can just maintain the state.\n\nThe source code for Lemmalog is available [here](https://github.com/JordyZomer/lemmalog).\n\nCheers!", "url": "https://wpnews.pro/news/i-accidentally-turned-llm-memory-into-program-analysis", "canonical_source": "https://pwning.systems/posts/llm-memory-program-analysis/", "published_at": "2026-08-28 14:00:08+00:00", "updated_at": "2026-08-28 14:18:34.948553+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-agents", "ai-research"], "entities": ["LLM", "Datalog", "LLDB"], "alternates": {"html": "https://wpnews.pro/news/i-accidentally-turned-llm-memory-into-program-analysis", "markdown": "https://wpnews.pro/news/i-accidentally-turned-llm-memory-into-program-analysis.md", "text": "https://wpnews.pro/news/i-accidentally-turned-llm-memory-into-program-analysis.txt", "jsonld": "https://wpnews.pro/news/i-accidentally-turned-llm-memory-into-program-analysis.jsonld"}}