<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Andre Lund</title>
    <subtitle>Systems, agents, and the occasional measurement.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://olund.dev/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://olund.dev"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-07-29T00:00:00+00:00</updated>
    <id>https://olund.dev/atom.xml</id>
    <entry xml:lang="en">
        <title>Audition the voice before you commission the work</title>
        <published>2026-07-29T00:00:00+00:00</published>
        <updated>2026-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://olund.dev/writing/audition-before-you-commission/"/>
        <id>https://olund.dev/writing/audition-before-you-commission/</id>
        
        <content type="html" xml:base="https://olund.dev/writing/audition-before-you-commission/">&lt;p&gt;Part of my tooling generates long-form written content in a configured
voice: an editorial identity that describes the audience, the register, the
vocabulary, what the writer sounds like. The identity is prose in a config.
And prose configs for tone have a nasty property: they are write-only. You
describe the voice you want (&quot;unhurried, curious, explains from first
principles, never hypey&quot;), the generator consumes it, and you learn whether
your description &lt;em&gt;worked&lt;&#x2F;em&gt; only after paying for a full generation run and
reading the result. If the register is off, you edit adjectives and pay
again.&lt;&#x2F;p&gt;
&lt;p&gt;The feedback loop is the problem. Tuning a persona through full generations
is like adjusting a recipe by catering a wedding each time. What I wanted
was a taste: given this identity, say two lines in this voice, right now,
for approximately nothing.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-smallest-possible-agent&quot;&gt;The smallest possible agent&lt;&#x2F;h2&gt;
&lt;p&gt;The fix is a preview agent, and the design goal was to make it as close to
free as an LLM call gets - because a preview you hesitate to run is a
preview that does not get run. Everything about it is subtraction:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No tools.&lt;&#x2F;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;&#x2F;em&gt;. Same input, same class of output,
nothing ambient to drift.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A fast local model.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A hard output schema.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Ephemeral by construction.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A timeout and one retry.&lt;&#x2F;strong&gt; Ninety seconds, one attempt to recover,
then fail visibly. A preview that hangs is worse than one that errors.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The prompt side is one rule repeated three ways: match the identity&#x27;s tone,
register, and vocabulary precisely; each line must be a speakable line of prose
on its own; do not perform meta-commentary. Sample lines that &lt;em&gt;describe&lt;&#x2F;em&gt;
the voice instead of &lt;em&gt;being&lt;&#x2F;em&gt; the voice are the failure mode, and the
instructions attack it directly.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-loop-it-creates&quot;&gt;The loop it creates&lt;&#x2F;h2&gt;
&lt;p&gt;In the settings UI, next to the identity editor, there is a sample button.
Type an optional topic, click, and a moment later: a handful of lines in
the configured voice. Edit the identity, sample again. The tuning loop
drops from &quot;generate a full piece, read it, wince&quot; to seconds per
iteration.&lt;&#x2F;p&gt;
&lt;p&gt;Two UI decisions carry more weight than they look like they should:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;The preview invalidates nothing.&lt;&#x2F;strong&gt; It is a plain fire-and-return call
with no cache updates, because it changes no state. Wiring a preview into
the app&#x27;s data layer as if it were a mutation is a category error that
makes every preview cost a refetch.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;The empty state teaches.&lt;&#x2F;strong&gt; With no identity configured, the button does
not disable silently; the endpoint refuses with &quot;identity is unset&quot; and the
UI says so. A preview feature whose precondition is invisible reads as
broken.&lt;&#x2F;p&gt;
&lt;p&gt;There is also a seam decision underneath: the route awaits the worker
synchronously, bounded at two minutes, instead of returning a job id the
client polls. Previews are interactive - the human is sitting there. The
moment a preview needs a progress bar, it has failed at being a preview,
so the API shape encodes the latency budget: if this cannot answer while
the user watches, it should error, not stream status updates.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;why-this-is-a-pattern-not-a-feature&quot;&gt;Why this is a pattern, not a feature&lt;&#x2F;h2&gt;
&lt;p&gt;The general shape: &lt;strong&gt;when a system consumes a human-authored description
and produces something expensive, insert the cheapest possible sampler
between the two.&lt;&#x2F;strong&gt; The description-to-output gap is where confidence
quietly dies - you wrote the config, you &lt;em&gt;think&lt;&#x2F;em&gt; it says what you mean,
and the only verification on offer costs a full run.&lt;&#x2F;p&gt;
&lt;p&gt;Samplers earn their place when they are:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Instant enough to be reflexive.&lt;&#x2F;strong&gt; Seconds, not minutes. The moment
sampling requires deciding whether it is worth it, iteration stops.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Free enough to be guiltless.&lt;&#x2F;strong&gt; A local model or the cheapest API tier.
The task is narrow by design; use the narrowest worker that does it.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Constrained enough to be honest.&lt;&#x2F;strong&gt; Schema-forced output in the target
format. A sampler that returns an essay about what it would do is
theater.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Stateless enough to be ignorable.&lt;&#x2F;strong&gt; No persistence, no history, no
cleanup. Run it forty times; nothing accumulates.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;I now reach for this shape whenever a prose config drives generation:
sample the persona before the piece, the summary style before the batch,
the reviewer&#x27;s severity before the review run. Each sampler is an
afternoon of work, because subtraction is fast to build - the entire agent
definition fits on one screen, and the worker runtime it rides on already
existed.&lt;&#x2F;p&gt;
&lt;p&gt;The quiet lesson underneath is about model routing. The instinct is to
send every task to the strongest model available. But a preview&#x27;s job is
to be &lt;em&gt;representative and immediate&lt;&#x2F;em&gt;, not maximal - and a small local
model with a tight schema and inline context is more representative of
&quot;what will the configured voice sound like&quot; than a frontier model
improvising with more freedom. Match the worker to the narrowness of the
task, and some tasks turn out to be nearly free.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Giving my agent a map of the codebase (drawn partly from its own footsteps)</title>
        <published>2026-07-29T00:00:00+00:00</published>
        <updated>2026-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://olund.dev/writing/code-map/"/>
        <id>https://olund.dev/writing/code-map/</id>
        
        <content type="html" xml:base="https://olund.dev/writing/code-map/">&lt;p&gt;Watch a coding agent land in an unfamiliar repository and you will see it do
