ReadingTwo Writers, One Index: How Static Files Corrupt Agent Memory
8 min read

Two Writers, One Index: How Static Files Corrupt Agent Memory

Both subagents read version N, so B's write 80 ms later lands on top of A's.

Five steps with two agents and one doc at version N. Both agents read N. A saves first, then B saves its old copy on top 80 ms later, and A's work is gone. Inside one transaction the version check refuses B's write, B re-reads and retries, and the doc ends as A+B.Two lanes, A and B, sit above and below a doc at version N; both read N. A cross marks No transaction: A writes first, then 80 ms later B writes over it, ending in A's work gone. A check marks One transaction: B's write is refused, then B re-reads the doc and retries, leaving A+B, both kept. both read N 80 ms No transaction A doc B N A B A's work gone One transaction A doc B N A refused re-reads retries A+B both kept Five steps with two agents and one doc at version N. Both agents read N. A saves first, then B saves its old copy on top 80 ms later, and A's work is gone. Inside one transaction the version check refuses B's write, B re-reads and retries, and the doc ends as A+B.Two lanes, A and B, sit above and below a doc at version N; both read N. A cross marks No transaction: A writes first, then 80 ms later B writes over it, ending in A's work gone. A check marks One transaction: B's write is refused, then B re-reads the doc and retries, leaving A+B, both kept. both read N 80 ms No transaction A doc B N A B A's work gone One transaction A doc B N A refused re-reads retries A+B both kept

Both agents read version N

Step 1: agents A and B both read version N of the doc, so each one holds its own copy of N. both read N No transaction A doc B N two copies of N

A saves its change first

Step 2: A writes its change, so the doc now holds A. both read N No transaction A doc B N A doc is A

B saves its old copy on top

Step 3: with no transaction, B writes 80 ms later. Its copy was made from N, so it lands on top of A and A's work is gone. both read N 80 ms No transaction A doc B N A B A's work gone

A transaction checks the version first

Step 4: inside one transaction, the doc has moved past version N, so B's write is refused. both read N 80 ms One transaction A doc B N A refused N is old, so no write

B re-reads and retries, and both stay

Step 5: B re-reads the doc and retries its write, and the doc ends as A+B with both changes kept. both read N 80 ms One transaction A doc B N A refused re-reads retries A+B both kept
1 of 5
Figure 1. Five steps with two agents and one doc at version N. Both agents read N. A saves first, then B saves its old copy on top 80 ms later, and A's work is gone. Inside one transaction the version check refuses B's write, B re-reads and retries, and the doc ends as A+B.

I moved agent memory out of a markdown summary index and into Firestore transactions on Cloud Run, so a write against a stale version is refused and retried instead of erasing the other one.

Listen to the audio overview(2:20)Fenrir Studio Voice
0:00
2:20

I sent two subagents to update one shared Firestore Google's cloud document database. It stores each record as a document and can change documents inside transactions. user profile at the same time. Both Subagent A helper agent that a main agent starts to do one part of a job, often in parallel with others. read Version A number on a record that goes up by one on each save, so an old copy shows. N of the document in the same millisecond. Subagent A saved its change. 80 ms later, Subagent B saved its own copy on top. B never saw A's change, so A's work was gone, and no error showed. This is a classic distributed split-brain Race condition A bug where the result depends on which of two processes happens to finish first..

Agent memory in flat Markdown A plain text format with light marks, such as # for a heading. Agents often keep notes in it. files breaks the same way, and so does any memory that agents write with no coordination. In production, it is sure to corrupt state. In long sessions with many turns, file-based memory also drifts away from the real state of each record. Then my model reasons over stale summaries and hallucinates. I call that a split-brain condition, too.

As I explored in Part 1: The 10 Cognitive Biases of Autonomous Systems, memory drift in long-running agents is rarely an LLM prompt failure. I found it is a Dual-write Saving one fact in two places, one after the other. If the second save fails, the two disagree. cache invalidation breakdown. Its cause is treating language models as database engines. To end the drift, I need ACID Atomic, consistent, isolated, durable. A database promise that each transaction lands whole or not at all. transaction boundaries and an atomic lock per session in a transactional store. I use Firestore Transaction Reads and writes that the database runs as one step. All of it happens, or none of it. for mine.

The distributed systems paradox: I often see engineers treat agent memory drift as a prompting defect. They try to fix the hallucinations with longer system prompts. In my production systems, memory drift in multi-turn agents is a dual-write cache invalidation failure. Its cause is that the language model got the database's indexing job.
PART 01

The static index anti-pattern and markdown storage

