<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Alexander Panasenko · Engineering Notes]]></title><description><![CDATA[Alexander Panasenko · Engineering Notes]]></description><link>https://alex09x.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6aed2df3d203f7f9dba8c01f/20986b3b-204e-47d9-9720-71d5cf24d711.jpg</url><title>Alexander Panasenko · Engineering Notes</title><link>https://alex09x.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 10:30:45 GMT</lastBuildDate><atom:link href="https://alex09x.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Reverse Engineering with AI: Your Decompiler Can Lie to You]]></title><description><![CDATA[A field name, 2,000 look-alike functions and a wrong pseudo-C branch: an XRef walkthrough checked against machine instructions.]]></description><link>https://alex09x.hashnode.dev/reverse-engineering-with-ai-your-decompiler-can-lie-to-you</link><guid isPermaLink="true">https://alex09x.hashnode.dev/reverse-engineering-with-ai-your-decompiler-can-lie-to-you</guid><category><![CDATA[reverse engineering]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Assembly]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Alexander Panasenko]]></dc:creator><pubDate>Fri, 06 Nov 2026 23:57:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aed2df3d203f7f9dba8c01f/f5669eb3-f732-4007-a8a2-935ebfa2651d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>TL;DR.</strong> Serialized field names can lead you to their offsets. The harder question is which code changes those fields: in a public example, 2,000 decoys write the same offsets as the real function. The real function and the displayed decoys earn the same 40 points for their writes to those fields. The real function gets ten more because it shares a caller, <code>main</code>, with the serializer; the decoys have no callers at all. The candidate is then checked against its instructions and call sites. The walkthrough uses XRef 0.4.0 and a synthetic C and assembly program; it demonstrates a method and is not a benchmark.</p>
</blockquote>
<p>Before the 0.4.0 release, one <code>xref field</code> query came back with <code>not_found_under_limits</code>: no matching layouts. The response flagged incomplete discovery under its analysis limits, but it gave no way to tell which keys the tool had seen or where it had lost the connection to the fields. That case is recorded in <a href="https://github.com/alex09x/xref/issues/76">issue 76</a>.</p>
<p>An empty list makes the next wrong move easy: widen the search at random, conclude that the structure is not there, or start taking the whole binary apart by hand. What I needed was an answer to a much narrower question: which observations get you from a field name to the code that changes it?</p>
<p>Put in familiar terms: a program reports <code>"status": "failed"</code>—which function changed that field? That is an illustrative way to frame the question. The recorded example below uses the keys <code>c</code>, <code>m</code> and <code>e</code>, preserved exactly so the source, instructions and output can be checked together.</p>
<p>When an AI agent does this work, it needs the same checkable chain a human does. In this setup, XRef is a native analyzer: the agent asks it questions through the CLI or MCP and works with the data that comes back. Finding a plausible function, understanding its role and reading its pseudo-C correctly are three separate steps, and the third is where the title comes from: later on, a historical XRef reproducer shows pseudo-C that, read literally, returns <code>99</code> for input the machine code evaluates to <code>4</code>. The recording below contains CLI commands and their captured outputs; the closing section turns the same sequence into a checklist an agent can follow.</p>
<p>For an executable without symbols, this is a surprisingly awkward question. You can know a field's name because it shows up in serialized output and know almost nothing about how it is laid out in memory. The source type names are gone. The function names are gone. A single small displacement can appear in thousands of completely unrelated instructions. A plausible decompilation can make an early guess look convincing long before the facts back it up.</p>
<p>While I was working on <a href="https://xrefcode.com/">XRef</a>, this question gradually got sharper: first find code from the name of a serialized field, then match a set of neighboring keys and find the functions that write those fields. Short keys exposed the next gap: even a negative result has to explain what the tool actually managed to check. You can follow that path through issues <a href="https://github.com/alex09x/xref/issues/58">58</a>, <a href="https://github.com/alex09x/xref/issues/74">74</a> and <a href="https://github.com/alex09x/xref/issues/76">76</a>.</p>
<p>The <code>xref field</code> command connects key uses to offsets relative to an incoming pointer, then finds and ranks candidate writers. A string reference, an offset and a matching function are different leads. To turn them into an answer, you have to follow the whole chain and keep the instructions each conclusion rests on.</p>
<p>This article works through one question step by step: <strong>which code changes the values associated with these keys?</strong> Along the way it shows the false leads that keep the problem from being a simple search for memory stores.</p>
<h2>What the public example actually is</h2>
<p>To follow this chain to the end, I use a small, published test program with a known answer, so every conclusion can be checked against the source and the machine instructions. Real-world tasks shaped what the tool needed to do; the walkthrough itself runs on the open example.</p>
<p>The <a href="https://github.com/alex09x/xref/tree/2f01e7ba8d9ccfab08f055cce3119e95dd1504bb/docs/walkthroughs/field-synthetic">existing recording</a> uses the published XRef 0.4.0 Linux executable. It was made on November 5, 2026; I reuse its commands and outputs here rather than claiming a new run. The exact build revision and file hashes are in the recording's environment record (linked at the end).</p>
<p>The target program is generated C and assembly. The part of the C that matters fits on one screen:</p>
<pre><code class="language-c">struct record { uint64_t c, m, e; };

void serializer(struct record *p) {
    emit("c", 1, &amp;p-&gt;c);
    emit("m", 1, &amp;p-&gt;m);
    emit("e", 1, &amp;p-&gt;e);
}