what a new hire does on day one: grep for names, list directories, open
files, and slowly assemble a mental model. Unlike the hire, the agent does
this &lt;em&gt;every session&lt;&#x2F;em&gt;. The model it builds is discarded when the context
window closes, and the next session pays for it again - in tokens, in
latency, and in the wrong turns an incomplete picture produces.&lt;&#x2F;p&gt;
&lt;p&gt;My fix is a per-project &lt;strong&gt;code map&lt;&#x2F;strong&gt;: a queryable navigation layer each
repository carries, rebuilt incrementally, that a new session receives a
digest of before it reads a single file. The interesting part is not that it
exists - static code intelligence is a mature field - but which three
signals it fuses, because the third one is something most tooling cannot
see at all.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;three-signals-one-map&quot;&gt;Three signals, one map&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;Structure&lt;&#x2F;strong&gt; is the conventional axis: symbols, calls, and imports parsed
with tree-sitter across whatever languages the repo mixes. This is
deliberately the shallow version of static analysis. Dedicated tools do
deep type resolution far better than I ever will, and competing with them
head-on would be a losing use of my time. Syntactic edges are good enough
for navigation, and the edge table records where each edge came from, so a
deeper per-language analyzer can add resolved edges later without a redesign.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Temporal&lt;&#x2F;strong&gt; comes from git: churn, ownership, and co-change - which files
historically change in the same commits. Parsed from plain &lt;code&gt;git log&lt;&#x2F;code&gt;
output; no library dependency, and the repo&#x27;s whole history is sitting
there with nothing better to do.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Behavioral&lt;&#x2F;strong&gt; is the differentiator. My memory system already records
every tool call an agent makes - every read, edit, and write, tagged with
project and session. Rolled up, that log yields a co-touch matrix: which
files did agents actually work on &lt;em&gt;together, in real sessions&lt;&#x2F;em&gt;. Editing
weighs more than reading in the rollup, because changing two files together
is stronger evidence of coupling than looking at them.&lt;&#x2F;p&gt;
&lt;p&gt;The distinction between the axes matters in practice. The call graph tells
you A depends on B - true, and sometimes useless, because half of a
codebase depends on B. The behavioral signal tells you that in the last
thirty sessions, every time A changed, B and C changed too. Those are A&#x27;s
&lt;em&gt;de facto&lt;&#x2F;em&gt; neighbors: the set you should have open when you touch it. Static
tools cannot compute this because they never see the work. Git co-change
approximates it but only at commit granularity, after the fact, and only
for changes - a session that read four files to safely edit a fifth leaves
no trace in git at all. The agent&#x27;s own footsteps are the only place this
signal exists.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cheap-at-session-start-deep-on-demand&quot;&gt;Cheap at session start, deep on demand&lt;&#x2F;h2&gt;
&lt;p&gt;The map is consumed two ways, and the split is deliberate.&lt;&#x2F;p&gt;
&lt;p&gt;The cheap path is a small rendered digest injected into every session&#x27;s
context at start: the subsystems, a few key files each, one line of purpose,
ordered by community detection over the fused graph. On my main project
right now that is 913 files folded into 33 communities. One refinement
earns its keep daily: the digest&#x27;s sections are &lt;em&gt;reordered by relevance to
the branch&#x27;s current task&lt;&#x2F;em&gt;, which the session-start machinery already knows
from the pointer file described in the previous post. A session opening a
branch about upload quality-control sees the QC subsystem first, not an
alphabetical list.&lt;&#x2F;p&gt;
&lt;p&gt;The deep path is a set of query tools the agent calls mid-session:
neighborhood (structural plus behavioral neighbors of a file), hotspots
(churn-weighted risk), and &lt;em&gt;why&lt;&#x2F;em&gt; - which is my favorite, because it joins
the map to the documentation standard from the previous post. Ask why a
file exists and the answer includes the architecture decision records that
cite it, alongside its callers. The decision trail and the call graph hang
off the same index, so &quot;what is this&quot; and &quot;why is it like this&quot; are one
query apart.&lt;&#x2F;p&gt;
&lt;p&gt;The two paths exist because they have different economics. The lay-of-the-
land question benefits from being &lt;em&gt;ambient&lt;&#x2F;em&gt; - answered before the agent
knows to ask, at zero query cost. Graph traversals cannot be pre-rendered
into prose without exploding; they stay behind tools. Pure-filesystem and
pure-query designs both lose to the split.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;staying-fresh-without-a-daemon&quot;&gt;Staying fresh without a daemon&lt;&#x2F;h2&gt;
&lt;p&gt;A navigation layer that drifts from the code is worse than none - an agent
routed by a stale call graph mis-navigates &lt;em&gt;confidently&lt;&#x2F;em&gt;. The freshness
model is a pattern I now reuse everywhere: &lt;strong&gt;lifecycle events catch the
common case; a scheduled sweep guarantees convergence.&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;When an agent edits a file, a post-edit hook re-parses just that file and
updates its rows - sub-second with tree-sitter, and it keeps the index
honest about the thing most likely to be queried next: the code the agent
just changed. But hooks are best-effort by nature. They miss hand edits
made outside a session, deletions, rebases. So a periodic full reindex
sweeps up whatever the hooks missed. Neither mechanism alone is sound: the
hook without the sweep accumulates drift forever; the sweep without the
hook means the map is wrong precisely about the current session&#x27;s work.
Together they give eventual consistency with a fast path where it matters.&lt;&#x2F;p&gt;
&lt;p&gt;The digest itself is recomputed on the slow cadence, not per-edit - which
subsystems exist and what matters most is stable week to week, and the one
expensive step (an LLM naming the communities) is not worth paying per
keystroke.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-i-deliberately-did-not-build&quot;&gt;What I deliberately did not build&lt;&#x2F;h2&gt;
&lt;p&gt;Three rejections shaped the design more than the features did:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No language servers, no deep type inference.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;No global cross-project graph.&lt;&#x2F;strong&gt; Each project&#x27;s map is self-contained
and lives with the project&#x27;s own data. A cross-project query (&quot;which
repos use this pattern&quot;) 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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;No separate behavior log.&lt;&#x2F;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;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;what-transfers&quot;&gt;What transfers&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Your agent&#x27;s work history is a first-class code signal.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Split ambient from on-demand.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Order context by the current task.&lt;&#x2F;strong&gt; The same digest reads twice as
well when the relevant subsystem comes first. You usually already know
the task; use it.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Freshness = fast best-effort path + guaranteed slow sweep.&lt;&#x2F;strong&gt; Any
derived index maintained only by event hooks is quietly wrong; any
maintained only by batch is wrong about right now.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Route around mature tools, not through them.&lt;&#x2F;strong&gt; Tree-sitter-shallow
plus a unique signal beats competing with twenty years of type-inference
engineering on their terms.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;This is the fourth post about the same small stack, and the pattern holds:
the memory system logs the footsteps, the presence layer keeps parallel
sessions legible, the docs standard preserves the why, and the code map
fuses all three into the thing an agent actually needs on arrival - a
sense of place.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Documentation my agents can trust (and why it made me faster too)</title>
        <published>2026-07-29T00:00:00+00:00</published>
        <updated>2026-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://olund.dev/writing/docs-agents-can-trust/"/>
        <id>https://olund.dev/writing/docs-agents-can-trust/</id>
        
        <content type="html" xml:base="https://olund.dev/writing/docs-agents-can-trust/">&lt;p&gt;The two posts before this one describe work finished weeks earlier: a
