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 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:
- Tier 1 (granular entity files): One file per record, with its full detail, such as
contacts/alice.mdortasks/task-402.json. - Tier 2 (the summary index): One high-level markdown file (such as
INDEX.mdorSUMMARY.md) with a table that sums up the active records, their priorities, and their statuses.
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.
Get the next essay by email
One email per new essay: a production failure, taken apart, and the code that fixed it. Reply to any of them to unsubscribe.
Failure anatomy: The three concurrency and state traps
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:
Step parallel agents through one shared index file and count the writes that quietly overwrite each other, then compare a transaction.
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.
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.
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 dimension | Fragile file-based storage | Transactional cloud memory |
|---|---|---|
| One fact, how many copies State coherence | Two 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 control | The 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 lifecycle | The 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 stability | An old summary. Stale markdown summary tables poison multi-turn agent reasoning. | The current record. Every query returns ground truth directly from the database engine. |
Production architecture overview
My transactional memory architecture has three tiers, and each tier has one job: ingress identity, stateless execution, and atomic state storage.
- Service isolation and client attestation. Firestore Security Rules set
allow read, write: if false;onagent_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.
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 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)
);
}
}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:{
"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.
Industry validation and benchmarks
- Remember and Reweight: Enhancing Multi-Agent Debate with Experience Memory and Confidence Estimation (Sep 2026, arXiv:2609.03619v1): Confirms that concurrent multi-agent state updates require explicit confidence-weighted state reconciliation to prevent conflicting memory overwrites.
- Trace2Tower: Transition-Aware EigenTrace Induction of Multi-Level Skills for LLM Agents (Sep 2026, arXiv:2609.05261v1): Confirms that multi-agent transition traces must enforce strict state-transition preconditions to prevent divergent execution graphs.
- Bernstein and Goodman (1981): Concurrency Control in Distributed Database Systems (ACM Computing Surveys)
- Leslie Lamport (1978): Time, Clocks, and the Ordering of Events in a Distributed System (CACM)
- Jim Gray (1981): The Transaction Concept: Virtues and Limitations (VLDB)
- Cloud Firestore Transactions and Batched Writes
- Firebase App Check Overview
- Cloud Run Serverless Compute Architecture
- Firebase Local Emulator Suite
- Google Genkit Open Source Framework