void writer(struct record *p, uint64_t value) {
    p-&gt;m += value;
    p-&gt;e ^= value;
}
</code></pre>
<p>This is an excerpt from the <a href="https://github.com/alex09x/xref/blob/2f01e7ba8d9ccfab08f055cce3119e95dd1504bb/docs/walkthroughs/field-synthetic/target.c">full generated source</a>. Function attributes and build flags preserve the call pattern the walkthrough relies on. <code>main</code> calls <code>writer</code> first, then <code>serializer</code>.</p>
<p>The <code>emit</code> function does nothing but mix the key, the length and the value at the given address into a <code>volatile</code> variable. That reproduces the machine-level pattern "key + value address → shared function" without a real JSON encoder or Serde. It is a synthetic C and assembly example for studying the mechanism. Assessing accuracy on real Rust applications requires a separate evaluation.</p>
<p>The assembly part adds 2,000 decoy functions. Each one writes at offsets <code>+8</code> and <code>+16</code> from its first argument. None of them is ever called. That is what keeps the question from being trivial: an analysis that just looks for matching offsets will produce plenty of convincing answers.</p>
<p>The program was built with GCC 13.3.0 using <code>-O2 -g0 -fno-inline -fno-ipa-icf</code>, then run through <code>objcopy --strip-all</code>. XRef reports 2,007 functions and zero symbols; in the recorded example, the boundaries of those functions come from <code>.eh_frame</code>, the stack-unwinding data. The analysis runs on the stripped file; the symbols of a separate reference build are used only to check the answer.</p>
<h2>A key string is a starting point, not a field</h2>
<p>Searching for <code>m</code> does not immediately tell you which memory location holds the value named <code>m</code>. A short string is an especially weak lead: its bytes can be part of a shared literal or be used in some other context. Those were exactly the cases that had to be handled after the request in issue 76. <a href="https://github.com/alex09x/xref/pull/77">PR 77</a> added shared-literal recognition for short keys, per-key diagnostics and a way to inspect partial matches.</p>
<p>What I need is the relationship between two arguments at the use site: the key being passed and the value passed along with it. Only then does the name acquire a useful local meaning.</p>
<p>The first query in the recording looks for layout evidence only:</p>
<pre><code class="language-sh">xref -b target-stripped field --keys c,m,e --layout-only
</code></pre>
<p>The report finds one candidate function, <code>0x11c0..0x1204</code>, with offsets relative to its incoming <code>rdi</code>. For the three keys it reports <code>c -&gt; 0x0</code>, <code>m -&gt; 0x8</code> and <code>e -&gt; 0x10</code>; the corresponding instructions read eight bytes.</p>
<p>These values are useful because the report also lists the call sites where each key and value are passed. In the recorded image they are <code>0x11d7</code>, <code>0x11ec</code> and <code>0x1202</code>; the last one is a tail jump to the same callee. You can inspect the function that passes the keys instead of accepting the layout as an unexplained result.</p>
<p>Here is a shortened disassembly excerpt from the recording. The omitted instructions load the keys and their lengths:</p>
<pre><code class="language-asm">0x11c5  mov rdx, rdi
0x11c8  mov rbx, rdi
0x11d7  call 0x11a0
0x11dc  lea rdx, [rbx+8]
0x11ec  call 0x11a0
0x11f1  lea rdx, [rbx+0x10]
0x1202  jmp short 0x11a0
</code></pre>
<p>On entry, <code>rdi</code> holds the pointer <code>p</code>. The copy saved in <code>rbx</code> survives the first call. So the values passed in <code>rdx</code> are <code>p</code>, <code>p + 8</code> and <code>p + 16</code>. In the full listing, the matching key string addresses and lengths are passed right next to them. That is how key names get tied to offsets.</p>
<p><img src="https://prod.codes/img/xref/re-ai-evidence-chain.png" alt="The evidence chain from key bytes to a checked candidate writer." /></p>
<p><em>Figure 1. Each arrow is a separate investigative step. Finding a later link does not automatically prove the earlier ones.</em></p>
<h2>A field's address and the value stored there are different things</h2>
<p>One distinction shaped the problem more than any other: following a pointer and following a value loaded through that pointer are not the same thing.</p>
<p>Consider two schematic instructions. They illustrate the distinction and are not an additional result from the recorded test program:</p>
<pre><code class="language-asm">lea rax, [rdi+8]   ; address: incoming pointer + 8
mov rax, [rdi+8]   ; value: eight bytes loaded there
</code></pre>
<p>After <code>lea</code>, <code>rax</code> still holds an address relative to the original input. After <code>mov</code>, it holds whatever was stored at that address: a number, a pointer to a different object, or a value the current analysis cannot classify. A later write through <code>[rax+16]</code> does not automatically become a write to <code>rdi+24</code>.</p>
<p>That is why it matters where a base pointer came from: whether it arrived as an argument, was returned from a call or was loaded from memory. Each leads to a different conclusion about which object you are looking at. Lose track of a pointer's origin, and later offsets can be wrongly attributed to the earlier object.</p>
<p>There is another practical trap here: searching only for <code>lea</code> can miss a field that is accessed through a direct load or store. An empty result from such a search does not prove the field was never used. It means the chosen instruction pattern was not found under those search conditions.</p>
<p><img src="https://prod.codes/img/xref/re-ai-address-versus-value.png" alt="An address calculation and a memory load have different pointer provenance." /></p>
<p><em>Figure 2. A schematic example: after a load from memory, you cannot keep tracking the original base by simply adding offsets.</em></p>
<h2>An access range is not a struct declaration</h2>
<p>The three offsets and read widths in this example give an addressed range of at least <code>0x18</code> bytes. It is tempting to write <code>sizeof(record) = 24</code> next to it and move on.</p>
<p>Here, the source lets us look up the real declaration. The stripped binary on its own supports a narrower claim: those positions were accessed with those widths. It does not follow that there are no fields beyond them, that no field was missed, or that the allocated memory ends there. Alignment, tail padding and how the memory was allocated are separate questions.</p>
<p>The report expresses this as <code>addressed-span&gt;=0x18</code> and <code>exact-size=unknown</code>. It also keeps the lexical order of call sites separate from the source declaration order or any universal execution order. These caveats are a useful part of the result, not an apology tacked on after the analysis.</p>
<p>The same goes for names. We observed the byte strings <code>c</code>, <code>m</code> and <code>e</code>. We know the type name <code>struct record</code> from the source and the reference build's symbols, not from the query result.</p>
<p>Explicit boundaries make the result more useful. You can search for writes at the observed offsets without first declaring an invented complete type.</p>
<h2>Two thousand functions that look like the answer</h2>
<p>Now let's ask for functions that write both named fields through the same pointer:</p>
<pre><code class="language-sh">xref -b target-stripped field --keys c,m,e \
  --write --writes-all-of m,e --limit 5