retrieval bug with twenty measured queries, a presence layer with its design
alternatives. I wrote each in an afternoon, and I reconstructed nothing. Every
number, every rejected option, every reason was already written down, in a
predictable place, in a form I could trust had not drifted since.&lt;&#x2F;p&gt;
&lt;p&gt;That is not diligence. Left to my own habits I document like everyone else:
enthusiastically at the start, then never again. The reason the records exist
is that my projects follow a documentation standard designed for a reader
with less context than any human: a coding agent landing cold. It turns out
that optimizing docs for that reader makes them dramatically better for the
human too - future-me is also a reader landing cold, just with more
overconfidence.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-failure-mode-nobody-can-tell-what-is-true&quot;&gt;The failure mode: nobody can tell what is true&lt;&#x2F;h2&gt;
&lt;p&gt;Before standardizing, I surveyed my own repos. The evidence was embarrassing
in a specific, countable way:&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;A monolithic build-plan file, plus a 960-line &quot;shipped&quot; ledger, with no
in-progress state and two sources of truth for &quot;done&quot; that disagreed.&lt;&#x2F;li&gt;
&lt;li&gt;A &quot;current task&quot; note that was stale more often than not, because nothing
ever forced it to be true.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;For a human this is friction. For an agent it is poison: an agent reads the
stale note and acts on it. The core question a repo has to answer - what is
decided, what is to-do, what is done, what is in-flight - had no reliable
answer anywhere.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;decisions-are-immutable-change-means-supersede&quot;&gt;Decisions are immutable; change means supersede&lt;&#x2F;h2&gt;
&lt;p&gt;The cornerstone rule: &lt;strong&gt;an accepted decision record is never edited again.&lt;&#x2F;strong&gt;
One decision per ADR, short. If the decision changes, you write a &lt;em&gt;new&lt;&#x2F;em&gt; ADR
that supersedes the old one, and the old one&#x27;s status flips to
&quot;superseded by NNNN&quot; - a one-line metadata change, with the body untouched.&lt;&#x2F;p&gt;
&lt;p&gt;Before adopting it I ran a research pass across the published field, with
each claim adversarially verified, and this was the single strongest
convergence: four independent primary sources (AWS&#x27;s prescriptive guidance,
adr-tools, log4brains, MADR) all name immutability-plus-supersession as the
cure for exactly the append-only journal sprawl my repos exhibited.&lt;&#x2F;p&gt;
&lt;p&gt;The deeper reason it works is trust, and trust is an agent requirement
before it is a style preference. An agent quoting a mutable document has to
wonder whether the text changed since the decision was made. An immutable
record with an explicit supersession chain cannot lie about its history: if
its status says accepted, the body means today what it meant the day it was
accepted. That property is what let me lift design rationale into a blog
post weeks later without re-verifying any of it against the code.&lt;&#x2F;p&gt;
&lt;p&gt;One companion rule keeps the immutability honest: &lt;strong&gt;as-built reality does
not live on the ADR.&lt;&#x2F;strong&gt; The ADR records the decision and carries one pointer
line to the plan that implemented it. How the implementation actually went -
the surprises, the deviations - lives in the plan. Without this split, the
pressure to &quot;just add a note&quot; reintroduces the journal.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;plans-stable-task-ids-and-a-decision-log&quot;&gt;Plans: stable task ids and a decision log&lt;&#x2F;h2&gt;
&lt;p&gt;Work items live as plan files in an &lt;code&gt;active&#x2F;&lt;&#x2F;code&gt; directory, moving to
&lt;code&gt;archive&#x2F;&lt;&#x2F;code&gt; when done. The skeleton is boring on purpose: intent, approach,
a task checklist, and a decision log. Two rules carry the weight:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Task ids are stable forever.&lt;&#x2F;strong&gt; Tasks are &lt;code&gt;T1&lt;&#x2F;code&gt;, &lt;code&gt;T2&lt;&#x2F;code&gt;, ... assigned at
creation and never renumbered. A split or reordered task gets a fresh id; an
existing id never silently changes meaning. This sounds pedantic until you
have commits, session notes, and a current-task pointer all referencing
&quot;T4&quot; - renumbering would quietly repoint every one of them at different
work. Stable ids are what let a task be checked off with a one-line
&quot;done, as built&quot; note and trusted years later.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Small decisions go in the plan&#x27;s decision log, not new ADRs.&lt;&#x2F;strong&gt; The
threshold: an ADR is for a decision that outlives one work item. Everything
local - &quot;we fold the panel by section, not per-fact, because...&quot; - lands as
a dated entry in the plan that needed it. This keeps ADRs rare and heavy
while still capturing the reasoning that debugging (or blogging) later
depends on. My measurement-traps post was essentially written from these
entries; the traps were recorded as decision-log entries the day they bit.&lt;&#x2F;p&gt;
&lt;p&gt;There is also a gate for honesty about ignorance: an unresolved question in
a plan is written as an explicit &lt;code&gt;[NEEDS CLARIFICATION: ...]&lt;&#x2F;code&gt; marker, and no
task it gates may start while one remains. An agent that hits the marker
asks instead of guessing. The alternative - the agent inventing an answer
with full confidence - is how plausible-but-wrong work happens.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;one-authoritative-home-per-question&quot;&gt;One authoritative home per question&lt;&#x2F;h2&gt;
&lt;p&gt;The piece that killed the most staleness is a small table: every question a
reader might ask has exactly one place whose answer counts.&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Question&lt;&#x2F;th&gt;&lt;th&gt;Authoritative home&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Why is it built this way?&lt;&#x2F;td&gt;&lt;td&gt;ADRs&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;What does it actually do?&lt;&#x2F;td&gt;&lt;td&gt;The code&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;What is to-do &#x2F; done?&lt;&#x2F;td&gt;&lt;td&gt;The plan&#x27;s task checklist&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Who is working on what right now?&lt;&#x2F;td&gt;&lt;td&gt;The ephemeral presence layer&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;What is this branch about?&lt;&#x2F;td&gt;&lt;td&gt;A one-line pointer file into the active plan&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;Two entries deserve comment. First, &lt;strong&gt;the code stays authoritative for
&lt;em&gt;what&lt;&#x2F;em&gt;&lt;&#x2F;strong&gt;. The research pass surfaced the fashionable inverse - make the spec
canonical and subordinate the code to it - and it failed adversarial
verification outright. Docs own intent and rationale; behavior is what ships.&lt;&#x2F;p&gt;
&lt;p&gt;Second, &lt;strong&gt;in-flight status is deliberately ephemeral&lt;&#x2F;strong&gt;. &quot;Session A is
working on T3&quot; lives in the live presence registry from the previous post,
never as a durable marker in the plan. A durable in-progress flag is
precisely the thing that rots when a session dies mid-task - which is how my
old current-task note became a professional liar. Durable files record what
is true durably; live state lives somewhere that dies with the process.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;discipline-that-is-not-enforced-is-decoration&quot;&gt;Discipline that is not enforced is decoration&lt;&#x2F;h2&gt;
&lt;p&gt;Everything above would decay in a month if it relied on my consistency, so
it does not:&lt;&#x2F;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;&#x2F;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&#x27;s implemented-by path that still resolves.&lt;&#x2F;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;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The lint matters more than it looks. It converts &quot;we agreed to do this&quot; into
&quot;the build tells you when you did not,&quot; which is the only form of agreement
that survives contact with a busy week.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-transfers&quot;&gt;What transfers&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Write for the reader with the least context.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Make records immutable and change explicit.&lt;&#x2F;strong&gt; A document that cannot
have been quietly edited is the only kind either of you can quote without
re-verifying.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Give every question exactly one authoritative home&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Stable ids beat tidy renumbering.&lt;&#x2F;strong&gt; Anything referenced from outside a
document must never change meaning.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Enforce mechanically or watch it drift.&lt;&#x2F;strong&gt; Templates make compliance
cheap; a lint makes drift loud.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The compounding effect surprised me. Each layer - the memory system, the
presence layer, this standard - was built to solve its own local problem,
and the posts about them exist because the layers also happen to document
each other. The infrastructure that keeps my agents honest turned out to be
the same infrastructure that lets me tell you about it.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>My agent system dreams at night, and that is where its memory comes from</title>
        <published>2026-07-29T00:00:00+00:00</published>
        <updated>2026-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://olund.dev/writing/dreaming/"/>
        <id>https://olund.dev/writing/dreaming/</id>
        
        <content type="html" xml:base="https://olund.dev/writing/dreaming/">&lt;p&gt;The first four posts in this series keep referring to durable facts my