To keep my context windows small and save input tokens, I first tried a two-tier storage pattern:

  1. Tier 1 (granular entity files): One file per record, with its full detail, such as contacts/alice.md or tasks/task-402.json.
  2. Tier 2 (the summary index): One high-level markdown file (such as INDEX.md or SUMMARY.md) with a table that sums up the active records, their priorities, and their statuses.
01

The decoupling threshold (turn 20+)

Every time my agent takes an action that changes state, it must do a dual-write. First it updates the entity file. Then it parses the summary table in the index file and updates that, too.

A language model edits a file by probability. Nothing makes the edit a transaction. So under load, dual-writes fail. My model updates the entity file but skips the index table. Or it rewrites the table with slightly changed column headers.

On turn 25, my agent checks its overall status to choose its next step. To save tokens, it reads the shorter summary index. That index holds stale, uncommitted state. My agent treats it as ground truth and enters a hallucination loop that it cannot recover from.

I enforce strict relational consistency for deterministic operational state: tasks, status queues, assignments, and tool locks. Associative episodic memory, such as user preferences and the nuances of a conversation, benefits from vector search Embedding A list of numbers that stands for the meaning of a text, so similar texts can be found by search.. My operational coordination layer still requires ACID transactions.

PART 02

Failure anatomy: The three concurrency and state traps

02

Trap 1: Concurrency collisions and lost updates

Flat files on a local filesystem have no built-in locking. Sometimes my agent spawns parallel subagents, or runs a background heartbeat while an interactive session is active. Then two processes try to write to tasks.md at the same time.

Without atomic row-level locks, the operating system runs those writes in an unpredictable order. Foundational distributed systems work by Leslie Lamport (1978) and Bernstein & Goodman (1981) established the result. Concurrent writes with no agreed order are sure to lose updates. The last process to finish silently overwrites the earlier changes, and no error is raised. That is a Lost update A save that silently replaces another save made in between, so that earlier change is gone.. In my architecture, Jim Gray's transaction concept (1981) is mandatory, because it guarantees ACID isolation.

I built the sandbox below to show how writes with no coordination silently drop state. It steps through the same read, think, write cycle as my terminal demo, one agent at a time. Choose how many agents share the markdown task index, then step through their writes with no transaction and with one:

Interactive Lab · split-brainTwo agents edit one file

Step parallel agents through one shared index file and count the writes that quietly overwrite each other, then compare a transaction.

ai-agent-split-brain-trap-demoScreen recording
Six concurrent subagents: 5 of 6 markdown writes lost with zero errors raised, then 6 of 6 committed under SQLite transactions
03

Trap 2: Memory tearing and container recycles

I run my agents on serverless platforms such as Cloud Run Google's service that runs my containers on demand and stops them, down to zero, when no requests come. or Cloud Functions. They give me elastic scale, but each container has a lifecycle. Say my agent process stops, or scales to zero, while it writes a 50 KB markdown index. Then the file is left half-written.

When the container starts again on the next turn, my agent finds truncated JSON A common text format for structured data. A parser needs the whole object; half of one fails. or broken markdown syntax. Its tool calls crash at once. For deeper client transport failure patterns, read my guide on Client-Side Runtime Agent Resilience.

04

Trap 3: Phantom state loops and stale cache trust

Sometimes an index file says a task is OPEN while the underlying database marks it RESOLVED. My agent then holds two versions of the truth, and it falls into split-brain confusion.

The model does not query the ground truth. It trusts the summary file, decides that the resolution must have failed, and re-runs its API calls against the external systems. In my early tests, this produced duplicate GitHub issues, repeated Slack pings, and wasted compute. For cost protection patterns against runaway execution loops, see The Production Reality of Firebase Spend Caps.

PART 03

The solution: The three-tier zero-drift stack

To remove the split-brain trap for good, I changed my architecture at its base: I offload state indexing from the LLM to a transactional database.

Architectural dimensionFragile file-based storageTransactional cloud memory
One fact, how many copies State coherenceTwo copies to keep in step. Dual-writes required across entity files and markdown index tables.One copy, read fresh. Single source of truth in Cloud Firestore with dynamic indexed queries.
Two writers at once Concurrency controlThe last save wins. No atomic file locks; parallel tool calls overwrite and clobber state.A stale save is refused. An optimistic version check inside runTransaction; the server transaction itself locks what it reads (pessimistic by default in Standard edition).
A container restarts Serverless lifecycleThe state on disk is gone. Local disk state lost on container recycle or scale-to-zero.Nothing lives on disk. Stateless Cloud Run workers with zero persistent local disk state.
What the agent reads Reasoning stabilityAn old summary. Stale markdown summary tables poison multi-turn agent reasoning.The current record. Every query returns ground truth directly from the database engine.
05