</code></pre>
<p>The <code>--writes-all-of m,e</code> flag restricts the answer to candidates that write both named fields. The key words here are <em>through the same pointer</em>. Two writes at offsets eight and sixteen from unrelated bases do not belong to the same object just because the numbers match. The query looks for writes at the <code>m</code> and <code>e</code> offsets relative to a common root. The root is the pointer the offsets are measured from; what matters next is whether it was an argument, a call result or a load from memory.</p>
<p>The first candidate starts at <code>0x1210</code>. In the transcript, its instructions are:</p>
<pre><code class="language-asm">0x1210  endbr64
0x1214  add [rdi+8], rsi
0x1218  xor [rdi+0x10], rsi
0x121c  ret
</code></pre>
<p>This matches the two updates in the source. Both instructions read the field's previous value and write back the result. The report marks them as <code>read_write</code> with a width of eight bytes. In the reference build's symbol table, the function is called <code>writer</code>. The <code>read_write</code> label by itself gives no ranking advantage: in the decoys, the <code>xor</code> instruction also reads and writes the field.</p>
<p>The next candidate looks plausible too. It writes <code>[rdi+8]</code> with <code>mov</code> and updates <code>[rdi+0x10]</code> with <code>xor</code>. The rest of the displayed decoys are built the same way. Their offsets really do match. These are not imaginary coincidences, and the instructions were not misread.</p>
<p>That is the core difficulty. A matching combination of offsets establishes a local access pattern. It does not prove that the pointer has the source type whose serializer we looked at, or that the function takes part in the operation we care about. The report calls this <code>same_root_offset_constellation_not_type_proof</code>.</p>
<p>If you present the answer as a list of recovered types, the ambiguity disappears from the formatting but stays in the machine code. I would rather see the candidates and state the next question explicitly.</p>
<h2>What ranking gives you, and what it doesn't</h2>
<p>In the recording, the real function scores 50 points and the displayed decoys score 40. Start with the shared part, which comes from <a href="https://github.com/alex09x/xref/blob/v0.4.0/src/serde_fields/writer_rank.rs">score calculation in XRef 0.4.0</a>:</p>
<ul>
<li>16 points: two writes to the target fields in the same block;</li>
<li>another 16: both writes start exactly at the target field and are no wider than the observed read of that field;</li>
<li>another 8: two fields of the layout are covered by such precise writes.</li>
</ul>
<p>That makes 40 for the real function and for the decoys alike. None of them compares a field before writing it, which would earn an extra bonus. Pointer origin is the same too: they all write through the first argument.</p>
<p>The ten-point difference comes from the direct call graph alone. <code>main</code> calls both <code>serializer</code> and <code>writer</code>, so the two are two edges apart through a shared caller. At distance two, the <a href="https://github.com/alex09x/xref/blob/v0.4.0/src/serde_fields/accessors.rs#L305-L310">call-graph bonus</a> is <code>(7 - 2) * 2 = 10</code>. The decoys have no callers and get no bonus.</p>
<p>That advantage is built into the test program: the decoys were deliberately left without callers. If they also sat next to the serializer in the graph, this signal would separate them less well. Here it shows why the known function came out on top. The path <code>serializer ← main → writer</code> describes relationships in the program; it is not a call to <code>writer</code> from the serializer, and it is not a trace of a particular execution.</p>
<p><img src="https://prod.codes/img/xref/re-ai-ranking-evidence.png" alt="The serializer and the writer share a caller; the decoys are disconnected." /></p>
<p><em>Figure 3. In the reference example, the shared caller gives <code>writer</code> ten extra points: 50 versus 40 for the decoys.</em></p>
<p>Development, however, also taught the opposite lesson: call-graph proximity alone is not enough. <a href="https://github.com/alex09x/xref/pull/118">PR 118</a> describes a case where the layout was already being found, but copy and initialization code sitting next to the serializer ranked above the right function: it wrote at matching offsets and collected the same proximity advantage. The answer was field-precision scoring. A write counts as precise only if it starts at the field's offset and is no wider than the observed read, so wide copies and dereferenced pointer slots earn no field-precision points; <a href="https://github.com/alex09x/xref/pull/120">PR 120</a> later excluded constant stores as well, because zero initialization should not score like writing a computed value. Comparing the field before writing it is a separate signal.</p>
<p>That is the difference between the demonstration and the engineering problem it came from. In the open example, <code>writer</code> and the displayed decoys earn the same 40 points for their writes, and the call graph makes the difference. In the harder case, field updates first had to be told apart from copying, initialization and pointer handling. A position in the list suggests which function to check next; it does not replace the check.</p>
<h2>Ask where the base pointer came from</h2>
<p>Version 0.4.0 provides <code>--writer-root</code> to distinguish bases that come from an argument, a call result or a memory load. That is useful when the investigation already has a reason to prefer one origin.</p>
<p>For example, if the object you care about comes from a lookup call, a candidate whose base is the returned pointer may be more relevant than an unrelated write relative to an argument. That is a hypothesis about the program's data flow, not a license to pick whichever filter produces the nicest first row.</p>
<p>The public recording includes a query rooted at the argument:</p>
<pre><code class="language-sh">xref -b target-stripped field --keys c,m,e \
  --write --writes-all-of m,e \
  --writer-root argument --limit 3 --json
</code></pre>
<p>In this example it does <strong>not</strong> remove the decoys. All of these functions, the real one included, work through the first argument. The candidate order stays the same. That is an important negative result: a useful filter does not have to be useful on every input.</p>
<p>So I first find out where the object comes from on the path I care about, and only then choose a filter. Swapping flags in until the first row looks right is an easy way to fit the answer to a guess.</p>
<h2>One function, different call sites, different questions</h2>
<p>The function we found answers part of the question: where the matching writes are. The next step is to work out from where, and under what condition, it gets called. That is a continuation of the investigation after <code>field</code>, not a decision the command makes on the researcher's behalf.</p>
<p>In this teaching example, one function is called with different arguments:</p>
<pre><code class="language-c">if (condition_a) {
    update(object, 0);
} else {
    update(object, 1);
}
</code></pre>
<p>There is one callee, but the branch condition and the argument differ. The recorded target contains no such branch; the snippet stands for the real binaries where one utility function serves several callers under different conditions. Pick the first call from the cross-reference list, and you have discarded the alternative before explaining it. You need to look at the specific call site and where the argument comes from.</p>
<p>These are separate questions, and <code>callers</code>, <code>args</code> and <code>slice</code> serve them: call sites, the values passed, and an instruction slice leading up to the point of interest. Choosing between branches stays with the researcher. That sequence is more useful than a single function found with no explanation of its role.</p>
<h2>Look at the candidate from another angle</h2>
<p>The investigation does not end once <code>0x1210</code> comes out on top. I look at the instructions, read the pseudo-C and check the callers:</p>
<pre><code class="language-sh">xref -b target-stripped asm 0x1210
xref -b target-stripped c 0x1210
xref -b target-stripped callers 0x1210
</code></pre>
<p>The recorded pseudo-C contains these operations:</p>
<pre><code class="language-c">*(u64 *)(arg_0 + 8) = (*(u64 *)(arg_0 + 8) + arg_1);
*(u64 *)(arg_0 + 16) = (*(u64 *)(arg_0 + 16) ^ arg_1);
</code></pre>
<p>This is XRef's pseudo-C notation, not portable C source recovered verbatim. It conveniently shows the addition and the XOR that are visible in the instructions. The <code>callers</code> output finds a direct call at <code>0x1085</code> inside <code>0x1050</code>; in the reference build, that function is <code>main</code>.</p>
<p>Each view here answers its own question: pseudo-C helps you read the operations, the disassembly shows the instructions, and <code>callers</code> shows the link to calling code. All of them come from the same tool; the final check for this test comes from the open source and the reference build's symbols.</p>
<p>That closes the loop on the example: the expected offsets, operations and caller all match, and the right function is ranked first.</p>
<p>One of these practical checks is exactly what exposed an earlier XRef bug: when reconstructing a branch after an <code>SBB</code> that wrote to a stack slot, the analyzer used the wrong value for the carry. The public <a href="https://github.com/alex09x/xref/issues/117">issue 117</a> contains a self-contained assembly and C reproducer. On input <code>(10,6,1)</code> the executable returns <code>4</code>, while the pseudo-C XRef produced at the time would return <code>99</code> if interpreted literally: the carry check used the already-modified left operand.</p>
<p><a href="https://github.com/alex09x/xref/pull/119">PR 119</a> fixed the flag calculation by keeping the operand's value from before the write. The bug was recorded in XRef 0.3.0 and a pre-release build; the fix landed before the 0.4.0 release used in this walkthrough. This is a historical reproducer that I did not rerun for this article. It shows why, after you find a function, you check the instruction that carries the semantics: what exactly was read, when the value changed, and which value the flag was computed from.</p>
<h2>An empty answer should explain itself</h2>
<p>Back to the empty answer this article started with. The <code>not_found_under_limits</code> flag describes the outcome of the query but does not tell you what to do next. Look for other strings? Raise the budget? Take the unsupported region apart by hand?</p>
<p>That is why PR 77 added diagnostics: which keys were seen, where references and value passes were found, which functions were analyzed, and what limited the search. Alongside them came <code>--max-functions</code>, <code>--max-instructions</code> and partial matching via <code>--min-keys</code>. That turns a negative answer into grounds for the next action.</p>
<p>The recording also contains a separate cold JSON query, run with an empty cache:</p>
<pre><code class="language-sh">xref -b target-stripped field --keys c,m,e \
  --write --writes-all-of m,e --limit 1 --json
