Encrypted Agent Session Continuity: Resuming AI Work Without Leaking Context
How to architect encrypted session persistence so AI agents can resume work across restarts, crashes, and deployments without exposing sensitive context to the storage layer.
AI agents that run complex, multi-step workflows face a fundamental tension: they need to persist state so they can survive restarts, but that state often contains sensitive data — API responses, user context, intermediate reasoning — that you cannot safely store in plaintext.
This post walks through the architectural patterns for encrypted agent session continuity: how to serialize, encrypt, and resume agent sessions in a way that keeps your storage layer zero-knowledge while still being practical to implement.
Why Session Persistence Is Harder Than It Looks
A stateless HTTP request is easy to reason about. An AI agent in the middle of a 47-step workflow is not. When that agent crashes or is rescheduled to a different container, it needs to reconstruct its position in the task graph, its working memory, and any tool outputs it accumulated along the way.
The naive approach is to serialize everything to a database. This works until you realize what ends up in that snapshot: user messages, file contents the agent fetched, credentials it was handed, intermediate reasoning traces that might reveal business logic. Your storage layer now holds a plaintext copy of everything sensitive that flowed through your agent.
Encrypting at the database level (encryption at rest) doesn't fix this. The database decrypts before returning data to your application, so the storage provider — or anyone with database access — can read session contents. You need the encryption to happen before data leaves your application.
The Core Pattern: Client-Side Session Encryption
The fundamental approach is to encrypt session snapshots before writing them to any persistence layer. The session key lives only in memory (or in a key management service), never alongside the data it protects.
Here's the basic structure:
interface AgentSessionSnapshot {
sessionId: string;
checkpoint: number;
taskGraph: SerializedTaskGraph;
workingMemory: MemoryEntry[];
toolOutputs: Record<string, unknown>;
timestamp: number;
}
async function saveSession(
snapshot: AgentSessionSnapshot,
encryptionKey: CryptoKey
): Promise<void> {
const plaintext = new TextEncoder().encode(JSON.stringify(snapshot));
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
encryptionKey,
plaintext
);
const payload = {
iv: Buffer.from(iv).toString("base64"),
data: Buffer.from(ciphertext).toString("base64"),
};
await storage.put(`sessions/${snapshot.sessionId}`, JSON.stringify(payload));
}
async function loadSession(
sessionId: string,
encryptionKey: CryptoKey
): Promise<AgentSessionSnapshot | null> {
const raw = await storage.get(`sessions/${sessionId}`);
if (!raw) return null;
const payload = JSON.parse(raw);
const iv = Buffer.from(payload.iv, "base64");
const ciphertext = Buffer.from(payload.data, "base64");
const plaintext = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
encryptionKey,
ciphertext
);
return JSON.parse(new TextDecoder().decode(plaintext));
}
AES-GCM with a fresh IV per write is the right choice here. The authentication tag catches tampering, and the per-write IV ensures that two snapshots of the same session produce different ciphertexts even when content hasn't changed.
Key Derivation: Where the Session Key Comes From
The trickiest part of this pattern is key management. You have a few options, each with different tradeoffs.
User-derived keys work well for interactive agents. Derive the session key from the user's password or passkey using PBKDF2 or Argon2, then keep it only in memory for the duration of the session. When the user logs out, the key is gone. The downside: agents that run unattended (background jobs, scheduled tasks) cannot use this approach because there's no user present to authenticate.
KMS-managed keys are the standard choice for server-side agents. Each session gets a unique data key generated by your KMS (AWS KMS, Google Cloud KMS, HashiCorp Vault, or a zero-knowledge service). Store only the encrypted data key alongside the session blob. When you need to resume, fetch the data key via the KMS API, decrypt it with your root key, then use it to decrypt the session. This keeps plaintext keys out of storage while allowing automated resumption.
Envelope encryption combines the two: generate a random session key, encrypt it with a KMS key, and store the wrapped key with the ciphertext. This is the pattern you want for production:
async function createSession(userId: string): Promise<SessionHandle> {
// KMS generates a data key and returns both versions
const { plaintext, ciphertext } = await kms.generateDataKey({
keyId: "alias/agent-sessions",
keySpec: "AES_256",
});
const sessionKey = await crypto.subtle.importKey(
"raw",
plaintext,
{ name: "AES-GCM" },
false,
["encrypt", "decrypt"]
);
// Immediately overwrite the plaintext buffer
plaintext.fill(0);
return {
sessionKey, // in memory only
wrappedKey: ciphertext, // stored with session data
};
}
Checkpoint Granularity: Balancing Recovery vs. Write Amplification
How often you snapshot matters. Checkpoint too rarely and you lose significant work on crash. Checkpoint too often and you're doing expensive cryptographic operations and network writes on every small state change.
A practical approach is tiered checkpointing:
- In-memory delta log for frequent, cheap updates (per tool call)
- Encrypted incremental snapshot every N steps or on task completion
- Full encrypted snapshot at major workflow boundaries
The incremental snapshots only serialize what changed since the last full snapshot. When resuming, load the full snapshot then replay the delta log forward. This significantly reduces per-checkpoint write size while keeping recovery granularity fine.
For the delta log, you can use a simpler structure:
interface SessionDelta {
baseCheckpoint: number;
ops: Array<{ path: string; op: "set" | "delete"; value?: unknown }>;
}
Apply the ops to your in-memory session state on load using a JSON Patch-style approach.
Handling Concurrent Agents and Race Conditions
When the same logical session can run on multiple workers (for parallelized sub-tasks, or because a crashed worker and its replacement both try to resume), you need optimistic concurrency control.
Store a version number with each snapshot. On write, use a conditional put that only succeeds if the stored version matches what you read:
await storage.putIfVersion(
`sessions/${sessionId}`,
encryptedPayload,
expectedVersion
);
If the conditional put fails, you have a conflict. Depending on your agent's task graph semantics, you can either abort (the other worker wins), merge (if your state is a CRDT or otherwise commutative), or escalate.
The versioning also provides a tamper-detection signal: if the version advances beyond what you expect, something else modified the session, and you should treat the session as potentially compromised.
Resumption Protocol: Getting Back to Work
When an agent restarts and needs to resume:
- Fetch the encrypted session blob and wrapped key from storage
- Call KMS to decrypt the wrapped key (this is your authorization check — KMS policy determines who can resume)
- Decrypt the session snapshot with the plaintext session key
- Validate the snapshot's integrity (timestamp recency, schema version, task graph consistency)
- Re-initialize tool connections (API clients, database handles) — don't serialize these
- Advance the task graph to the first incomplete node
The KMS call in step 2 is the right place for access control. You can attach IAM conditions like aws:SourceIp or aws:PrincipalTag to the KMS key policy to ensure only authorized environments can decrypt session data. An agent binary that escapes its sandbox cannot resume sessions if it can't reach KMS with valid credentials.
What Not to Put in Sessions
Even with encryption, there are things that don't belong in session snapshots:
Short-lived credentials — don't serialize OAuth tokens, API keys, or temporary STS credentials. On resume, re-acquire them. Storing credentials in snapshots creates a separate revocation problem: if a credential is revoked while the session is paused, you'll resume with a dead credential you can't identify as expired without trying it.
Large binary payloads — file contents, images, and embeddings should be referenced by content-addressable hash, not inlined. Store the content separately (also encrypted), and keep only the reference in the session. This keeps snapshot size manageable and avoids re-encrypting large blobs on every checkpoint.
Raw model outputs containing PII — if your agent processes user data, consider whether intermediate reasoning traces need to persist at all. Sometimes it's better to re-run a cheap extraction step on resume than to persist its output.
Tying It Together
Encrypted session continuity lets you build AI agents that can run long, stateful workflows without forcing a choice between reliability and privacy. The storage layer sees only ciphertext; the key management layer enforces authorization; and the agent framework handles the mechanics of checkpointing and resumption.
The pattern scales from a single-user web app (user-derived keys, browser localStorage for the encrypted blob) to enterprise multi-tenant deployments (KMS per tenant, S3 or Firestore for blobs, versioned puts for concurrency control). The cryptographic primitive is the same in both cases — AES-GCM with a key that never touches storage.
If you're building on top of a zero-knowledge storage provider like BitAtlas, the blob storage layer handles the encryption for you, and you get auditable access logs without needing to build that infrastructure yourself. The session key management pattern above still applies, but you can lean on the storage SDK rather than implementing raw WebCrypto calls for every checkpoint.
The investment in this pattern pays off when something goes wrong — which, in production agentic systems, is a matter of when rather than if.