Production architecture overview

My transactional memory architecture has three tiers, and each tier has one job: ingress identity, stateless execution, and atomic state storage.

The client's tool call reaches a stateless Cloud Run worker, and every write that worker makes goes through runTransaction into Firestore. A direct write from the client never lands: Security Rules deny every client read and write on agent_tasks, and only the worker's Admin SDK, which bypasses rules, writes. Client Cloud Run worker stateless, no local disk Firestore ACID, fresh reads tool call runTransaction denied by rules direct write The client's tool call reaches a stateless Cloud Run worker, and every write that worker makes goes through runTransaction into Firestore. A direct write from the client never lands: Security Rules deny every client read and write on agent_tasks, and only the worker's Admin SDK, which bypasses rules, writes. Client tool call Cloud Run worker stateless, no local disk runTransaction Firestore ACID, fresh reads direct write denied by rules
Figure 2. The client's tool call reaches a stateless Cloud Run worker, and every write that worker makes goes through runTransaction into Firestore. A direct write from the client never lands: Security Rules deny every client read and write on agent_tasks, and only the worker's Admin SDK, which bypasses rules, writes.
  • Service isolation and client attestation. Firestore Security Rules set allow read, write: if false; on agent_tasks, so no client SDK can touch it. Only my backend's Admin SDK The Firebase server library. It acts as a trusted service account, so Security Rules do not apply to it. writes. Server libraries bypass rules and authorize through the worker's IAM service account, so every write goes through the transactional path. App Check on the client-facing endpoints is a separate abuse control. It plays no part in the drift fix.
  • Stateless tool execution and OCC Optimistic concurrency control. Write without a lock, but check the version first, and retry if it moved. coordination. The worker runs agent tool logic statelessly. No conversation state or scratch files stay on a container's local disk from one turn to the next.
  • Atomic document transactions and dynamic indexes. Firestore provides ACID document transactions, atomic counters, and query indexes that reflect 100% fresh state on every read.
PART 04

Production implementation: Transactional memory in TypeScript

The TypeScript module below is my atomic agent memory engine. It runs on Cloud Run with the Firebase Admin SDK. It uses Firestore transactions to prevent lost updates. On a version conflict, it hands back the current state, so the agent can re-plan and retry. Its dynamic query helpers remove static index files entirely:

transactional-memory-engine.ts
TypeScript
// Transactional Agent Memory Engine for Cloud Run and Cloud Firestore
import { initializeApp, getApps } from 'firebase-admin/app';
import { getFirestore, FieldPath, FieldValue, Timestamp } from 'firebase-admin/firestore';

if (getApps().length === 0) {
  initializeApp(); // Uses Application Default Credentials on Cloud Run
}

const db = getFirestore();

export type TaskStatus = 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED';

export interface AgentTaskDocument {
  title: string;
  status: TaskStatus;
  version: number;
  assignedAgent: string;
  lastUpdated: Timestamp;
  payload: Record<string, unknown>;
}

export interface AgentTaskResponse {
  id: string;
  title: string;
  status: TaskStatus;
  version: number;
  assignedAgent: string;
  lastUpdated: string;
  payload: Record<string, unknown>;
}

export type TaskUpdates = Partial<Pick<AgentTaskDocument, 'status' | 'assignedAgent' | 'payload'>>;

export type MutationResult =
  | { success: true; newVersion: number }
  | { success: false; reason: 'NOT_FOUND' }
  | { success: false; reason: 'VERSION_CONFLICT'; currentState: AgentTaskResponse };

export class TransactionalMemoryEngine {
  private tasksCol = db.collection('agent_tasks');

  private formatTask(id: string, data: AgentTaskDocument): AgentTaskResponse {
    return {
      id,
      title: data.title,
      status: data.status,
      version: data.version,
      assignedAgent: data.assignedAgent,
      lastUpdated: data.lastUpdated
        ? data.lastUpdated.toDate().toISOString()
        : new Date().toISOString(),
      payload: data.payload || {},
    };
  }