</code></pre>
<p>This cold JSON query with <code>--limit 1</code> reports 2,007 scanned functions. Of those, 2,003 were checked for candidate writes, and four were excluded by the image-scan prefilter, the pass that rejects functions before the per-candidate write analysis runs. The three selected functions were analyzed with no exclusions for unsupported control flow or the instruction budget.</p>
<p>But the output is still limited. In the same response, the <code>accessors_truncated</code> field is <code>true</code>. That means there are more candidate writers than were printed. The rest have not been judged irrelevant. A limit on how the result is presented and a limit on analysis coverage must not be confused.</p>
<p>I look at coverage and limits first, then interpret the result. An empty list after incomplete analysis is a reason to refine the query. A single printed row with <code>accessors_truncated: true</code> is a reason to look at the other candidates, not to declare the winner the only one.</p>
<h2>Fast repeats: what can be cached and what can't</h2>
<p>Coverage affects more than how you read an answer. It also decides whether the index that was built can be saved. During development, repeated queries ran into exactly this boundary: the key scan hit the upper limit on the number of functions and never finished, so the index was never written and the next query started from scratch. PR 118 covers that root cause.</p>
<p>Saving half an index as if it were complete would be worse: a fast answer could rest on an unfinished picture and still look complete. So analysis limits and safe reuse are tied together. Speeding up this path has to preserve what the result means and report coverage explicitly.</p>
<p>For a researcher, repeats come in different kinds too. A new set of keys can reuse a ready index of references, calls and writes. A long-lived session keeps the image and its state in memory. An identical question can be served from a saved report. All three modes are useful, but "answer a new question" and "return the previous answer" are different jobs.</p>
<p>The recording explicitly disables ready-result replay and the background daemon. Every cold command uses an empty cache directory; one separate query reuses the index built by the previous cold writer search. Its JSON reports one cache hit each for the scan index and the writer index. For the cold runs, those values are zero.</p>
<p>Those hits show that reuse worked. A single run with a different filter and limit cannot measure how much it saves: a comparison would mean running the same work many times. But the engineering boundary is already clear: you need to know whether you are reusing a complete index, session state or a finished report.</p>
<h2>A workflow you can hand to an agent</h2>
<p>The useful unit of work is a short sequence of questions, not an instruction to keep searching until something plausible turns up:</p>
<ol>
<li>Pin down the exact input file and tool revision. For every address, record which file it came from.</li>
<li>Find where the keys are passed and look at the witness instructions. Check the base and the address-versus-value distinction.</li>
<li>Record the observed offsets and widths without inventing a complete source declaration.</li>
<li>Ask for writers at the target offsets through a single root. Keep the candidate and coverage details.</li>
<li>Justify the ranking or the root filter with a specific hypothesis about the program.</li>
<li>Look at the selected candidate's instructions and callers. Use pseudo-C to read, not as a substitute for instruction evidence.</li>
<li>State the remaining uncertainty and the observation that would help resolve it.</li>
</ol>
<p>JSON makes it easier to pass these steps between tools. It does not relieve the agent of following them in order. Evidence for a field lives in <code>candidates[].fields[].emissions[].evidence_steps[]</code>, and candidate writers in <code>accessor_candidates[]</code>. The caller can keep these anchors without collapsing everything into a sentence about a recovered type.</p>
<p>If you later need to show which path actually executes, a static report is not that observation. Examining a running process is a separate step with its own platform, access and process-impact constraints. This recording involves no attaching to a live process and no observing writes at run time.</p>
<h2>What I took away from this</h2>
<p>The tempting shortcut is to jump from a name to a type, from an offset to an object, or from the first candidate to a proven writer. Each of those jumps erases exactly the distinction that made the original question hard.</p>
<p>The public example checks a useful result: three keys lead to three offsets, the offsets lead to candidate writers, and the known writer comes out first for reasons the report spells out. The two thousand decoys are an essential part of the explanation. They show why finding the writes was not enough.</p>
<p>This is the direction I want to take XRef: make a narrow question about a binary cheap enough to ask again and again, and return enough evidence that the next question becomes clearer. The goal is not to make uncertainty disappear, but to stop hiding it inside an attractive answer.</p>
<p>You can repeat the walkthrough on the same public example: the same binary, the same commands, the same checkable answer, with links in the sources below. XRef is an open project: <a href="https://github.com/alex09x/xref">source code</a>, with installation and documentation at <a href="https://xrefcode.com/">xrefcode.com</a>.</p>
<h2>Sources and reproduction</h2>
<p>The full recording contains stdout, stderr, exit codes, environment details and a reproduction script. The shortened listings above keep the recording's addresses; they belong to that exact image and may change on a rebuild.</p>
<ul>
<li><a href="https://github.com/alex09x/xref/tree/2f01e7ba8d9ccfab08f055cce3119e95dd1504bb/docs/walkthroughs/field-synthetic">Public recording and full walkthrough with source-backed checks</a>.</li>
<li><a href="https://github.com/alex09x/xref/blob/2f01e7ba8d9ccfab08f055cce3119e95dd1504bb/docs/walkthroughs/field-synthetic/transcript.txt">Verbatim transcript</a> and <a href="https://github.com/alex09x/xref/blob/2f01e7ba8d9ccfab08f055cce3119e95dd1504bb/docs/walkthroughs/field-synthetic/environment.txt">environment</a>.</li>
<li><a href="https://github.com/alex09x/xref/releases/tag/v0.4.0">XRef 0.4.0 release</a> and <a href="https://doi.org/10.5281/zenodo.23968336">versioned software archive</a>.</li>
<li><a href="https://github.com/alex09x/xref/blob/1686ee4b84e63423beed2109b6ac3bf03df0e535/docs/field-search.md">Field search documentation, corrected after the release</a>.</li>
<li><a href="https://prod.codes/blog/xref-0-4-0-from-serialized-keys-to-the-writer/">Earlier short walkthrough</a>, based on the same recording rather than an independent experiment.</li>
<li><a href="https://xrefcode.com/">XRef website</a> with installation, documentation and recorded examples.</li>
</ul>
<p>The test program was generated with <code>--functions 2000 --writers</code>; its source, generator and input file hashes are published in the recording. This article does not report a new benchmark, a comparative accuracy study or production measurements.</p>
<p><em>Originally published at <a href="https://prod.codes/blog/reverse-engineering-with-ai-your-decompiler-can-lie-to-you/">prod.codes</a>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Your AI Agent Has Amnesia. Mine Keeps Receipts.]]></title><description><![CDATA[How KYB uses Git-backed records, lifecycle states and temporal queries to share operational memory across AI agent sessions, with practical limits.]]></description><link>https://alex09x.hashnode.dev/your-ai-agent-has-amnesia-mine-keeps-receipts</link><guid isPermaLink="true">https://alex09x.hashnode.dev/your-ai-agent-has-amnesia-mine-keeps-receipts</guid><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[Git]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[Alexander Panasenko]]></dc:creator><pubDate>Fri, 06 Nov 2026 05:49:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6aed2df3d203f7f9dba8c01f/7e04b859-88df-4748-ad11-fa2843928736.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>TL;DR.</strong> Scattered Markdown notes kept sending my agents down paths we had already rejected. I built a shared Git-backed store (KYB) with versioned records and lifecycle states — active, archived, retracted. Query current state, history, or a time slice separately; replace wrong text instead of appending; separate finished work from wrong knowledge. Pinned to KYB v0.3.3, no controlled benchmarks — mechanisms and field notes below.</p>
</blockquote>
<p>Before KYB, my coding agents already had memory: Markdown files scattered across different places. The flaw showed up whenever another agent returned to a workspace, found an older note, and mistook it for the active plan. In my experience, an agent would sometimes spend hours pursuing an approach we had already investigated and rejected. The file still held a perfectly reasonable case for the old approach, while the decision to abandon it lived somewhere else.</p>
<p>I am Alexander Panasenko, the author of KYB (Know Your Business), so I have a stake in this story: it is my open-source project. This is an account of my engineering experience with Claude Code, OpenAI Codex, and Google Antigravity from summer through October 2026. The text, its translation, and the diagrams were prepared with AI assistance, based on my own experience, the public source code, and saved development records.</p>
<p>A fresh session does not automatically inherit the conversation that just ended. A human had to step in and explain which file still mattered, why we changed course, and where the outcome was recorded. An obvious temptation is to write one more summary note, but that note needs remembering too: without updates, it goes stale like the rest.</p>
<p>So here is the question this piece addresses: how do you preserve operational knowledge across sessions without letting an outdated note send the next agent down a path you already abandoned? My short answer is to save a little, with versions and lifecycle state, and keep three things apart: what is true now, what was recorded then, and how an attempt ended. The rest of this piece shows where each boundary came from.</p>
<p>The mechanics below are pinned to the public <a href="https://github.com/alex09x/kyb/releases/tag/v0.3.3">KYB v0.3.3</a> release from October 27, 2026 (commit <code>06f63b5</code>). The episodes below keep their dates: new features are not attributed to old versions, and the current <code>main</code> branch may differ. Our internal records are not public. I have not run a controlled comparison of agents with and without KYB, so the hours lost to stale notes are my personal recollection, not a measured saving after adoption.</p>
<h2>Why Files Were Not Enough</h2>
<p>In hindsight, the problem had three parts. First, there was no single shared entry point: notes lived in different places, and none was canonical across sessions. Second, a note had no explicit lifecycle state: a file describing a rejected plan looked just like one describing the active plan. Third, separate files do not by themselves establish which version of a decision is authoritative: even when notes are already tracked in Git, an agent reading a file in a working tree or a neighboring note cannot tell without a separate check whether that text superseded an earlier approach or was superseded later.</p>
<p>My first real fix was simple: a shared service for operational facts, incidents, and tasks that every session could reach. Its first interface was a CLI on top of an HTTP API; agents know how to run commands, and that was enough to start. Later, the server gained Model Context Protocol (MCP) support so agents could call its tools directly. The CLI, HTTP API, and MCP all lead to the same records: three doors into one room, not three separate memories.</p>
<p>What KYB did not become matters as much. It does not save hidden model reasoning or archive whole conversations. A record exists only when a human or agent explicitly saves it: knowledge under a stable key, an incident with detection and outcome, or a task with owner and status. Each record is a Markdown file in a Git repository. The search index is built on top and can be rebuilt; the repository stays canonical. I chose small, verifiable records over an automatic chat archive on purpose: an archive has everything, but finding the active conclusion in it is as hard as in a pile of files. One more distinction: the public repository holds the engine, while our working records live in a separate internal store. Installing KYB gives you your own store under your own access rules, with none of our notes.</p>
<h2>What to Record So Nobody Repeats a Rejected Path</h2>
<p>The shared server solved place, not content. Pour the same old notes into a database and search just finds stale text faster. So the next question was: what should survive a session?</p>
<p>The most useful record usually answers a question source code cannot: why was it built this way? Code shows the chosen implementation, not which options were tried, where they fell short, or which constraint decided the outcome. Without that, the next agent may carefully rebuild a rejected design while sincerely believing it is an improvement—exactly my original problem.</p>
<p>Material therefore falls into three kinds. <strong>Knowledge</strong> is a state or rule worth returning to: a service's role, an interface agreement, a way to verify configuration. An <strong>incident</strong> is a problem that happened: symptom, impact, how to tell whether it continues, and how it ended. A <strong>task</strong> is work to be done, or the reason it was closed. The split keeps you from confusing an intention with a fact, or a finished attempt with an active recommendation.</p>
<p>Take an example record about choosing a message queue. "We use option B" says too little. The next agent does not need to obey an old conclusion blindly; it needs to tell whether the conditions behind it still hold. I would write such a record roughly like this (a template, not a real benchmark):</p>
<pre><code class="language-text">Decision: Use option B for this workload.
Reason: Option A failed requirement X under conditions Y.
Verification: Link to test, configuration, or report with revision SHA.
Revisit if: X or Y changes.
Related: Task key where the options were compared.
</code></pre>
<p>The "Revisit if" line turns an old decision from a prohibition into a condition: if X or Y has changed, revisiting is legitimate, and the agent does not have to guess whether the record expired.</p>
<p>The same logic produced habits I now treat as part of the record. A claim without a source soon becomes one more text asking to be trusted, so note how and when a fact was checked: not "the API listens on 9090," but "at revision X, checked the <code>service-api</code> listener, port 9090." Before creating a record, search for an existing key; the same knowledge under two names recreates the old chaos inside the new system, and Git will not clean it up. Hunches and long logs of competing hypotheses belong in an open task or incident, not standing knowledge. Secrets do not belong in the store at all; a pointer to where an authorized person or tool can fetch them suffices.</p>
<p>In v0.3.3, there is also a guard against silently losing tags: rewriting a task or incident keeps any tags the write omits. An explicitly empty list clears them. This is a narrow rule for metadata, not a partial update of any record: replacing the knowledge under an existing key still requires keeping the full current text.</p>
<h2>One Port, Different Questions</h2>
<p>Even a clean record serves different questions. In a toy example, Monday's entry records that <code>service-api</code> listens on port <code>8080</code>. On Wednesday, the service moves to <code>9090</code>, and the record is updated under the same key. "Where should I connect right now?" and "What was recorded on Monday?" need different answers, and both are fair questions.</p>
<p>Overwrite the row without history and Monday's answer is gone. Return every version without dates or states and an agent may pick the wrong one. Git offers a third path: the key stays the same while its edits form a history. Still, history alone answers nothing until you say which slice you need.</p>
<p>Each KYB query mode matches one question. A regular query searches current state: "where do I connect now." <code>--history</code> searches matching text across all saved content versions, including the current one; <code>kyb history &lt;key&gt;</code> shows a record’s complete chain of changes. <code>--as-of</code> builds a slice of the store at a chosen moment and searches inside it, telling you what the store knew when a release went out. <code>--changed-between</code> searches versions written within an interval and returns, for each key, the latest matching version from that window.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6aed2df3d203f7f9dba8c01f/e72e6ce3-ca3d-4240-ad1c-6a0bd2fd1b52.png" alt="Toy port change: the current query selects the new version, as-of before the change selects the prior one, history may return both, and export writes a commit snapshot with the latest version of each key." style="display:block;margin:0 auto" />

