<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Andre</title>
    <description>The latest articles on DEV Community by Andre (@olund).</description>
    <link>https://dev.to/olund</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4052635%2F364d9a05-51b8-4972-aa2d-914351e31938.jpg</url>
      <title>DEV Community: Andre</title>
      <link>https://dev.to/olund</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/olund"/>
    <language>en</language>
    <item>
      <title>Audition the voice before you commission the work</title>
      <dc:creator>Andre</dc:creator>
      <pubDate>Wed, 29 Jul 2026 10:15:11 +0000</pubDate>
      <link>https://dev.to/olund/audition-the-voice-before-you-commission-the-work-39ic</link>
      <guid>https://dev.to/olund/audition-the-voice-before-you-commission-the-work-39ic</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://olund.dev/writing/audition-before-you-commission/" rel="noopener noreferrer"&gt;olund.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Part of my tooling generates long-form written content in a configured&lt;br&gt;
voice: an editorial identity that describes the audience, the register, the&lt;br&gt;
vocabulary, what the writer sounds like. The identity is prose in a config.&lt;br&gt;
And prose configs for tone have a nasty property: they are write-only. You&lt;br&gt;
describe the voice you want ("unhurried, curious, explains from first&lt;br&gt;
principles, never hypey"), the generator consumes it, and you learn whether&lt;br&gt;
your description &lt;em&gt;worked&lt;/em&gt; only after paying for a full generation run and&lt;br&gt;
reading the result. If the register is off, you edit adjectives and pay&lt;br&gt;
again.&lt;/p&gt;

&lt;p&gt;The feedback loop is the problem. Tuning a persona through full generations&lt;br&gt;
is like adjusting a recipe by catering a wedding each time. What I wanted&lt;br&gt;
was a taste: given this identity, say two lines in this voice, right now,&lt;br&gt;
for approximately nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The smallest possible agent
&lt;/h2&gt;

&lt;p&gt;The fix is a preview agent, and the design goal was to make it as close to&lt;br&gt;
free as an LLM call gets - because a preview you hesitate to run is a&lt;br&gt;
preview that does not get run. Everything about it is subtraction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No tools.&lt;/strong&gt; The agent cannot read files, browse, or search. Everything
it needs - the identity text and an optional topic - is passed inline in
the dispatch. This is not just cost control: an agent with no tools and
fully inline context is &lt;em&gt;reproducible&lt;/em&gt;. Same input, same class of output,
nothing ambient to drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A fast local model.&lt;/strong&gt; Auditioning a register is narrow work; it needs
fidelity to a style description, not reasoning depth. The call routes to
the cheapest tier in my worker runtime - a small model running on my own
machine. A measured audition costs about 500 tokens end to end, which on
local hardware rounds to zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A hard output schema.&lt;/strong&gt; The agent must return two to six sample lines,
each a sentence or two the voice would actually speak, and nothing else -
no headings, no stage directions, no commentary about the voice. The
schema is enforced at the call layer, so a malformed response retries
rather than reaching the UI. A preview is a contract, not a chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ephemeral by construction.&lt;/strong&gt; The route that serves it writes nothing:
no database row, no artifact, no history. Preview output that persists
becomes state - something to list, migrate, and clean up. The whole value
of a preview is that it evaporates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A timeout and one retry.&lt;/strong&gt; Ninety seconds, one attempt to recover,
then fail visibly. A preview that hangs is worse than one that errors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prompt side is one rule repeated three ways: match the identity's tone,&lt;br&gt;
register, and vocabulary precisely; each line must be a speakable line of prose&lt;br&gt;
on its own; do not perform meta-commentary. Sample lines that &lt;em&gt;describe&lt;/em&gt;&lt;br&gt;
the voice instead of &lt;em&gt;being&lt;/em&gt; the voice are the failure mode, and the&lt;br&gt;
instructions attack it directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop it creates
&lt;/h2&gt;

&lt;p&gt;In the settings UI, next to the identity editor, there is a sample button.&lt;br&gt;
Type an optional topic, click, and a moment later: a handful of lines in&lt;br&gt;
the configured voice. Edit the identity, sample again. The tuning loop&lt;br&gt;
drops from "generate a full piece, read it, wince" to seconds per&lt;br&gt;
iteration.&lt;/p&gt;

&lt;p&gt;Two UI decisions carry more weight than they look like they should:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The preview invalidates nothing.&lt;/strong&gt; It is a plain fire-and-return call&lt;br&gt;
with no cache updates, because it changes no state. Wiring a preview into&lt;br&gt;
the app's data layer as if it were a mutation is a category error that&lt;br&gt;
makes every preview cost a refetch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The empty state teaches.&lt;/strong&gt; With no identity configured, the button does&lt;br&gt;
not disable silently; the endpoint refuses with "identity is unset" and the&lt;br&gt;
UI says so. A preview feature whose precondition is invisible reads as&lt;br&gt;
broken.&lt;/p&gt;

&lt;p&gt;There is also a seam decision underneath: the route awaits the worker&lt;br&gt;
synchronously, bounded at two minutes, instead of returning a job id the&lt;br&gt;
client polls. Previews are interactive - the human is sitting there. The&lt;br&gt;
moment a preview needs a progress bar, it has failed at being a preview,&lt;br&gt;
so the API shape encodes the latency budget: if this cannot answer while&lt;br&gt;
the user watches, it should error, not stream status updates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a pattern, not a feature
&lt;/h2&gt;

&lt;p&gt;The general shape: &lt;strong&gt;when a system consumes a human-authored description&lt;br&gt;
and produces something expensive, insert the cheapest possible sampler&lt;br&gt;
between the two.&lt;/strong&gt; The description-to-output gap is where confidence&lt;br&gt;
quietly dies - you wrote the config, you &lt;em&gt;think&lt;/em&gt; it says what you mean,&lt;br&gt;
and the only verification on offer costs a full run.&lt;/p&gt;

&lt;p&gt;Samplers earn their place when they are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Instant enough to be reflexive.&lt;/strong&gt; Seconds, not minutes. The moment
sampling requires deciding whether it is worth it, iteration stops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free enough to be guiltless.&lt;/strong&gt; A local model or the cheapest API tier.
The task is narrow by design; use the narrowest worker that does it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constrained enough to be honest.&lt;/strong&gt; Schema-forced output in the target
format. A sampler that returns an essay about what it would do is
theater.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateless enough to be ignorable.&lt;/strong&gt; No persistence, no history, no
cleanup. Run it forty times; nothing accumulates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I now reach for this shape whenever a prose config drives generation:&lt;br&gt;
sample the persona before the piece, the summary style before the batch,&lt;br&gt;
the reviewer's severity before the review run. Each sampler is an&lt;br&gt;
afternoon of work, because subtraction is fast to build - the entire agent&lt;br&gt;
definition fits on one screen, and the worker runtime it rides on already&lt;br&gt;
existed.&lt;/p&gt;

&lt;p&gt;The quiet lesson underneath is about model routing. The instinct is to&lt;br&gt;
send every task to the strongest model available. But a preview's job is&lt;br&gt;
to be &lt;em&gt;representative and immediate&lt;/em&gt;, not maximal - and a small local&lt;br&gt;
model with a tight schema and inline context is more representative of&lt;br&gt;
"what will the configured voice sound like" than a frontier model&lt;br&gt;
improvising with more freedom. Match the worker to the narrowness of the&lt;br&gt;
task, and some tasks turn out to be nearly free.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>ux</category>
    </item>
    <item>
      <title>My agent system dreams at night, and that is where its memory comes from</title>
      <dc:creator>Andre</dc:creator>
      <pubDate>Wed, 29 Jul 2026 08:48:03 +0000</pubDate>
      <link>https://dev.to/olund/my-agent-system-dreams-at-night-and-that-is-where-its-memory-comes-from-27f0</link>
      <guid>https://dev.to/olund/my-agent-system-dreams-at-night-and-that-is-where-its-memory-comes-from-27f0</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://olund.dev/writing/dreaming/" rel="noopener noreferrer"&gt;olund.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My agents recall durable facts across sessions. None of those facts got&lt;br&gt;
there because an agent, mid-task, decided "this is worth remembering&lt;br&gt;
forever." Almost&lt;br&gt;
everything an agent thinks is memorable in the moment is noise a week&lt;br&gt;
later, and an agent given a direct write path to durable memory will fill&lt;br&gt;
it with confident junk.&lt;/p&gt;

&lt;p&gt;Instead, my system does what brains do: it consolidates offline. Twice a&lt;br&gt;
day, on a timer, a process wakes up, reads what happened since it last ran,&lt;br&gt;
and decides - slowly, with gates - what deserves to survive. I call it&lt;br&gt;
dreaming, and the name has turned out to be more than a joke: the design&lt;br&gt;
questions are genuinely sleep-shaped. What gets replayed? What gets&lt;br&gt;
promoted to long-term storage? What gets discarded? And what happens when&lt;br&gt;
the process misfires?&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape: a timer, not a daemon
&lt;/h2&gt;

&lt;p&gt;Dreaming is a one-shot process fired by a scheduler - a morning cycle for&lt;br&gt;
lighter work (recommendations, "what needs attention"), an evening cycle&lt;br&gt;
for the heavy memory-promotion pass. No resident daemon, no queue service.&lt;br&gt;
Each run is a fresh process that reads files, thinks, writes files, and&lt;br&gt;
exits. Everything in my stack is filesystem-first, and consolidation is no&lt;br&gt;
exception: if the machine is off, the cycle is skipped and the next one&lt;br&gt;
picks up the unprocessed range.&lt;/p&gt;

&lt;p&gt;Two operational rules matter more than the schedule:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exactly one machine dreams.&lt;/strong&gt; My state syncs across machines, and two&lt;br&gt;
consolidators writing the same durable files would conflict endlessly. One&lt;br&gt;
machine owns dreaming; the others read the results. Single-writer is the&lt;br&gt;
cheapest concurrency model that exists, and choosing it here removed a&lt;br&gt;
whole category of merge problems before they happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every cycle has a hard token budget.&lt;/strong&gt; The reflection step calls an LLM,&lt;br&gt;
and an unbounded loop over a busy day's events is an unbounded bill. A&lt;br&gt;
cycle gets a fixed input and output cap; hitting the cap mid-cycle means&lt;br&gt;
finish gating what you already produced, log the overrun, and let the next&lt;br&gt;
cycle continue from there. A runaway day costs a known maximum. The daily&lt;br&gt;
spend lands around one to three dollars, which I consider cheap for a&lt;br&gt;
memory that maintains itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline: cluster, reflect, score, gate
&lt;/h2&gt;

&lt;p&gt;The evening pass runs the day's episodic events - tool calls, session&lt;br&gt;
summaries, captured thoughts - through a fixed sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cluster.&lt;/strong&gt; Events are embedded and grouped by density. A durable fact
almost never comes from one event; it comes from the same theme
surfacing across sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reflect.&lt;/strong&gt; An LLM reads each cluster and proposes candidates: "these
events support the fact that X." Each candidate carries the ids of the
events supporting it, so provenance survives the whole trip.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Score.&lt;/strong&gt; Candidates get a confidence score and pass structural checks:
enough distinct supporting events, spread over enough time, not a
restatement of something already known.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge.&lt;/strong&gt; Two LLM-backed checks run against existing memory: does this
contradict a stored fact, and is it a duplicate?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promote or queue.&lt;/strong&gt; High-confidence, well-corroborated candidates can
be written to durable memory automatically. Everything else lands as a
card in a review queue where I accept or reject with a keystroke, and
applying accepted items produces a git commit - the memory file's
history &lt;em&gt;is&lt;/em&gt; an audit log.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The single most important design fact: &lt;strong&gt;the interesting engineering is&lt;br&gt;
entirely in steps 3 through 5.&lt;/strong&gt; Generating candidates is easy; any LLM&lt;br&gt;
over any event log will happily propose memories. The system's quality is&lt;br&gt;
decided by what it refuses to write. Durable memory pollution compounds -&lt;br&gt;
a bad fact gets recalled, believed, cited, and built upon by dozens of&lt;br&gt;
future sessions - so the write gate is where the paranoia belongs.&lt;/p&gt;

&lt;p&gt;Concretely: auto-promotion requires both a confidence threshold and at&lt;br&gt;
least three supporting events. A candidate with two supporters can be&lt;br&gt;
judged genuinely durable by the reflector, and it still cannot enter&lt;br&gt;
memory unattended - it routes to the review queue instead. The rule is not&lt;br&gt;
"two events are not evidence"; it is "two events are not enough evidence&lt;br&gt;
to skip the human."&lt;/p&gt;

&lt;h2&gt;
  
  
  Two failure stories worth their tuition
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The silent floor mismatch.&lt;/strong&gt; For a while, the clustering stage was&lt;br&gt;
allowed to form two-event clusters, but the scoring stage silently dropped&lt;br&gt;
any candidate with fewer than three supporting events. Every two-event&lt;br&gt;
cluster the reflector judged durable was structurally discarded - not&lt;br&gt;
rejected with a reason, just gone. The pipeline looked healthy: cycles ran&lt;br&gt;
green, promotions happened, nothing errored. It was simply quieter than it&lt;br&gt;
should have been, and quiet is the hardest defect to notice. The fix&lt;br&gt;
lowered the score floor to match the cluster floor and moved the&lt;br&gt;
three-event rule to the auto-promote boundary, where it belongs: the&lt;br&gt;
candidate now survives to the review queue and the human sees it. The&lt;br&gt;
general lesson: when two stages of a pipeline disagree about a threshold,&lt;br&gt;
the disagreement does not error - it silently shrinks your output, and&lt;br&gt;
you will attribute the quietness to "slow week" for months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The judges that aborted the dream.&lt;/strong&gt; The contradiction and duplicate&lt;br&gt;
judges originally propagated a hard LLM error - an outage, a quota blip -&lt;br&gt;
straight up, aborting the entire cycle and discarding all the clustering&lt;br&gt;
and reflection work before them. Meanwhile a &lt;em&gt;garbled&lt;/em&gt; LLM response was&lt;br&gt;
handled gracefully with a conservative default. That asymmetry made no&lt;br&gt;
sense: the transient network error was more destructive than the corrupted&lt;br&gt;
answer. The policy now is uniform: any LLM-backed stage that fails hard&lt;br&gt;
degrades to its conservative default (no contradiction found, assume&lt;br&gt;
novel) and the cycle completes. For a nightly batch process, resilience&lt;br&gt;
beats strictness - a conservative default risks one duplicate card in a&lt;br&gt;
review queue; an abort discards a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Consolidation grows skills, not just facts
&lt;/h2&gt;

&lt;p&gt;The part I did not plan and now value most: the same clustering that finds&lt;br&gt;
durable facts also finds &lt;em&gt;recurring work&lt;/em&gt;. When the cycle notices the same&lt;br&gt;
kind of procedure performed across at least three instances on multiple&lt;br&gt;
days with no skill covering it, it proposes one - and above a confidence&lt;br&gt;
threshold it auto-writes a draft skill file. The draft is inert: invisible&lt;br&gt;
to every agent harness until I explicitly promote it. The human gate did&lt;br&gt;
not disappear; it moved from "write the draft" to "activate the draft,"&lt;br&gt;
which is a cheaper place for me to pay attention.&lt;/p&gt;

&lt;p&gt;This is the sleep metaphor completing itself. Consolidation is not just&lt;br&gt;
deciding what to remember; it is noticing what you keep doing and turning&lt;br&gt;
it into ability. The facts feed recall, the skills feed behavior, and both&lt;br&gt;
come out of the same nightly replay of the day's events.&lt;/p&gt;

&lt;h2&gt;
  
  
  What transfers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separate the write path from the work path.&lt;/strong&gt; Agents mid-task are the
worst judges of durability. Let them capture freely into an append-only
log; let a slower, gated process decide what becomes permanent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the engineering into refusal.&lt;/strong&gt; Candidate generation is free;
memory pollution compounds. Corroboration floors, contradiction checks,
and a human queue for everything below the bar are the product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move human gates to the cheapest checkpoint; do not delete them.&lt;/strong&gt;
Auto-draft plus manual activate beats both manual-everything and
full autonomy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch processes should degrade, not abort.&lt;/strong&gt; A conservative default
wastes a little; a dead cycle wastes the day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit your pipeline for silent disagreements.&lt;/strong&gt; Two stages with
inconsistent thresholds produce no error, only quiet. Count what enters
and exits each stage, and alarm on structural drops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap the spend structurally.&lt;/strong&gt; A per-cycle token ceiling with graceful
overrun turns "LLM loop over unbounded input" from a risk into a line
item.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>agents</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Giving my agent a map of the codebase (drawn partly from its own footsteps)</title>
      <dc:creator>Andre</dc:creator>
      <pubDate>Wed, 29 Jul 2026 08:36:53 +0000</pubDate>
      <link>https://dev.to/olund/giving-my-agent-a-map-of-the-codebase-drawn-partly-from-its-own-footsteps-1lm</link>
      <guid>https://dev.to/olund/giving-my-agent-a-map-of-the-codebase-drawn-partly-from-its-own-footsteps-1lm</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://olund.dev/writing/code-map/" rel="noopener noreferrer"&gt;olund.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Watch a coding agent land in an unfamiliar repository and you will see it do&lt;br&gt;
what a new hire does on day one: grep for names, list directories, open&lt;br&gt;
files, and slowly assemble a mental model. Unlike the hire, the agent does&lt;br&gt;
this &lt;em&gt;every session&lt;/em&gt;. The model it builds is discarded when the context&lt;br&gt;
window closes, and the next session pays for it again - in tokens, in&lt;br&gt;
latency, and in the wrong turns an incomplete picture produces.&lt;/p&gt;

&lt;p&gt;My fix is a per-project &lt;strong&gt;code map&lt;/strong&gt;: a queryable navigation layer each&lt;br&gt;
repository carries, rebuilt incrementally, that a new session receives a&lt;br&gt;
digest of before it reads a single file. The interesting part is not that it&lt;br&gt;
exists - static code intelligence is a mature field - but which three&lt;br&gt;
signals it fuses, because the third one is something most tooling cannot&lt;br&gt;
see at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three signals, one map
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Structure&lt;/strong&gt; is the conventional axis: symbols, calls, and imports parsed&lt;br&gt;
with tree-sitter across whatever languages the repo mixes. This is&lt;br&gt;
deliberately the shallow version of static analysis. Dedicated tools do&lt;br&gt;
deep type resolution far better than I ever will, and competing with them&lt;br&gt;
head-on would be a losing use of my time. Syntactic edges are good enough&lt;br&gt;
for navigation, and the edge table records where each edge came from, so a&lt;br&gt;
deeper per-language analyzer can add resolved edges later without a redesign.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Temporal&lt;/strong&gt; comes from git: churn, ownership, and co-change - which files&lt;br&gt;
historically change in the same commits. Parsed from plain &lt;code&gt;git log&lt;/code&gt;&lt;br&gt;
output; no library dependency, and the repo's whole history is sitting&lt;br&gt;
there with nothing better to do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Behavioral&lt;/strong&gt; is the differentiator. My memory system already records&lt;br&gt;
every tool call an agent makes - every read, edit, and write, tagged with&lt;br&gt;
project and session. Rolled up, that log yields a co-touch matrix: which&lt;br&gt;
files did agents actually work on &lt;em&gt;together, in real sessions&lt;/em&gt;. Editing&lt;br&gt;
weighs more than reading in the rollup, because changing two files together&lt;br&gt;
is stronger evidence of coupling than looking at them.&lt;/p&gt;

&lt;p&gt;The distinction between the axes matters in practice. The call graph tells&lt;br&gt;
you A depends on B - true, and sometimes useless, because half of a&lt;br&gt;
codebase depends on B. The behavioral signal tells you that in the last&lt;br&gt;
thirty sessions, every time A changed, B and C changed too. Those are A's&lt;br&gt;
&lt;em&gt;de facto&lt;/em&gt; neighbors: the set you should have open when you touch it. Static&lt;br&gt;
tools cannot compute this because they never see the work. Git co-change&lt;br&gt;
approximates it but only at commit granularity, after the fact, and only&lt;br&gt;
for changes - a session that read four files to safely edit a fifth leaves&lt;br&gt;
no trace in git at all. The agent's own footsteps are the only place this&lt;br&gt;
signal exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cheap at session start, deep on demand
&lt;/h2&gt;

&lt;p&gt;The map is consumed two ways, and the split is deliberate.&lt;/p&gt;

&lt;p&gt;The cheap path is a small rendered digest injected into every session's&lt;br&gt;
context at start: the subsystems, a few key files each, one line of purpose,&lt;br&gt;
ordered by community detection over the fused graph. On my main project&lt;br&gt;
right now that is 913 files folded into 33 communities. One refinement&lt;br&gt;
earns its keep daily: the digest's sections are &lt;em&gt;reordered by relevance to&lt;br&gt;
the branch's current task&lt;/em&gt;, which the session-start machinery already knows&lt;br&gt;
from the branch's&lt;br&gt;
&lt;a href="https://olund.dev/writing/docs-agents-can-trust/" rel="noopener noreferrer"&gt;current-task pointer&lt;/a&gt;. A session opening a&lt;br&gt;
branch about upload quality-control sees the QC subsystem first, not an&lt;br&gt;
alphabetical list.&lt;/p&gt;

&lt;p&gt;The deep path is a set of query tools the agent calls mid-session:&lt;br&gt;
neighborhood (structural plus behavioral neighbors of a file), hotspots&lt;br&gt;
(churn-weighted risk), and &lt;em&gt;why&lt;/em&gt; - which is my favorite, because it joins&lt;br&gt;
the map to the&lt;br&gt;
&lt;a href="https://olund.dev/writing/docs-agents-can-trust/" rel="noopener noreferrer"&gt;documentation standard&lt;/a&gt;. Ask why a&lt;br&gt;
file exists and the answer includes the architecture decision records that&lt;br&gt;
cite it, alongside its callers. The decision trail and the call graph hang&lt;br&gt;
off the same index, so "what is this" and "why is it like this" are one&lt;br&gt;
query apart.&lt;/p&gt;

&lt;p&gt;The two paths exist because they have different economics. The lay-of-the-&lt;br&gt;
land question benefits from being &lt;em&gt;ambient&lt;/em&gt; - answered before the agent&lt;br&gt;
knows to ask, at zero query cost. Graph traversals cannot be pre-rendered&lt;br&gt;
into prose without exploding; they stay behind tools. Pure-filesystem and&lt;br&gt;
pure-query designs both lose to the split.&lt;/p&gt;

&lt;h2&gt;
  
  
  Staying fresh without a daemon
&lt;/h2&gt;

&lt;p&gt;A navigation layer that drifts from the code is worse than none - an agent&lt;br&gt;
routed by a stale call graph mis-navigates &lt;em&gt;confidently&lt;/em&gt;. The freshness&lt;br&gt;
model is a pattern I now reuse everywhere: &lt;strong&gt;lifecycle events catch the&lt;br&gt;
common case; a scheduled sweep guarantees convergence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When an agent edits a file, a post-edit hook re-parses just that file and&lt;br&gt;
updates its rows - sub-second with tree-sitter, and it keeps the index&lt;br&gt;
honest about the thing most likely to be queried next: the code the agent&lt;br&gt;
just changed. But hooks are best-effort by nature. They miss hand edits&lt;br&gt;
made outside a session, deletions, rebases. So a periodic full reindex&lt;br&gt;
sweeps up whatever the hooks missed. Neither mechanism alone is sound: the&lt;br&gt;
hook without the sweep accumulates drift forever; the sweep without the&lt;br&gt;
hook means the map is wrong precisely about the current session's work.&lt;br&gt;
Together they give eventual consistency with a fast path where it matters.&lt;/p&gt;

&lt;p&gt;The digest itself is recomputed on the slow cadence, not per-edit - which&lt;br&gt;
subsystems exist and what matters most is stable week to week, and the one&lt;br&gt;
expensive step (an LLM naming the communities) is not worth paying per&lt;br&gt;
keystroke.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I deliberately did not build
&lt;/h2&gt;

&lt;p&gt;Three rejections shaped the design more than the features did:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No language servers, no deep type inference.&lt;/strong&gt; Spinning up N language
servers across every watched project is operationally heavy, and it
competes with mature tools on their home turf. The behavioral signal is
the moat; syntactic structure is scaffolding for it. The v2 path (per-
language deepening adapters) is recorded, provenance-tagged, and unbuilt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No global cross-project graph.&lt;/strong&gt; Each project's map is self-contained
and lives with the project's own data. A cross-project query ("which
repos use this pattern") has no consumer yet, and speculative generality
would tax every tool signature. When a real consumer shows up, a thin
aggregator can be added over the per-project stores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No separate behavior log.&lt;/strong&gt; The co-touch matrix is a rollup over the
event log the memory system already keeps. A dedicated tracking store
would duplicate data, lose the joins with session decisions, and double
the surface I have to keep redaction-clean.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What transfers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your agent's work history is a first-class code signal.&lt;/strong&gt; If you run
agents against a repo and log their tool calls, you are sitting on
coupling data no static analyzer can derive. The rollup is a SQL view,
not a research project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split ambient from on-demand.&lt;/strong&gt; A small always-injected digest for
orientation, tools for depth. Neither substitutes for the other, and the
digest is only trustworthy if something recomputes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Order context by the current task.&lt;/strong&gt; The same digest reads twice as
well when the relevant subsystem comes first. You usually already know
the task; use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness = fast best-effort path + guaranteed slow sweep.&lt;/strong&gt; Any
derived index maintained only by event hooks is quietly wrong; any
maintained only by batch is wrong about right now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route around mature tools, not through them.&lt;/strong&gt; Tree-sitter-shallow
plus a unique signal beats competing with twenty years of type-inference
engineering on their terms.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>rust</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Documentation my agents can trust (and why it made me faster too)</title>
      <dc:creator>Andre</dc:creator>
      <pubDate>Wed, 29 Jul 2026 08:21:12 +0000</pubDate>
      <link>https://dev.to/olund/documentation-my-agents-can-trust-and-why-it-made-me-faster-too-554f</link>
      <guid>https://dev.to/olund/documentation-my-agents-can-trust-and-why-it-made-me-faster-too-554f</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://olund.dev/writing/docs-agents-can-trust/" rel="noopener noreferrer"&gt;olund.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two earlier posts -&lt;br&gt;
&lt;a href="https://olund.dev/writing/measuring-agent-memory/" rel="noopener noreferrer"&gt;a retrieval bug with twenty measured queries&lt;/a&gt;&lt;br&gt;
and &lt;a href="https://olund.dev/writing/parallel-agent-sessions/" rel="noopener noreferrer"&gt;a presence layer with its design alternatives&lt;/a&gt; -&lt;br&gt;
describe work finished weeks earlier. I wrote each in an afternoon, and I reconstructed nothing. Every&lt;br&gt;
number, every rejected option, every reason was already written down, in a&lt;br&gt;
predictable place, in a form I could trust had not drifted since.&lt;/p&gt;

&lt;p&gt;That is not diligence. Left to my own habits I document like everyone else:&lt;br&gt;
enthusiastically at the start, then never again. The reason the records exist&lt;br&gt;
is that my projects follow a documentation standard designed for a reader&lt;br&gt;
with less context than any human: a coding agent landing cold. It turns out&lt;br&gt;
that optimizing docs for that reader makes them dramatically better for the&lt;br&gt;
human too - future-me is also a reader landing cold, just with more&lt;br&gt;
overconfidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode: nobody can tell what is true
&lt;/h2&gt;

&lt;p&gt;Before standardizing, I surveyed my own repos. The evidence was embarrassing&lt;br&gt;
in a specific, countable way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Architecture decision records that only ever grew. One ADR had accumulated
nine dated amendment sections and roughly 390 lines. Another was over 500.
Reading one meant reading a journal and mentally replaying which parts
still applied.&lt;/li&gt;
&lt;li&gt;A monolithic build-plan file, plus a 960-line "shipped" ledger, with no
in-progress state and two sources of truth for "done" that disagreed.&lt;/li&gt;
&lt;li&gt;A "current task" note that was stale more often than not, because nothing
ever forced it to be true.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a human this is friction. For an agent it is poison: an agent reads the&lt;br&gt;
stale note and acts on it. The core question a repo has to answer - what is&lt;br&gt;
decided, what is to-do, what is done, what is in-flight - had no reliable&lt;br&gt;
answer anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decisions are immutable; change means supersede
&lt;/h2&gt;

&lt;p&gt;The cornerstone rule: &lt;strong&gt;an accepted decision record is never edited again.&lt;/strong&gt;&lt;br&gt;
One decision per ADR, short. If the decision changes, you write a &lt;em&gt;new&lt;/em&gt; ADR&lt;br&gt;
that supersedes the old one, and the old one's status flips to&lt;br&gt;
"superseded by NNNN" - a one-line metadata change, with the body untouched.&lt;/p&gt;

&lt;p&gt;Before adopting it I ran a research pass across the published field, with&lt;br&gt;
each claim adversarially verified, and this was the single strongest&lt;br&gt;
convergence: four independent primary sources (AWS's prescriptive guidance,&lt;br&gt;
adr-tools, log4brains, MADR) all name immutability-plus-supersession as the&lt;br&gt;
cure for exactly the append-only journal sprawl my repos exhibited.&lt;/p&gt;

&lt;p&gt;The deeper reason it works is trust, and trust is an agent requirement&lt;br&gt;
before it is a style preference. An agent quoting a mutable document has to&lt;br&gt;
wonder whether the text changed since the decision was made. An immutable&lt;br&gt;
record with an explicit supersession chain cannot lie about its history: if&lt;br&gt;
its status says accepted, the body means today what it meant the day it was&lt;br&gt;
accepted. That property is what let me lift design rationale into a blog&lt;br&gt;
post weeks later without re-verifying any of it against the code.&lt;/p&gt;

&lt;p&gt;One companion rule keeps the immutability honest: &lt;strong&gt;as-built reality does&lt;br&gt;
not live on the ADR.&lt;/strong&gt; The ADR records the decision and carries one pointer&lt;br&gt;
line to the plan that implemented it. How the implementation actually went -&lt;br&gt;
the surprises, the deviations - lives in the plan. Without this split, the&lt;br&gt;
pressure to "just add a note" reintroduces the journal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Plans: stable task ids and a decision log
&lt;/h2&gt;

&lt;p&gt;Work items live as plan files in an &lt;code&gt;active/&lt;/code&gt; directory, moving to&lt;br&gt;
&lt;code&gt;archive/&lt;/code&gt; when done. The skeleton is boring on purpose: intent, approach,&lt;br&gt;
a task checklist, and a decision log. Two rules carry the weight:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Task ids are stable forever.&lt;/strong&gt; Tasks are &lt;code&gt;T1&lt;/code&gt;, &lt;code&gt;T2&lt;/code&gt;, ... assigned at&lt;br&gt;
creation and never renumbered. A split or reordered task gets a fresh id; an&lt;br&gt;
existing id never silently changes meaning. This sounds pedantic until you&lt;br&gt;
have commits, session notes, and a current-task pointer all referencing&lt;br&gt;
"T4" - renumbering would quietly repoint every one of them at different&lt;br&gt;
work. Stable ids are what let a task be checked off with a one-line&lt;br&gt;
"done, as built" note and trusted years later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Small decisions go in the plan's decision log, not new ADRs.&lt;/strong&gt; The&lt;br&gt;
threshold: an ADR is for a decision that outlives one work item. Everything&lt;br&gt;
local - "we fold the panel by section, not per-fact, because..." - lands as&lt;br&gt;
a dated entry in the plan that needed it. This keeps ADRs rare and heavy&lt;br&gt;
while still capturing the reasoning that debugging (or blogging) later&lt;br&gt;
depends on. My measurement-traps post was essentially written from these&lt;br&gt;
entries; the traps were recorded as decision-log entries the day they bit.&lt;/p&gt;

&lt;p&gt;There is also a gate for honesty about ignorance: an unresolved question in&lt;br&gt;
a plan is written as an explicit &lt;code&gt;[NEEDS CLARIFICATION: ...]&lt;/code&gt; marker, and no&lt;br&gt;
task it gates may start while one remains. An agent that hits the marker&lt;br&gt;
asks instead of guessing. The alternative - the agent inventing an answer&lt;br&gt;
with full confidence - is how plausible-but-wrong work happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  One authoritative home per question
&lt;/h2&gt;

&lt;p&gt;The piece that killed the most staleness is a small table: every question a&lt;br&gt;
reader might ask has exactly one place whose answer counts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Authoritative home&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Why is it built this way?&lt;/td&gt;
&lt;td&gt;ADRs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What does it actually do?&lt;/td&gt;
&lt;td&gt;The code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What is to-do / done?&lt;/td&gt;
&lt;td&gt;The plan's task checklist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who is working on what right now?&lt;/td&gt;
&lt;td&gt;The ephemeral presence layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What is this branch about?&lt;/td&gt;
&lt;td&gt;A one-line pointer file into the active plan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two entries deserve comment. First, &lt;strong&gt;the code stays authoritative for&lt;br&gt;
*what&lt;/strong&gt;*. The research pass surfaced the fashionable inverse - make the spec&lt;br&gt;
canonical and subordinate the code to it - and it failed adversarial&lt;br&gt;
verification outright. Docs own intent and rationale; behavior is what ships.&lt;/p&gt;

&lt;p&gt;Second, &lt;strong&gt;in-flight status is deliberately ephemeral&lt;/strong&gt;. "Session A is&lt;br&gt;
working on T3" lives in the&lt;br&gt;
&lt;a href="https://olund.dev/writing/parallel-agent-sessions/" rel="noopener noreferrer"&gt;live presence registry&lt;/a&gt;,&lt;br&gt;
never as a durable marker in the plan. A durable in-progress flag is&lt;br&gt;
precisely the thing that rots when a session dies mid-task - which is how my&lt;br&gt;
old current-task note became a professional liar. Durable files record what&lt;br&gt;
is true durably; live state lives somewhere that dies with the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Discipline that is not enforced is decoration
&lt;/h2&gt;

&lt;p&gt;Everything above would decay in a month if it relied on my consistency, so&lt;br&gt;
it does not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New repos are scaffolded with the structure, templates, and glossary in
their first commit, so the standard is the path of least resistance.&lt;/li&gt;
&lt;li&gt;A lint runs over the machine-readable invariants: frontmatter status
values, task-id uniqueness, archive moves, pointer files that reference
plans that exist, an ADR's implemented-by path that still resolves.&lt;/li&gt;
&lt;li&gt;The migration rule for old repos is forward-only: nothing is retroactively
rewritten, but touching an old open item means first lifting it into a
real plan. Old mess is contained, not laundered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lint matters more than it looks. It converts "we agreed to do this" into&lt;br&gt;
"the build tells you when you did not," which is the only form of agreement&lt;br&gt;
that survives contact with a busy week.&lt;/p&gt;

&lt;h2&gt;
  
  
  What transfers
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write for the reader with the least context.&lt;/strong&gt; An agent landing cold is
the honest proxy for future-you. If an agent can reconstruct what is
decided, done, and open from your repo, so can any human.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make records immutable and change explicit.&lt;/strong&gt; A document that cannot
have been quietly edited is the only kind either of you can quote without
re-verifying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give every question exactly one authoritative home&lt;/strong&gt;, and be honest
about which axis is durable and which is ephemeral. Most staleness is a
durable file claiming to know live state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stable ids beat tidy renumbering.&lt;/strong&gt; Anything referenced from outside a
document must never change meaning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce mechanically or watch it drift.&lt;/strong&gt; Templates make compliance
cheap; a lint makes drift loud.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The compounding effect surprised me. Each layer - the memory system, the&lt;br&gt;
presence layer, this standard - was built to solve its own local problem,&lt;br&gt;
and the posts about them exist because the layers also happen to document&lt;br&gt;
each other. The infrastructure that keeps my agents honest turned out to be&lt;br&gt;
the same infrastructure that lets me tell you about it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>documentation</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Running parallel agent sessions without them stepping on each other</title>
      <dc:creator>Andre</dc:creator>
      <pubDate>Wed, 29 Jul 2026 07:50:23 +0000</pubDate>
      <link>https://dev.to/olund/running-parallel-agent-sessions-without-them-stepping-on-each-other-441o</link>
      <guid>https://dev.to/olund/running-parallel-agent-sessions-without-them-stepping-on-each-other-441o</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://olund.dev/writing/parallel-agent-sessions/" rel="noopener noreferrer"&gt;olund.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I usually have several coding-agent sessions running at once. Sometimes on&lt;br&gt;
different projects, often on the same repo: one session deep in a refactor,&lt;br&gt;
one writing docs, one investigating a bug. Nothing about an agent harness&lt;br&gt;
makes this safe by default. The two failure modes show up fast:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Corruption&lt;/strong&gt;: two sessions write the same state file and one clobbers
the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collision&lt;/strong&gt;: two sessions, blind to each other, pick up the same work
or edit the same files, and you discover it at diff time.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These feel like one problem ("agents conflict") but they are structurally&lt;br&gt;
different, and in my memory system they got structurally different fixes.&lt;br&gt;
The corruption fix has no runtime component at all. The collision fix has no&lt;br&gt;
locks. This post is about both, and about the things I deliberately did not&lt;br&gt;
build.&lt;/p&gt;
&lt;h2&gt;
  
  
  Corruption: solved by construction, not coordination
&lt;/h2&gt;

&lt;p&gt;Each of my agent sessions keeps working memory on disk: a scratchpad,&lt;br&gt;
a decision log, open questions. The naive layout - one set of files per&lt;br&gt;
project branch - dies immediately with two live sessions, and the classic&lt;br&gt;
answer is locking or serialized writes through a daemon.&lt;/p&gt;

&lt;p&gt;The layout that needs neither: &lt;strong&gt;every session writes only to its own&lt;br&gt;
directory, keyed by its session id&lt;/strong&gt;. Subagents inherit the id with a suffix,&lt;br&gt;
so even a session's own workers cannot collide with their parent. No two&lt;br&gt;
writers ever touch the same file, not because a lock stops them, but because&lt;br&gt;
no shared file exists. At session start, a materialize step folds all&lt;br&gt;
sessions' entries into one read-only merged view, so a new session still&lt;br&gt;
reads everything that happened on the branch - stamped as historical&lt;br&gt;
reference, because a snapshot is stale by definition.&lt;/p&gt;

&lt;p&gt;There is exactly one exception, and it is load-bearing: a single&lt;br&gt;
&lt;code&gt;current-task.md&lt;/code&gt; per branch, shared by every session, last writer wins.&lt;br&gt;
That file answers "what is this branch about right now", which is only&lt;br&gt;
useful &lt;em&gt;because&lt;/em&gt; it is shared. The design rule that fell out: share nothing&lt;br&gt;
by default, and when you do share, share one small file whose whole point is&lt;br&gt;
being the single meeting place.&lt;/p&gt;

&lt;p&gt;This solved corruption completely. It did nothing for collision - a session&lt;br&gt;
writing safely in its own directory can still cheerfully redo work another&lt;br&gt;
session finished an hour ago.&lt;/p&gt;
&lt;h2&gt;
  
  
  Collision: sessions need to see each other
&lt;/h2&gt;

&lt;p&gt;The actual pain, once corruption was gone, was launching a second session&lt;br&gt;
into a repo blind. It did not know a first session existed, let alone what&lt;br&gt;
files it was touching. My fix is a &lt;strong&gt;presence layer&lt;/strong&gt;: a machine-local&lt;br&gt;
registry of live sessions, maintained entirely by lifecycle hooks that were&lt;br&gt;
already firing on session start, tool use, and session end.&lt;/p&gt;

&lt;p&gt;Each record carries the branch, the files the session has touched, an&lt;br&gt;
optional one-line intent, and a freshness timestamp. When a new session&lt;br&gt;
starts, it gets a block injected into its context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 other session(s) live on this branch right now - avoid editing the same files:
- 6679ade6 - touching channel.md, production-readiness.md (active 5m ago)
- 30b9890c - touching audit.rs, audit.test.ts (active 52m ago)
- c5e09075 - touching review-queue.md, CONTEXT.md (active 1h ago)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is most of the feature. The dominant collision case was never two&lt;br&gt;
sessions racing for a file at the same millisecond; it was me starting&lt;br&gt;
session B without remembering what session A was doing. Awareness at launch&lt;br&gt;
time, plus an on-demand "who else is here" query the agent can run&lt;br&gt;
mid-session, covers almost all of it.&lt;/p&gt;

&lt;p&gt;The part that took actual design care is trust. A presence registry is only&lt;br&gt;
useful if the agent can believe it: phantom peers - records of sessions that&lt;br&gt;
died without cleaning up - turn the block into noise the agent learns to&lt;br&gt;
ignore, and then the feature is worse than nothing. Session-end hooks are&lt;br&gt;
unreliable by nature (crashes, hard kills, dead batteries), so liveness is&lt;br&gt;
checked at &lt;em&gt;read&lt;/em&gt; time against the OS: does the recorded pid exist, and does&lt;br&gt;
its process start-time match what the record captured? The second check&lt;br&gt;
matters more than it looks. Pids get reused; without the start-time match, a&lt;br&gt;
recycled pid makes a dead session read as alive forever, which is precisely&lt;br&gt;
the failure the layer exists to prevent.&lt;/p&gt;

&lt;h2&gt;
  
  
  No locks, on purpose
&lt;/h2&gt;

&lt;p&gt;The instinctive design here is mutual exclusion: lock files a session is&lt;br&gt;
editing, deny the other session access. I rejected it, and the reasoning&lt;br&gt;
generalizes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The problem was awareness, not exclusion.&lt;/strong&gt; Sessions were not fighting
over files; they were ignorant of each other. Informing them fixes the
actual failure. Blocking them fixes a different, mostly hypothetical one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A lock is hostile to the human in the loop.&lt;/strong&gt; These are my own parallel
sessions. A hard lock means my repo tells &lt;em&gt;me&lt;/em&gt; no when I ask a second
session to touch a file the first one grazed an hour ago.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Advisory degrades gracefully; locks degrade catastrophically.&lt;/strong&gt; A stale
presence record wastes a warning line. A stale lock blocks work until
someone hunts it down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the whole layer is advisory. It informs, it never blocks. Weeks in, I&lt;br&gt;
have not once wished for the lock.&lt;/p&gt;

&lt;p&gt;I also rejected the heavier alternative: a durable task queue where every&lt;br&gt;
session formally claims a task before touching anything. Queues answer&lt;br&gt;
"what is the status of this work" - a real question, but a different one.&lt;br&gt;
For live collision avoidance a claimed task is simultaneously too coarse&lt;br&gt;
("someone is on the refactor" does not tell you which file they are editing&lt;br&gt;
right now) and too much ceremony (every session pays a claim step to defend&lt;br&gt;
against a rare event). Presence is finer and free: the hooks fire anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The human side: which session needs me
&lt;/h2&gt;

&lt;p&gt;Coordination between sessions is half the story. The other half is&lt;br&gt;
coordinating &lt;em&gt;me&lt;/em&gt;. With four sessions across three projects, the expensive&lt;br&gt;
question stops being "are the agents colliding" and becomes "which one is&lt;br&gt;
waiting on my input while I stare at a different terminal".&lt;/p&gt;

&lt;p&gt;Presence records carry a state field for this: &lt;code&gt;working&lt;/code&gt; or &lt;code&gt;waiting&lt;/code&gt;. Turn&lt;br&gt;
boundaries drive it - submitting a prompt marks the session working,&lt;br&gt;
the agent finishing its turn marks it waiting. The subtlety is that tool&lt;br&gt;
activity alone cannot tell you this: a session that is thinking, or reading&lt;br&gt;
files, looks idle by every activity metric while genuinely busy. Only the&lt;br&gt;
turn boundary is truthful. A workbench view renders the sessions per&lt;br&gt;
project with a marker on whoever is waiting, and a backgrounded session&lt;br&gt;
going idle fires a notification.&lt;/p&gt;

&lt;p&gt;The effect on throughput is larger than the collision fix. Parallel&lt;br&gt;
sessions were already &lt;em&gt;safe&lt;/em&gt;; this made them &lt;em&gt;worth it&lt;/em&gt;, because agent idle&lt;br&gt;
time - finished, waiting, unnoticed - was where the parallelism actually&lt;br&gt;
leaked.&lt;/p&gt;

&lt;h2&gt;
  
  
  What transfers
&lt;/h2&gt;

&lt;p&gt;The specifics are mine, but the shapes are portable to any multi-session&lt;br&gt;
agent setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Partition state by writer; make shared files a deliberate exception.&lt;/strong&gt;
Corruption problems you solve by construction do not come back. Locks you
add have to be right forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inject awareness at start time.&lt;/strong&gt; The dominant collision is launching
blind, not racing. A cheap "who is here, touching what" block at session
start beats sophisticated conflict detection you build later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A registry nobody trusts is worse than none.&lt;/strong&gt; Whatever your liveness
story is, phantom entries are the death of an advisory system - the reader
learns to ignore it. Verify liveness at read time, against a source that
survives crashes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer advisory over blocking until proven otherwise.&lt;/strong&gt; Especially when
every session ultimately answers to one human.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track waiting, not just working.&lt;/strong&gt; If you run parallel sessions, the
bottleneck quietly becomes your own attention. Surface it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this needed a coordinator process, a message bus, or a database.&lt;br&gt;
It is a directory convention, a handful of JSON files in a cache directory,&lt;br&gt;
and hooks that were already firing. The boring infrastructure was the&lt;br&gt;
feature.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>productivity</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How I measure whether my agent's memory works</title>
      <dc:creator>Andre</dc:creator>
      <pubDate>Wed, 29 Jul 2026 07:50:20 +0000</pubDate>
      <link>https://dev.to/olund/how-i-measure-whether-my-agents-memory-works-3imp</link>
      <guid>https://dev.to/olund/how-i-measure-whether-my-agents-memory-works-3imp</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://olund.dev/writing/measuring-agent-memory/" rel="noopener noreferrer"&gt;olund.dev&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I run my coding agents on top of a personal memory system I built: durable&lt;br&gt;
facts live in a markdown file, an indexer embeds them with a local model&lt;br&gt;
(bge-m3), and a &lt;code&gt;recall&lt;/code&gt; command does semantic search over the result. Sessions&lt;br&gt;
query it constantly. It looked healthy for months.&lt;/p&gt;

&lt;p&gt;Then I asked it something I knew was in there: whether my system supports&lt;br&gt;
Windows. The fact existed, word for word, in the durable file. &lt;code&gt;recall&lt;/code&gt;&lt;br&gt;
returned zero hits from it. Not ranked low - absent.&lt;/p&gt;

&lt;p&gt;This post is the story of finding out why, and more usefully, the methods that&lt;br&gt;
made every step provable instead of plausible. If you are building agent&lt;br&gt;
memory, the specific bug will probably not be yours. The measurement traps&lt;br&gt;
will be.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug: the text was never embedded
&lt;/h2&gt;

&lt;p&gt;My indexer chunked the memory file by markdown section. One section had grown&lt;br&gt;
to 904 tokens. The embedding library I use (fastembed) silently truncates&lt;br&gt;
input at a default of 512 tokens unless you override it, and nothing in my&lt;br&gt;
code did. Nothing upstream bounded chunk size either.&lt;/p&gt;

&lt;p&gt;So the encoder saw the first 1687 characters of a 3090-character section and&lt;br&gt;
threw away the rest - 45.4% of the text, containing three entire facts. No&lt;br&gt;
error, no warning, no log line. The stored vector simply did not contain&lt;br&gt;
their meaning. Retrieval was not failing to find them. There was nothing to&lt;br&gt;
find.&lt;/p&gt;

&lt;p&gt;The nasty property: this bug is monotone with growth. Every new fact appended&lt;br&gt;
to the section pushed more content past the cut. The memory system degraded&lt;br&gt;
&lt;em&gt;because&lt;/em&gt; it was being used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving it causally, not correlationally
&lt;/h2&gt;

&lt;p&gt;"The section is long and recall is bad" is a correlation. Before touching any&lt;br&gt;
code I wanted the mechanism pinned, because my first diagnosis was wrong (more&lt;br&gt;
on that below). The probe that settled it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;query text&lt;/th&gt;
&lt;th&gt;distance to the stored chunk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;the full 3090-char section&lt;/td&gt;
&lt;td&gt;0.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;only the first 1687 chars&lt;/td&gt;
&lt;td&gt;0.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;only the discarded 1403-char tail&lt;/td&gt;
&lt;td&gt;0.4247&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full section and its truncated prefix embed to the &lt;em&gt;same vector&lt;/em&gt; - exact&lt;br&gt;
zero distance. The tail contributes nothing at all. That is not what dilution&lt;br&gt;
looks like; that is a step function. Correlating each fact's character offset&lt;br&gt;
with its self-retrieval distance gave a Spearman rho of +0.952: a cliff at the&lt;br&gt;
cut position, not a gradient.&lt;/p&gt;

&lt;p&gt;This mattered because my original hypothesis was centroid dilution - one&lt;br&gt;
embedding averaging over many facts, dominated by the numerous ones. Dilution&lt;br&gt;
suggests fixes like re-ranking or splitting long sections more finely.&lt;br&gt;
Truncation means data is missing from the index entirely, and no amount of&lt;br&gt;
ranking cleverness can retrieve a vector that was never computed. Wrong&lt;br&gt;
mechanism, wrong fix. The ten-minute probe was cheaper than shipping the&lt;br&gt;
wrong repair.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that looks right and the one that is
&lt;/h2&gt;

&lt;p&gt;The one-line fix exists: the model genuinely supports an 8192-token window,&lt;br&gt;
and a single &lt;code&gt;.with_max_length(8192)&lt;/code&gt; restores it. I rejected it for three&lt;br&gt;
reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It trades a proven defect for an unproven one - the multi-fact centroid
I could now actually measure. Even on a five-fact section that fits the
window completely, querying any single fact's verbatim text returned its
section at distance 0.21-0.34, against a 0.0000 floor for whole-chunk
self-queries. Bundling costs retrieval sharpness even without truncation.&lt;/li&gt;
&lt;li&gt;Quadratic attention on a 16x sequence, paid on every indexed chunk, for
the benefit of exactly one oversized section.&lt;/li&gt;
&lt;li&gt;It leaves the granularity wrong. Citations, quality reports, and injection
payloads all wanted to address individual facts and could not.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the fix was to make the &lt;em&gt;fact&lt;/em&gt; the unit of embedding: the indexer now&lt;br&gt;
parses the memory file's per-fact structure and emits one chunk per fact.&lt;br&gt;
Every fact is 58-335 tokens, comfortably inside any window.&lt;/p&gt;

&lt;p&gt;And separately - this is the part that prevents recurrence - the indexer now&lt;br&gt;
counts every chunk's tokens against the encoder's actual window and warns on&lt;br&gt;
offenders, and my health command reports the count. The window &lt;em&gt;guard&lt;/em&gt;, not&lt;br&gt;
the window &lt;em&gt;raise&lt;/em&gt;, is what stops this bug class from coming back. Four wiki&lt;br&gt;
documents are over the limit today; the detector names them, and that debt is&lt;br&gt;
visible instead of silent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measuring the fix instead of arguing about it
&lt;/h2&gt;

&lt;p&gt;The design had one open risk I could not reason away: per-fact chunks might&lt;br&gt;
lose on &lt;em&gt;broad&lt;/em&gt; queries ("what side projects am I working on"), where a big&lt;br&gt;
section chunk plausibly wins by covering more topics. I had accepted this as&lt;br&gt;
a trade-off in the design review.&lt;/p&gt;

&lt;p&gt;Instead of accepting it, I measured it. Two indexes over the identical&lt;br&gt;
corpus: arm A chunked by section (exactly reproducing the live store), arm B&lt;br&gt;
chunked by fact. Twenty queries against both - memory-directed ones, broad&lt;br&gt;
ones, and eight wiki-directed queries as a counter-direction check, because&lt;br&gt;
an improvement that comes from crowding out the rest of the corpus is not an&lt;br&gt;
improvement.&lt;/p&gt;

&lt;p&gt;The accepted trade-off inverted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rank-1 distance was never worse: 12 of 12 memory-directed queries.&lt;/li&gt;
&lt;li&gt;Memory facts present in a default top-5 rose from 9 to 27 across queries.&lt;/li&gt;
&lt;li&gt;All six broad queries improved or tied. The flagship broad query went from
one memory hit at d=0.4763 to three hits led by d=0.3537.&lt;/li&gt;
&lt;li&gt;Four narrow probes went from zero memory chunks in the top 5 to rank 1.&lt;/li&gt;
&lt;li&gt;The eight wiki queries kept rank 1 unchanged, all eight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The section chunk's assumed advantage on broad queries never existed. It was&lt;br&gt;
an artifact of there being only two memory chunks in the index at all - of&lt;br&gt;
course one of them "won broad queries"; there was nothing else to return.&lt;br&gt;
This is my strongest argument for measuring over arguing: the risk I had&lt;br&gt;
formally accepted in a design document was not real.&lt;/p&gt;

&lt;p&gt;End to end, the six-probe acceptance set went from 0/6 correct to 6/6, and&lt;br&gt;
every one of the twelve facts now self-retrieves at exact d=0.0000 - which&lt;br&gt;
doubles as a standing proof that no fact is truncated anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two ways my "before" measurement almost lied
&lt;/h2&gt;

&lt;p&gt;An honest before/after table requires the "before" arm to actually measure&lt;br&gt;
the old behavior. Mine silently stopped doing that twice, and both failures&lt;br&gt;
produced the same poisonous output: a table showing no change at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trap one: the old store was auto-upgraded by the new binary.&lt;/strong&gt; My index&lt;br&gt;
store wipes and rebuilds itself when its schema version is outdated. The fix&lt;br&gt;
bumped the schema. So when I pointed the &lt;em&gt;new&lt;/em&gt; binary at the preserved&lt;br&gt;
&lt;em&gt;old&lt;/em&gt; store to get "before" numbers, the first query quietly rebuilt the&lt;br&gt;
entire store with per-fact chunking - converting my before-arm into a second&lt;br&gt;
after-arm. The before-arm has to be driven by the old &lt;em&gt;binary&lt;/em&gt;, not just the&lt;br&gt;
old data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trap two: the old binary overwrote the new one.&lt;/strong&gt; I built that old binary&lt;br&gt;
from a git worktree, but with the build cache directory shared with the main&lt;br&gt;
checkout - which overwrote the release binary I was using for the "after"&lt;br&gt;
column. Both columns were now measuring pre-fix code.&lt;/p&gt;

&lt;p&gt;I caught it because of two smells: the after column exactly equaled the&lt;br&gt;
before column, and a known-good result I had measured minutes earlier had&lt;br&gt;
silently regressed. The rule I keep now: pin each arm to its own binary path,&lt;br&gt;
and assert the two binaries differ before trusting any comparison between&lt;br&gt;
them. "Before equals after, everywhere, exactly" is not a null result. It is&lt;br&gt;
an instrument failure.&lt;/p&gt;

&lt;p&gt;A third, related trap lives in the test suite. My mock embedder hashes the&lt;br&gt;
full input text with no truncation, so every truncation test passes against&lt;br&gt;
it - including on the broken code. The causal tests require the real encoder&lt;br&gt;
(a 558MB model download I keep out of CI), so CI instead pins the structural&lt;br&gt;
properties the fix guarantees: one chunk per fact, identity fields populated.&lt;br&gt;
The proof runs by hand at the gate; the regression guard runs on every&lt;br&gt;
commit; and I wrote down which is which, so a green CI run cannot be mistaken&lt;br&gt;
for the causal proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  What transfers
&lt;/h2&gt;

&lt;p&gt;None of this needed special tooling - a CLI, a scratch directory for each&lt;br&gt;
index, and discipline about what counts as evidence. The parts I would carry&lt;br&gt;
to any agent-memory system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prove mechanism before choosing a fix.&lt;/strong&gt; The cheap causal probe
(full vs. prefix vs. tail) killed a wrong diagnosis that would have led to
a plausible, useless repair.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make missing data observable.&lt;/strong&gt; The worst property of my bug was
silence. The token-count guard turns the next occurrence into a warning
with a filename in it. And "not measured" must render as &lt;em&gt;not measured&lt;/em&gt; -
my health check deliberately refuses to show a green tick when the data to
judge is absent, because a green that certifies nothing is worse than no
check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure accepted risks; they may not exist.&lt;/strong&gt; The broad-query trade-off
survived a design review as a reasonable-sounding caveat. It did not
survive twenty queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Distrust symmetric results.&lt;/strong&gt; Identical before/after numbers mean your
instrument broke, until proven otherwise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claim what you measured, nothing more.&lt;/strong&gt; Everything above is one corpus
of 129 chunks, one embedding model, twenty queries, on my machine. It
generalizes as a method, not as numbers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole investigation - diagnosis, fix, and both measurement arms - is&lt;br&gt;
recorded in the project's decision records and work plan, which is why I&lt;br&gt;
could write this post weeks later without reconstructing anything. How that&lt;br&gt;
documentation discipline works is&lt;br&gt;
&lt;a href="https://olund.dev/writing/docs-agents-can-trust/" rel="noopener noreferrer"&gt;its own post&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>rust</category>
      <category>debugging</category>
    </item>
  </channel>
</rss>