agents recall across sessions. None of those facts got there because an
agent, mid-task, decided &quot;this is worth remembering forever.&quot; Almost
everything an agent thinks is memorable in the moment is noise a week
later, and an agent given a direct write path to durable memory will fill
it with confident junk.&lt;&#x2F;p&gt;
&lt;p&gt;Instead, my system does what brains do: it consolidates offline. Twice a
day, on a timer, a process wakes up, reads what happened since it last ran,
and decides - slowly, with gates - what deserves to survive. I call it
dreaming, and the name has turned out to be more than a joke: the design
questions are genuinely sleep-shaped. What gets replayed? What gets
promoted to long-term storage? What gets discarded? And what happens when
the process misfires?&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-shape-a-timer-not-a-daemon&quot;&gt;The shape: a timer, not a daemon&lt;&#x2F;h2&gt;
&lt;p&gt;Dreaming is a one-shot process fired by a scheduler - a morning cycle for
lighter work (recommendations, &quot;what needs attention&quot;), an evening cycle
for the heavy memory-promotion pass. No resident daemon, no queue service.
Each run is a fresh process that reads files, thinks, writes files, and
exits. Everything in my stack is filesystem-first, and consolidation is no
exception: if the machine is off, the cycle is skipped and the next one
picks up the unprocessed range.&lt;&#x2F;p&gt;
&lt;p&gt;Two operational rules matter more than the schedule:&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Exactly one machine dreams.&lt;&#x2F;strong&gt; My state syncs across machines, and two
consolidators writing the same durable files would conflict endlessly. One
machine owns dreaming; the others read the results. Single-writer is the
cheapest concurrency model that exists, and choosing it here removed a
whole category of merge problems before they happened.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Every cycle has a hard token budget.&lt;&#x2F;strong&gt; The reflection step calls an LLM,
and an unbounded loop over a busy day&#x27;s events is an unbounded bill. A
cycle gets a fixed input and output cap; hitting the cap mid-cycle means
finish gating what you already produced, log the overrun, and let the next
cycle continue from there. A runaway day costs a known maximum. The daily
spend lands around one to three dollars, which I consider cheap for a
memory that maintains itself.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-pipeline-cluster-reflect-score-gate&quot;&gt;The pipeline: cluster, reflect, score, gate&lt;&#x2F;h2&gt;
&lt;p&gt;The evening pass runs the day&#x27;s episodic events - tool calls, session
summaries, captured thoughts - through a fixed sequence:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cluster.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Reflect.&lt;&#x2F;strong&gt; An LLM reads each cluster and proposes candidates: &quot;these
events support the fact that X.&quot; Each candidate carries the ids of the
events supporting it, so provenance survives the whole trip.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Score.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Judge.&lt;&#x2F;strong&gt; Two LLM-backed checks run against existing memory: does this
contradict a stored fact, and is it a duplicate?&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Promote or queue.&lt;&#x2F;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&#x27;s
history &lt;em&gt;is&lt;&#x2F;em&gt; an audit log.&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;The single most important design fact: &lt;strong&gt;the interesting engineering is
entirely in steps 3 through 5.&lt;&#x2F;strong&gt; Generating candidates is easy; any LLM
over any event log will happily propose memories. The system&#x27;s quality is
decided by what it refuses to write. Durable memory pollution compounds -
a bad fact gets recalled, believed, cited, and built upon by dozens of
future sessions - so the write gate is where the paranoia belongs.&lt;&#x2F;p&gt;
&lt;p&gt;Concretely: auto-promotion requires both a confidence threshold and at
least three supporting events. A candidate with two supporters can be
judged genuinely durable by the reflector, and it still cannot enter
memory unattended - it routes to the review queue instead. The rule is not
&quot;two events are not evidence&quot;; it is &quot;two events are not enough evidence
to skip the human.&quot;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;two-failure-stories-worth-their-tuition&quot;&gt;Two failure stories worth their tuition&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;The silent floor mismatch.&lt;&#x2F;strong&gt; For a while, the clustering stage was
allowed to form two-event clusters, but the scoring stage silently dropped
any candidate with fewer than three supporting events. Every two-event
cluster the reflector judged durable was structurally discarded - not
rejected with a reason, just gone. The pipeline looked healthy: cycles ran
green, promotions happened, nothing errored. It was simply quieter than it
should have been, and quiet is the hardest defect to notice. The fix
lowered the score floor to match the cluster floor and moved the
three-event rule to the auto-promote boundary, where it belongs: the
candidate now survives to the review queue and the human sees it. The
general lesson: when two stages of a pipeline disagree about a threshold,
the disagreement does not error - it silently shrinks your output, and
you will attribute the quietness to &quot;slow week&quot; for months.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;The judges that aborted the dream.&lt;&#x2F;strong&gt; The contradiction and duplicate
judges originally propagated a hard LLM error - an outage, a quota blip -
straight up, aborting the entire cycle and discarding all the clustering
and reflection work before them. Meanwhile a &lt;em&gt;garbled&lt;&#x2F;em&gt; LLM response was
handled gracefully with a conservative default. That asymmetry made no
sense: the transient network error was more destructive than the corrupted
answer. The policy now is uniform: any LLM-backed stage that fails hard
degrades to its conservative default (no contradiction found, assume
novel) and the cycle completes. For a nightly batch process, resilience
beats strictness - a conservative default risks one duplicate card in a
review queue; an abort discards a day.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;consolidation-grows-skills-not-just-facts&quot;&gt;Consolidation grows skills, not just facts&lt;&#x2F;h2&gt;
&lt;p&gt;The part I did not plan and now value most: the same clustering that finds
durable facts also finds &lt;em&gt;recurring work&lt;&#x2F;em&gt;. When the cycle notices the same
kind of procedure performed across at least three instances on multiple
days with no skill covering it, it proposes one - and above a confidence
threshold it auto-writes a draft skill file. The draft is inert: invisible
to every agent harness until I explicitly promote it. The human gate did
not disappear; it moved from &quot;write the draft&quot; to &quot;activate the draft,&quot;
which is a cheaper place for me to pay attention.&lt;&#x2F;p&gt;
&lt;p&gt;This is the sleep metaphor completing itself. Consolidation is not just
deciding what to remember; it is noticing what you keep doing and turning
it into ability. The facts feed recall, the skills feed behavior, and both
come out of the same nightly replay of the day&#x27;s events.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-transfers&quot;&gt;What transfers&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Separate the write path from the work path.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Put the engineering into refusal.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Move human gates to the cheapest checkpoint; do not delete them.&lt;&#x2F;strong&gt;
Auto-draft plus manual activate beats both manual-everything and
full autonomy.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Batch processes should degrade, not abort.&lt;&#x2F;strong&gt; A conservative default
wastes a little; a dead cycle wastes the day.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Audit your pipeline for silent disagreements.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Cap the spend structurally.&lt;&#x2F;strong&gt; A per-cycle token ceiling with graceful
overrun turns &quot;LLM loop over unbounded input&quot; from a risk into a line
item.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Every layer in this series feeds this one: the sessions write the events,
the presence layer keeps them attributable, the docs standard records the
decisions, the code map consumes the footsteps - and dreaming is the slow
process at the bottom that turns a day of all of it into the few sentences
worth keeping.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>How I measure whether my agent&#x27;s memory works</title>
        <published>2026-07-29T00:00:00+00:00</published>
        <updated>2026-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://olund.dev/writing/measuring-agent-memory/"/>
        <id>https://olund.dev/writing/measuring-agent-memory/</id>
        
        <content type="html" xml:base="https://olund.dev/writing/measuring-agent-memory/">&lt;p&gt;I run my coding agents on top of a personal memory system I built: durable