<p><em>Figure 1. A toy example with synthetic ports; R1 and R2 are Git commits, not real-world events.</em></p>
<p>Figure 1 maps those answers onto one key. The current query gets R2 and port 9090; a cutoff between the commits gets R1 and port 8080; history may return both if both match; and export, discussed below, writes one latest row per key.</p>
<p>The old port appears in historical results by design, not by mistake. Pick the question and mode first; judge the hits second.</p>
<blockquote>
<p><strong>A note for readers checking this against the code.</strong> In <code>--as-of</code>, KYB first selects, for each key, the latest version recorded no later than time T—in revision order, not by the largest timestamp—and only then tests it against the query. A retraction recorded by T hides the key if the selected version is not newer than that retraction. In <code>--as-of</code> and at the upper bound of <code>--changed-between</code>, a bare date means 23:59:59 UTC; at the start of <code>--changed-between</code> it means 00:00:00 UTC. A SHA passed instead of a date becomes its commit timestamp rather than selecting an exact Git tree, and versions written within the same second cannot be distinguished by a time cutoff. See <a href="https://github.com/alex09x/kyb/blob/06f63b5ae0f8fdff24abb3b2c1c848ff41758faa/src/index.rs"><code>index.rs</code></a> and <a href="https://github.com/alex09x/kyb/blob/06f63b5ae0f8fdff24abb3b2c1c848ff41758faa/src/main.rs"><code>main.rs</code></a> in v0.3.3.</p>
</blockquote>
<h2>The Correction That Did Not Replace the Mistake</h2>
<p>The "slice first, match second" order came from practice: we hit the same problem twice, first in notes, then in code.</p>
<p>A shared server did not cure the habit of appending clarifications to the end of a document. A September internal record examined a case where a correction was added below an incorrect instruction while the instruction stayed on top. A human reading the whole page might catch the contradiction; an agent taking the first matching passage still got the old advice. It was the stale-Markdown trap again, except both versions now shared one page instead of sitting in different folders.</p>
<p>After that, our working rule got specific: edit the active text, then search for the retired wording. Checking that "the right paragraph was added" is too weak; you must confirm the wrong one no longer reads as an instruction. Git removes the main reason to keep the old paragraph in the body, since earlier wording survives in history. And if the same instruction was once copied under another key, searching for a distinctive old phrase after the fix can help surface a duplicate within the searchable store—KYB does not deduplicate automatically or guarantee that every copy is found.</p>
<p>In October, we found a software version of the same mistake. Historical cutoff search first collected versions containing the query term and only then picked the latest. When a newer version no longer contained the old term, it dropped out of candidates too early, and the query returned old text even for a moment after replacement. In effect, the code did what the agent did with the page: it matched words without checking whether the passage still applied.</p>
<p>The fix in <a href="https://github.com/alex09x/kyb/pull/17">PR #17</a> of October 11 reversed the order: first select each key's state at the target moment, then test for a match. This is the order in effect in v0.3.3, the version this piece is written against. For full history search, old text is appropriate; for the state after a correction, it is the wrong answer. The patch and regression test are public; we did not measure how many real agent decisions the bug affected.</p>
<p>I covered both cases in more detail in a <a href="https://hackernoon.com/building-reliable-shared-memory-for-ai-agents-with-git-hybrid-search-and-temporal-retrieval">HackerNoon article</a>. The lesson for shared memory is simple: verify the whole path from correction to the next read. A successful write says nothing on its own about which passage the next query will return.</p>
<h2>Write Time Is Not Event Time</h2>
<p>Time slices tempt you to read the store's history as an event log. The same example shows why that is wrong. Say the service switched ports at 15:50, but the record was updated only at 16:20. An <code>--as-of</code> query for 16:00 returns the old port even though the cutover had already happened. The store was not wrong; it simply answers about write time, not event time.</p>
<p>KYB's temporal filters use Git commit time. That tells you what the store recorded and when; it does not replace deployment logs, metrics, or checking the running system. Incident fields do not prove event time on their own either. A client can supply <code>started_at</code> and <code>detected_at</code>; if <code>detected_at</code> is empty, the server keeps a stored value or fills in the current time. The server records the closing timestamp, <code>resolved_at</code>; clients do not supply that field. In an investigation, the store's history is one source, not the referee.</p>
<h2>Finished, or Wrong?</h2>
<p>Another boundary appears once a record stops being current: "this record no longer applies" can mean two very different things.</p>
<p>First, the work is finished. An incident was resolved, or a task was done or dropped, yet the next agent still needs to know how the attempt ended, especially if it failed. KYB archives such records: they leave the active tree, while their final version stays in regular current-state search. Closing a task as <code>done</code> or <code>dropped</code> requires resolution text, including why dropped work was abandoned. The field guarantees only that text exists, not that it is true or useful; a bare "done" tells a future reader nothing.</p>
<p>Second, the knowledge turned out to be wrong. Then the record is retracted: it stops answering regular queries while its Git history remains. To an agent, these are distinct signals: "work is finished and its outcome is worth reading" versus "this is no longer knowledge." Collapse both into a single delete or overwrite, and a reader cannot tell whether the old record disappeared because the job was done or because it was wrong—the exact ambiguity my old Markdown files created.</p>
<p>States also matter for the store as a whole. Version 0.3.1 (October 15) added <code>kyb export</code>, which writes a commit snapshot: one row per key, with its latest content and state (<code>active</code>, <code>archived</code>, or <code>retracted</code>). Export answers "which keys existed at that commit, and in what state?" without including earlier versions; for those, use history.</p>
<blockquote>
<p><strong>Interface details.</strong> By default, export uses the current commit; <code>--at</code> pins another. The command is available in the CLI and over HTTP (<code>GET /export</code>), but is not an MCP tool in v0.3.3; see <a href="https://github.com/alex09x/kyb/blob/06f63b5ae0f8fdff24abb3b2c1c848ff41758faa/src/export.rs"><code>export.rs</code></a> and <a href="https://github.com/alex09x/kyb/blob/06f63b5ae0f8fdff24abb3b2c1c848ff41758faa/src/mcp.rs"><code>mcp.rs</code></a>. <code>--changed-between</code> returns content versions written inside the window, not retraction events. Whether a key was retracted can be checked from its history or an export snapshot.</p>
</blockquote>
<h2>A Task You Can Find Too Late</h2>
<p>Keeping a history of decisions still leaves another problem: open commitments. Even a correctly saved task is useless if you remember it after its deadline. The store may hold an exact date while an ordinary search for a service never shows it: the human or agent was asking about something else.</p>
<p>Deadlines arrived on October 27: v0.3.2 added them, and <a href="https://github.com/alex09x/kyb/releases/tag/v0.3.3">v0.3.3</a>, released the same day, refined them and added their display in the console. A task can now carry a due date, a marker for external consequences of missing it, and a reminder window. If the date depends on a future event, it can be kept as a provisional one.</p>
<blockquote>
<p><strong>Task fields.</strong> The due date is stored in <code>due</code>, the marker for external consequences in <code>hard</code>, the reminder window in <code>remind_days</code>. A provisional date is set through <code>expected</code> together with <code>waits_on</code>. A deadline field omitted on rewrite keeps its previous value; moving a date remains a version in Git.</p>
</blockquote>
<p>Take an example task about renewing a certificate. Writing "by December 15" in its body is not enough: to the search engine, that is just text. The <code>due</code> field lets a date be compared with a calendar, and <code>hard</code> explains why missing it matters. Successful search results, record reads, and task and incident lists can carry a <code>deadlines</code> block from the server: up to five overdue or soon-due open tasks. This selection is not limited to records matching the original question. Overdue items come before future ones; within each group, <code>hard</code> deadlines come first. The count of remaining records is shown separately. In the CLI, the reminder goes to stderr while the JSON answer stays on stdout. The block can be switched off, and if computing it fails, the main answer may arrive without it: the absence of a reminder does not prove there are no commitments.</p>
<p>For a "today" deadline, the reader's calendar matters. The CLI and the console send their own date; without one, the server uses its own. Historical queries work the other way: in <code>--as-of</code>, a bare date means the end of the day in UTC.</p>
<p>In the v0.3.3 web console, deadlines appear in a strip under the header, in the task queue, and in a separate by-date view. You can open a task, move its date, or close it with an outcome. But neither the strip nor the JSON does the work: KYB does not run a task on a schedule, repeat it, or send notifications. An agent still has to call the tool, read the reminder, and react — a limited way to make commitments more visible while reading, not proof that we miss deadlines less often.</p>
<h2>Finding Is Not Knowing</h2>
<p>Clear states still leave the last stretch: finding the right record. KYB searches current records by words and, if a model is loaded, by meaning. Historical versions in v0.3.3 are searched by words only: vectors exist only for current versions. Comparing the quality of current and historical results without accounting for that difference would mislead. The API response reports whether semantic search took part in a query.</p>
<blockquote>
<p><strong>How search works.</strong> Tantivy performs BM25 keyword search, and the multilingual-e5-small model, via ONNX Runtime, adds semantic candidates. The two lists are merged by reciprocal rank fusion. Without a model, only keyword search remains.</p>
</blockquote>
<img src="https://cdn.hashnode.com/uploads/covers/6aed2df3d203f7f9dba8c01f/7fa656ea-83bc-483f-8f90-c16f63dc99a7.png" alt="KYB 0.3.3 diagram: CLI, HTTP API, and MCP talk to a single server; Git stores revisions, the search index derives from them, and current and temporal queries use different search paths." style="display:block;margin:0 auto" />