  /**
   * Writes only if nobody changed the task since the caller read expectedVersion.
   * The version field is the optimistic check; the transaction makes read, check, and write atomic.
   */
  async updateTaskAtomic(
    taskId: string,
    expectedVersion: number,
    updates: TaskUpdates
  ): Promise<MutationResult> {
    const taskRef = this.tasksCol.doc(taskId);
    const { payload = {}, ...fields } = updates;

    // Expected outcomes are return values; anything thrown (permissions, deadlines, contention) propagates
    return db.runTransaction(async (transaction): Promise<MutationResult> => {
      const snapshot = await transaction.get(taskRef);
      if (!snapshot.exists) {
        return { success: false, reason: 'NOT_FOUND' };
      }

      const currentData = snapshot.data() as AgentTaskDocument;
      if (currentData.version !== expectedVersion) {
        return { success: false, reason: 'VERSION_CONFLICT', currentState: this.formatTask(taskId, currentData) };
      }

      const newVersion = currentData.version + 1;
      // Field/value pairs: a FieldPath merges each key into the payload map as-is, even with dots or slashes
      transaction.update(taskRef, 'version', newVersion, 'lastUpdated', FieldValue.serverTimestamp(),
        ...Object.entries(fields).flat(),
        ...Object.entries(payload).flatMap(([key, value]) => [new FieldPath('payload', key), value]));
      return { success: true, newVersion };
    });
  }

  /**
   * On a version conflict, re-plans against the fresh state and retries, as agent B does in Figure 1.
   */
  async updateWithRetry(
    task: AgentTaskResponse,
    plan: (current: AgentTaskResponse) => TaskUpdates,
    maxAttempts = 3
  ): Promise<MutationResult> {
    let current = task;
    for (let attempt = 1; ; attempt++) {
      const result = await this.updateTaskAtomic(current.id, current.version, plan(current));
      if (result.success || result.reason !== 'VERSION_CONFLICT' || attempt >= maxAttempts) return result;
      current = result.currentState; // re-read: plan again on top of the write that won
    }
  }

  /**
   * Fetches fresh, query-indexed state directly from Firestore.
   */
  async getActiveTasksForAgent(
    agentId: string,
    limitCount = 10
  ): Promise<AgentTaskResponse[]> {
    const querySnapshot = await this.tasksCol
      .where('assignedAgent', '==', agentId)
      .where('status', 'in', ['PENDING', 'IN_PROGRESS'])
      .orderBy('lastUpdated', 'desc')
      .limit(limitCount)
      .get();

    return querySnapshot.docs.map((doc) =>
      this.formatTask(doc.id, doc.data() as AgentTaskDocument)
    );
  }
}
Firestore composite index configuration: A query that combines equality filters, in operators, and custom ordering needs a Composite index A Firestore index over several fields at once. A query that filters and sorts on many fields needs one.. Deploy the following configuration in your firestore.indexes.json:
firestore.indexes.json
JSON
{
  "indexes": [
    {
      "collectionGroup": "agent_tasks",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "assignedAgent", "order": "ASCENDING" },
        { "fieldPath": "status", "order": "ASCENDING" },
        { "fieldPath": "lastUpdated", "order": "DESCENDING" }
      ]
    }
  ]
}

I develop and test offline for $0.00. The Firebase Local Emulator A local program that behaves like a cloud service, so I can test without a real cloud project. Suite runs the entire transactional stack on my machine, so I provision no cloud resources. I start it with firebase emulators:start --only firestore.

Architectural takeaway: In stateful autonomous systems, I never use language models to maintain storage indexes. I offload state to atomic database transactions. The database engine does the indexing and the concurrency control.
Architecture blueprint and spec: Inspect my complete Transactional Memory Blueprint → or scaffold a repository-native specification tree with my noVibes Agent Spec Generator →

Industry validation and benchmarks

First published 12 Aug 2026 · last revised 8 Oct 2026 · 30 revisions

Hand this to your agent

You

Read this essay by Ibrahim Ulukaya, https://ulukaya.dev/posts/ai-agent-split-brain-trap, and look at my project. Where do my agents keep agent memory in a markdown summary index or static files where a second write can erase the first, and what should move into Firestore transactions? Name the file and line for each finding.

Cited by
  1. One Outdated Doc Fooled My Agent: Check Each Claim Against Live Data Before It Acts

    ……e drift in autonomous systems Ghost in the Loop Series: Part 2: The AI Agent Split-Brain Trap and Part 1: The 10 Cognitive Biases of Autonomous Systems…

  2. Instrument: Lord of the Flies: 10-Agent Council Table ($1,000 Treasury)

    Watch 10 council agents hold a survival vote every 5 seconds: unverified rounds drain $65 from the $1,000 shared treasury and banish the highest-suspicion agent until proof-enforced consensus is toggled on.

  3. Instrument: Two agents edit one file

    Step parallel agents through one shared index file and count the writes that quietly overwrite each other, then compare a transaction.

  4. Blueprint: Transactional Memory and State Store Blueprint: Split-Brain Defense

    Static markdown summaries inevitably drift from live database state during concurrent writes. This architecture enforces ACID transaction boundaries and atomic session mutex locks across multi-turn agent sidecars.