facts live in a markdown file, an indexer embeds them with a local model
(bge-m3), and a &lt;code&gt;recall&lt;&#x2F;code&gt; command does semantic search over the result. Sessions
query it constantly. It looked healthy for months.&lt;&#x2F;p&gt;
&lt;p&gt;Then I asked it something I knew was in there: whether my system supports
Windows. The fact existed, word for word, in the durable file. &lt;code&gt;recall&lt;&#x2F;code&gt;
returned zero hits from it. Not ranked low - absent.&lt;&#x2F;p&gt;
&lt;p&gt;This post is the story of finding out why, and more usefully, the methods that
made every step provable instead of plausible. If you are building agent
memory, the specific bug will probably not be yours. The measurement traps
will be.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-bug-the-text-was-never-embedded&quot;&gt;The bug: the text was never embedded&lt;&#x2F;h2&gt;
&lt;p&gt;My indexer chunked the memory file by markdown section. One section had grown
to 904 tokens. The embedding library I use (fastembed) silently truncates
input at a default of 512 tokens unless you override it, and nothing in my
code did. Nothing upstream bounded chunk size either.&lt;&#x2F;p&gt;
&lt;p&gt;So the encoder saw the first 1687 characters of a 3090-character section and
threw away the rest - 45.4% of the text, containing three entire facts. No
error, no warning, no log line. The stored vector simply did not contain
their meaning. Retrieval was not failing to find them. There was nothing to
find.&lt;&#x2F;p&gt;
&lt;p&gt;The nasty property: this bug is monotone with growth. Every new fact appended
to the section pushed more content past the cut. The memory system degraded
&lt;em&gt;because&lt;&#x2F;em&gt; it was being used.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;proving-it-causally-not-correlationally&quot;&gt;Proving it causally, not correlationally&lt;&#x2F;h2&gt;
&lt;p&gt;&quot;The section is long and recall is bad&quot; is a correlation. Before touching any
code I wanted the mechanism pinned, because my first diagnosis was wrong (more
on that below). The probe that settled it:&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;query text&lt;&#x2F;th&gt;&lt;th&gt;distance to the stored chunk&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;the full 3090-char section&lt;&#x2F;td&gt;&lt;td&gt;0.0000&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;only the first 1687 chars&lt;&#x2F;td&gt;&lt;td&gt;0.0000&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;only the discarded 1403-char tail&lt;&#x2F;td&gt;&lt;td&gt;0.4247&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;The full section and its truncated prefix embed to the &lt;em&gt;same vector&lt;&#x2F;em&gt; - exact
zero distance. The tail contributes nothing at all. That is not what dilution
looks like; that is a step function. Correlating each fact&#x27;s character offset
with its self-retrieval distance gave a Spearman rho of +0.952: a cliff at the
cut position, not a gradient.&lt;&#x2F;p&gt;
&lt;p&gt;This mattered because my original hypothesis was centroid dilution - one
embedding averaging over many facts, dominated by the numerous ones. Dilution
suggests fixes like re-ranking or splitting long sections more finely.
Truncation means data is missing from the index entirely, and no amount of
ranking cleverness can retrieve a vector that was never computed. Wrong
mechanism, wrong fix. The ten-minute probe was cheaper than shipping the
wrong repair.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-fix-that-looks-right-and-the-one-that-is&quot;&gt;The fix that looks right and the one that is&lt;&#x2F;h2&gt;
&lt;p&gt;The one-line fix exists: the model genuinely supports an 8192-token window,
and a single &lt;code&gt;.with_max_length(8192)&lt;&#x2F;code&gt; restores it. I rejected it for three
reasons:&lt;&#x2F;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&#x27;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;&#x2F;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;&#x2F;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;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;So the fix was to make the &lt;em&gt;fact&lt;&#x2F;em&gt; the unit of embedding: the indexer now
parses the memory file&#x27;s per-fact structure and emits one chunk per fact.
Every fact is 58-335 tokens, comfortably inside any window.&lt;&#x2F;p&gt;
&lt;p&gt;And separately - this is the part that prevents recurrence - the indexer now
counts every chunk&#x27;s tokens against the encoder&#x27;s actual window and warns on
offenders, and my health command reports the count. The window &lt;em&gt;guard&lt;&#x2F;em&gt;, not
the window &lt;em&gt;raise&lt;&#x2F;em&gt;, is what stops this bug class from coming back. Four wiki
documents are over the limit today; the detector names them, and that debt is
visible instead of silent.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;measuring-the-fix-instead-of-arguing-about-it&quot;&gt;Measuring the fix instead of arguing about it&lt;&#x2F;h2&gt;
&lt;p&gt;The design had one open risk I could not reason away: per-fact chunks might
lose on &lt;em&gt;broad&lt;&#x2F;em&gt; queries (&quot;what side projects am I working on&quot;), where a big
section chunk plausibly wins by covering more topics. I had accepted this as
a trade-off in the design review.&lt;&#x2F;p&gt;
&lt;p&gt;Instead of accepting it, I measured it. Two indexes over the identical
corpus: arm A chunked by section (exactly reproducing the live store), arm B
chunked by fact. Twenty queries against both - memory-directed ones, broad
ones, and eight wiki-directed queries as a counter-direction check, because
an improvement that comes from crowding out the rest of the corpus is not an
improvement.&lt;&#x2F;p&gt;
&lt;p&gt;The accepted trade-off inverted:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Rank-1 distance was never worse: 12 of 12 memory-directed queries.&lt;&#x2F;li&gt;
&lt;li&gt;Memory facts present in a default top-5 rose from 9 to 27 across queries.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;Four narrow probes went from zero memory chunks in the top 5 to rank 1.&lt;&#x2F;li&gt;
&lt;li&gt;The eight wiki queries kept rank 1 unchanged, all eight.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The section chunk&#x27;s assumed advantage on broad queries never existed. It was
an artifact of there being only two memory chunks in the index at all - of
course one of them &quot;won broad queries&quot;; there was nothing else to return.
This is my strongest argument for measuring over arguing: the risk I had
formally accepted in a design document was not real.&lt;&#x2F;p&gt;
&lt;p&gt;End to end, the six-probe acceptance set went from 0&#x2F;6 correct to 6&#x2F;6, and
every one of the twelve facts now self-retrieves at exact d=0.0000 - which
doubles as a standing proof that no fact is truncated anymore.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-two-ways-my-before-measurement-almost-lied&quot;&gt;The two ways my &quot;before&quot; measurement almost lied&lt;&#x2F;h2&gt;
&lt;p&gt;An honest before&#x2F;after table requires the &quot;before&quot; arm to actually measure
the old behavior. Mine silently stopped doing that twice, and both failures
produced the same poisonous output: a table showing no change at all.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Trap one: the old store was auto-upgraded by the new binary.&lt;&#x2F;strong&gt; My index
store wipes and rebuilds itself when its schema version is outdated. The fix
bumped the schema. So when I pointed the &lt;em&gt;new&lt;&#x2F;em&gt; binary at the preserved
&lt;em&gt;old&lt;&#x2F;em&gt; store to get &quot;before&quot; numbers, the first query quietly rebuilt the
entire store with per-fact chunking - converting my before-arm into a second
after-arm. The before-arm has to be driven by the old &lt;em&gt;binary&lt;&#x2F;em&gt;, not just the
old data.&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Trap two: the old binary overwrote the new one.&lt;&#x2F;strong&gt; I built that old binary
from a git worktree, but with the build cache directory shared with the main
checkout - which overwrote the release binary I was using for the &quot;after&quot;
column. Both columns were now measuring pre-fix code.&lt;&#x2F;p&gt;
&lt;p&gt;I caught it because of two smells: the after column exactly equaled the
before column, and a known-good result I had measured minutes earlier had
silently regressed. The rule I keep now: pin each arm to its own binary path,
and assert the two binaries differ before trusting any comparison between
them. &quot;Before equals after, everywhere, exactly&quot; is not a null result. It is
an instrument failure.&lt;&#x2F;p&gt;
&lt;p&gt;A third, related trap lives in the test suite. My mock embedder hashes the
full input text with no truncation, so every truncation test passes against
it - including on the broken code. The causal tests require the real encoder
(a 558MB model download I keep out of CI), so CI instead pins the structural
properties the fix guarantees: one chunk per fact, identity fields populated.
The proof runs by hand at the gate; the regression guard runs on every
commit; and I wrote down which is which, so a green CI run cannot be mistaken
for the causal proof.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-transfers&quot;&gt;What transfers&lt;&#x2F;h2&gt;
&lt;p&gt;None of this needed special tooling - a CLI, a scratch directory for each
index, and discipline about what counts as evidence. The parts I would carry
to any agent-memory system:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prove mechanism before choosing a fix.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Make missing data observable.&lt;&#x2F;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 &quot;not measured&quot; must render as &lt;em&gt;not measured&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Measure accepted risks; they may not exist.&lt;&#x2F;strong&gt; The broad-query trade-off
survived a design review as a reasonable-sounding caveat. It did not
survive twenty queries.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Distrust symmetric results.&lt;&#x2F;strong&gt; Identical before&#x2F;after numbers mean your
instrument broke, until proven otherwise.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Claim what you measured, nothing more.&lt;&#x2F;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;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;The whole investigation - diagnosis, fix, and both measurement arms - is
recorded as an architecture decision record and a work plan in the project&#x27;s
docs, which is also why I can write this post months of context later without
reconstructing anything from memory. But that discipline is a story for
another post.&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="en">
        <title>Running parallel agent sessions without them stepping on each other</title>
        <published>2026-07-29T00:00:00+00:00</published>
        <updated>2026-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://olund.dev/writing/parallel-agent-sessions/"/>
        <id>https://olund.dev/writing/parallel-agent-sessions/</id>
        
        <content type="html" xml:base="https://olund.dev/writing/parallel-agent-sessions/">&lt;p&gt;I usually have several coding-agent sessions running at once. Sometimes on