<p><em>Figure 2. One canonical store, several interfaces, two search paths.</em></p>
<p>Figure 2 puts the pieces together: three client interfaces lead to one server, Git holds canonical versions, the index derives from them, and current and temporal queries take different paths. The web console uses the same HTTP API; its on-screen queues are not shown here. No diagram can guarantee that an agent will ask at all. A hit shows what was saved, not that it is true, and an uncalled tool never helps. How much that matters, I learned from the agents themselves.</p>
<h2>What the Agents Told Us</h2>
<p>On October 10, 2026 (PDT, UTC−7), I asked five internal coding agents how the shared store fit into their work. Four replied in writing; the replies live in an internal record committed October 11 UTC. This is a small qualitative survey, not a representative sample or usage telemetry. Still, one issue recurred: KYB works on demand, whereas some memory files load into context automatically at session start. The tool can be available while the agent forgets to call it, so part of our durable knowledge kept living in separate files instead of the shared store.</p>
<p>My original problem returned from the other side: before, an agent found an old file and trusted it; now it had a path to the current record but no prompt to take it. The agents asked for a short digest of workspace records at session start and an explicit verification status: who confirmed a fact, when, from what source, and at which revision. They also noted that behavioral rules for agents are awkward to model as ordinary knowledge. This is user feedback, not proof that automatic context injection improves accuracy. When that record was written, automatic <code>kyb context</code> was only a proposal, and the v0.3.3 server does not provide it. The deadline reminders described above arrived only after this survey, in late October. They return part of the current commitments, not the whole project context.</p>
<p>Separately, I tested provenance: when different agents write records, you need to know which one did. On October 11, I ran trial writes via the CLI from Claude Code, Codex, and Antigravity, and via MCP from Claude Code, using the attribution mechanism from <a href="https://github.com/alex09x/kyb/pull/16">PR #16</a>. Git history recorded labels for agent, session, workspace, and write transport, confirming those paths. In v0.3.3, the CLI refines this attribution: it walks up the process tree and picks the nearest agent with its own session. For example, Claude Code inside an Antigravity terminal should not be recorded as Antigravity; an explicit <code>KYB_AGENT</code> takes priority. Because clients supply the labels, they help audit changes but do not provide cryptographic identity or prove regular store usage.</p>
<p>An episode before the survey showed what a shared dependency costs. On October 8, an agent reported that several <code>kyb query</code> calls exited with no output after an external timeout expired. On October 11, repeat queries answered in under a second. Logs did not cover the first report, and nobody checked stderr or server status then, leaving the cause unknown—server, network, or client call. I treat this as an agent's report, not a reproduced defect or benchmark: when agents rely on a shared service, availability and clear timeout diagnostics become part of memory. The v0.3.3 CLI makes those diagnostics explicit: the exit code distinguishes success, a server rejection, a server error, and an unreachable server; the error goes to stderr, and stdout stays empty on failure. It still does not explain the October episode after the fact. If an agent checks only for text and ignores the exit code, even a good store history will not help.</p>
<h2>A Loop You Have to Practice</h2>
<p>The cases above and the agents' feedback point to Figure 3: a record must be written explicitly in one session and looked up explicitly in the next.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6aed2df3d203f7f9dba8c01f/608da3f9-90e7-480f-8243-884cefecab59.png" alt="The shared memory cycle: an agent in one session verifies a fact and explicitly saves a version in Git; the next session explicitly searches for the record and checks it against a current source." style="display:block;margin:0 auto" />

<p><em>Figure 3. A record survives the session, but it does not enter a new context window on its own.</em></p>
<p>In Figure 3, Session A at the top learns something: the agent observes a change, checks a source, separates fact from hypothesis, and explicitly saves a version under a stable key. Git keeps the revision, and the index derives from it. Session B below is the next session. Because it does not automatically inherit the prior chat, it starts with a specific question, queries in the right mode, checks the result against a current source, and only then uses, corrects, or retracts the record. This is the intended order, not observed behavior of every agent.</p>
<p>Three points matter most. First, the question: "Tell me everything about the project" returns too much text, while "Why did we choose option B for this operation?" or "How did the last outage on this service end?" gives a clear target. An empty result may just need a component name, key, tag, or different wording. Second, verification: code at the right revision for an interface decision, configuration or observed responses for a running service. A record does not replace checking the primary source; it helps you locate what to check and how. Third, the update: an agent that learns a recommendation no longer applies and leaves the fix only in the current chat sets the same trap for the next session. Updating the record under its existing key, with reason and source, belongs in a task's definition of done.</p>
<h2>What I Can Show, and What I Can't</h2>
<p>Everything above rests on public code or saved internal records. Back to an earlier version of the system — v0.2.0 from August 16. One field's history shows what such records preserve. An internal task described a backlog of over fifty open tasks with no explicit priority, forcing urgency into tags. Status filtering existed; importance ranking did not. The initial request named the field <code>severity</code>, by analogy with incidents. Discussion refined that: <code>severity</code> describes damage that already happened, while <code>priority</code> fits future work. <code>priority</code> shipped in <a href="https://github.com/alex09x/kyb/blob/fa1bd111711ff6568e15302019de0617117e2430/src/model.rs#L105-L107">KYB v0.2.0</a>—a concrete chain from friction to product change preserved in the store, not a measurement of agent speed.</p>
<p>What I lack is a reliable count of reads per agent, a link between reads and task outcomes, or a controlled comparison with and without KYB. That requires fixed tasks and models, a log of when each agent queried the store and what it got, and blind grading, knowing an available tool is not always called. Until such data exists, I describe mechanisms and observations, not causal effects.</p>
<p>This approach is not for everyone. A Git-backed shared store fits when several sessions or machines read and update changing knowledge and humans want history and diffs. For a single agent in one repository, a good README and a few project files are often enough. Git itself needs no server, but this design adds a shared server and an index to maintain, and the records still have to be verified and kept current. If your knowledge rarely changes, it may be one system too many.</p>
<h2>Back to the Old Note</h2>
<p>Recall the opening scene: an agent finds a well-written note and heads down a path we abandoned long ago. What would change if that note lived in a shared store?</p>
<p>The agent would not get smarter. Instead, the record would have one shared entry point, an explicit lifecycle state, and a queryable version history under a single key. If the plan was abandoned, its task can be closed with the reason and wrong knowledge can be retracted from current search, separating the record's status from an active instruction. If the plan evolved, the earlier text sits in history and the body holds the current one. If the question is about the past, a slice at the right moment answers about the past without mixing it into the present. None of this guarantees that an agent will interpret a hit correctly, and it helps only when the agent asks the question and checks the answer against the source.</p>
<p>The practical takeaway does not require installing KYB. Next time you correct a note meant for agents, in a file, a wiki, or a store of your own, check three things. Did you replace the wrong text instead of appending a fix below it? Can a reader tell whether the plan was canceled, completed, or turned out to be wrong? Did you record why the decision was made and when it should be revisited? Three yes answers will not guarantee that the next agent never takes a wrong turn, but they give the next reader—human or session—an explicit status, the reason behind the decision, and a source to check instead of a stale note masquerading as the active plan.</p>
<p>To see how KYB works, start with the <a href="https://github.com/alex09x/kyb/releases/tag/v0.3.3">v0.3.3 release</a> and the <a href="https://github.com/alex09x/kyb/blob/06f63b5ae0f8fdff24abb3b2c1c848ff41758faa/README.md">README of that version</a>, which cover prebuilt binaries, installation, and Docker Compose. For a first check, complete that setup, save one decision with a "Revisit if" line, then query it from a fresh session. For Docker, use a separate fresh checkout: Compose keeps its data in that checkout's <code>./data</code> directory.</p>
<hr />
<p>Originally published on <a href="https://prod.codes/blog/your-ai-agent-has-amnesia-mine-keeps-receipts/">prod.codes</a>.</p>
]]></content:encoded></item></channel></rss>