different projects, often on the same repo: one session deep in a refactor,
one writing docs, one investigating a bug. Nothing about an agent harness
makes this safe by default. The two failure modes show up fast:&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Corruption&lt;&#x2F;strong&gt;: two sessions write the same state file and one clobbers
the other.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Collision&lt;&#x2F;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;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;These feel like one problem (&quot;agents conflict&quot;) but they are structurally
different, and in my memory system they got structurally different fixes.
The corruption fix has no runtime component at all. The collision fix has no
locks. This post is about both, and about the things I deliberately did not
build.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;corruption-solved-by-construction-not-coordination&quot;&gt;Corruption: solved by construction, not coordination&lt;&#x2F;h2&gt;
&lt;p&gt;Each of my agent sessions keeps working memory on disk: a scratchpad,
a decision log, open questions. The naive layout - one set of files per
project branch - dies immediately with two live sessions, and the classic
answer is locking or serialized writes through a daemon.&lt;&#x2F;p&gt;
&lt;p&gt;The layout that needs neither: &lt;strong&gt;every session writes only to its own
directory, keyed by its session id&lt;&#x2F;strong&gt;. Subagents inherit the id with a suffix,
so even a session&#x27;s own workers cannot collide with their parent. No two
writers ever touch the same file, not because a lock stops them, but because
no shared file exists. At session start, a materialize step folds all
sessions&#x27; entries into one read-only merged view, so a new session still
reads everything that happened on the branch - stamped as historical
reference, because a snapshot is stale by definition.&lt;&#x2F;p&gt;
&lt;p&gt;There is exactly one exception, and it is load-bearing: a single
&lt;code&gt;current-task.md&lt;&#x2F;code&gt; per branch, shared by every session, last writer wins.
That file answers &quot;what is this branch about right now&quot;, which is only
useful &lt;em&gt;because&lt;&#x2F;em&gt; it is shared. The design rule that fell out: share nothing
by default, and when you do share, share one small file whose whole point is
being the single meeting place.&lt;&#x2F;p&gt;
&lt;p&gt;This solved corruption completely. It did nothing for collision - a session
writing safely in its own directory can still cheerfully redo work another
session finished an hour ago.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;collision-sessions-need-to-see-each-other&quot;&gt;Collision: sessions need to see each other&lt;&#x2F;h2&gt;
&lt;p&gt;The actual pain, once corruption was gone, was launching a second session
into a repo blind. It did not know a first session existed, let alone what
files it was touching. My fix is a &lt;strong&gt;presence layer&lt;&#x2F;strong&gt;: a machine-local
registry of live sessions, maintained entirely by lifecycle hooks that were
already firing on session start, tool use, and session end.&lt;&#x2F;p&gt;
&lt;p&gt;Each record carries the branch, the files the session has touched, an
optional one-line intent, and a freshness timestamp. When a new session
starts, it gets a block injected into its context:&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #BFBDB6; background-color: #0D1017;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;3 other session(s) live on this branch right now - avoid editing the same files:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;- 6679ade6 - touching channel.md, production-readiness.md (active 5m ago)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;- 30b9890c - touching audit.rs, audit.test.ts (active 52m ago)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;- c5e09075 - touching review-queue.md, CONTEXT.md (active 1h ago)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;That is most of the feature. The dominant collision case was never two
sessions racing for a file at the same millisecond; it was me starting
session B without remembering what session A was doing. Awareness at launch
time, plus an on-demand &quot;who else is here&quot; query the agent can run
mid-session, covers almost all of it.&lt;&#x2F;p&gt;
&lt;p&gt;The part that took actual design care is trust. A presence registry is only
useful if the agent can believe it: phantom peers - records of sessions that
died without cleaning up - turn the block into noise the agent learns to
ignore, and then the feature is worse than nothing. Session-end hooks are
unreliable by nature (crashes, hard kills, dead batteries), so liveness is
checked at &lt;em&gt;read&lt;&#x2F;em&gt; time against the OS: does the recorded pid exist, and does
its process start-time match what the record captured? The second check
matters more than it looks. Pids get reused; without the start-time match, a
recycled pid makes a dead session read as alive forever, which is precisely
the failure the layer exists to prevent.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;no-locks-on-purpose&quot;&gt;No locks, on purpose&lt;&#x2F;h2&gt;
&lt;p&gt;The instinctive design here is mutual exclusion: lock files a session is
editing, deny the other session access. I rejected it, and the reasoning
generalizes:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The problem was awareness, not exclusion.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A lock is hostile to the human in the loop.&lt;&#x2F;strong&gt; These are my own parallel
sessions. A hard lock means my repo tells &lt;em&gt;me&lt;&#x2F;em&gt; no when I ask a second
session to touch a file the first one grazed an hour ago.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Advisory degrades gracefully; locks degrade catastrophically.&lt;&#x2F;strong&gt; A stale
presence record wastes a warning line. A stale lock blocks work until
someone hunts it down.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;So the whole layer is advisory. It informs, it never blocks. Weeks in, I
have not once wished for the lock.&lt;&#x2F;p&gt;
&lt;p&gt;I also rejected the heavier alternative: a durable task queue where every
session formally claims a task before touching anything. Queues answer
&quot;what is the status of this work&quot; - a real question, but a different one.
For live collision avoidance a claimed task is simultaneously too coarse
(&quot;someone is on the refactor&quot; does not tell you which file they are editing
right now) and too much ceremony (every session pays a claim step to defend
against a rare event). Presence is finer and free: the hooks fire anyway.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;the-human-side-which-session-needs-me&quot;&gt;The human side: which session needs me&lt;&#x2F;h2&gt;
&lt;p&gt;Coordination between sessions is half the story. The other half is
coordinating &lt;em&gt;me&lt;&#x2F;em&gt;. With four sessions across three projects, the expensive
question stops being &quot;are the agents colliding&quot; and becomes &quot;which one is
waiting on my input while I stare at a different terminal&quot;.&lt;&#x2F;p&gt;
&lt;p&gt;Presence records carry a state field for this: &lt;code&gt;working&lt;&#x2F;code&gt; or &lt;code&gt;waiting&lt;&#x2F;code&gt;. Turn
boundaries drive it - submitting a prompt marks the session working,
the agent finishing its turn marks it waiting. The subtlety is that tool
activity alone cannot tell you this: a session that is thinking, or reading
files, looks idle by every activity metric while genuinely busy. Only the
turn boundary is truthful. A workbench view renders the sessions per
project with a marker on whoever is waiting, and a backgrounded session
going idle fires a notification.&lt;&#x2F;p&gt;
&lt;p&gt;The effect on throughput is larger than the collision fix. Parallel
sessions were already &lt;em&gt;safe&lt;&#x2F;em&gt;; this made them &lt;em&gt;worth it&lt;&#x2F;em&gt;, because agent idle
time - finished, waiting, unnoticed - was where the parallelism actually
leaked.&lt;&#x2F;p&gt;
&lt;h2 id=&quot;what-transfers&quot;&gt;What transfers&lt;&#x2F;h2&gt;
&lt;p&gt;The specifics are mine, but the shapes are portable to any multi-session
agent setup:&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Partition state by writer; make shared files a deliberate exception.&lt;&#x2F;strong&gt;
Corruption problems you solve by construction do not come back. Locks you
add have to be right forever.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Inject awareness at start time.&lt;&#x2F;strong&gt; The dominant collision is launching
blind, not racing. A cheap &quot;who is here, touching what&quot; block at session
start beats sophisticated conflict detection you build later.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;A registry nobody trusts is worse than none.&lt;&#x2F;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;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Prefer advisory over blocking until proven otherwise.&lt;&#x2F;strong&gt; Especially when
every session ultimately answers to one human.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Track waiting, not just working.&lt;&#x2F;strong&gt; If you run parallel sessions, the
bottleneck quietly becomes your own attention. Surface it.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;None of this needed a coordinator process, a message bus, or a database.
It is a directory convention, a handful of JSON files in a cache directory,
and hooks that were already firing. The boring infrastructure was the
feature.&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
