Skip to main content

External Session Storage

By default, qodercli keeps session history on the machine where it runs. In a multi-instance, container, or serverless deployment, the next request may reach a different machine that cannot read the earlier session. External session storage keeps a mirror of each session in storage that your application controls. The mirror is an additional copy: qodercli still writes the session locally, and any host can continue a session later by using its session ID. Use external storage when:
  • requests can move between service instances
  • local disks are temporary or unreliable
  • your application must control access, encryption, backups, or retention
For an application that always runs on one machine, local session storage is usually enough.

How it works

first request
  query()
    -> qodercli writes the session locally
    -> the SDK mirrors new session entries to your store

later request, on any host
  query({ resume: sessionId })
    -> the SDK loads session history from your store
    -> qodercli continues the session
Two properties follow from this design:
  • Writes are best-effort. The SDK mirrors entries in the background. A failed write is reported but never stops the running conversation; see Operations.
  • Entries are opaque. Your store persists and returns SDK-defined transcript entries as-is. Your application does not need to understand qodercli's local file format.

Quick start

InMemorySessionStore keeps data in the current process only. Use it to validate the wiring — that queries save to and resume from a store — before you connect a shared backend. It cannot demonstrate cross-host resume, because the data is gone when the process exits; for that, implement a real store. See Implementing a store.
import {
  InMemorySessionStore,
  qodercliAuth,
  query,
} from '@qoder-ai/qoder-agent-sdk';

const store = new InMemorySessionStore();
const options = {
  auth: qodercliAuth(),
  cwd: '/path/to/project',
  sessionStore: store,
};

// First query: run and capture the session ID.
let sessionId: string | undefined;
for await (const message of query({
  prompt: 'Remember the number 42.',
  options,
})) {
  if (message.type === 'result') sessionId = message.session_id;
}
if (!sessionId) throw new Error('The first query did not return a session ID.');

// Later query: resume from the store and the model recalls prior context.
for await (const message of query({
  prompt: 'What number did I ask you to remember?',
  options: { ...options, resume: sessionId },
})) {
  if (message.type === 'result') console.log(message.result); // -> "42"
}
Every host must use the same cwd, so the SDK identifies requests as belonging to the same project.

Using a store in your application

Pass the store in options.sessionStore for every query that should mirror or restore a session, then choose how to select the session:
  • Known session ID — pass resume: sessionId.
  • Most recent session for the project — pass continue: true. This requires the store to implement listSessions.

Managing stored sessions

The session-management functions accept the same sessionStore option and operate on the store instead of local files:
import {
  listSessions,
  getSessionInfo,
  getSessionMessages,
  listSubagents,
  getSubagentMessages,
  renameSession,
  tagSession,
  forkSession,
  deleteSession,
} from '@qoder-ai/qoder-agent-sdk';

const project = { dir: '/path/to/project', sessionStore: store };
const sessions = await listSessions(project);
const messages = await getSessionMessages(sessionId, project);
await renameSession(sessionId, 'Investigation #1', project);
await deleteSession(sessionId, project);
listSessions requires the store to implement listSessions; listSubagents requires listSubkeys; deleteSession requires delete. getSubagentMessages also uses listSubkeys when it is available. To copy an existing local session into the store — for example, when migrating a host that previously ran without external storage — use importSessionToStore:
import { importSessionToStore } from '@qoder-ai/qoder-agent-sdk';

await importSessionToStore(sessionId, store, { dir: '/path/to/project' });

Implementing a store

The SDK does not ship a production-ready store; your application implements the SessionStore interface against shared storage of its choice. For runnable Redis and PostgreSQL reference implementations that show the usage, see the TypeScript samples or Python samples. They are starting points, not production-ready packages.
type SessionKey = {
  projectKey: string; // derived from cwd; identifies the project
  sessionId: string; // the session UUID
  subpath?: string; // set for subagent transcripts, e.g. "subagents/agent-<id>"
};

type SessionStoreEntry = {
  type: string;
  uuid?: string;
  timestamp?: string;
  [key: string]: unknown; // opaque transcript line — store and return unchanged
};

interface SessionStore {
  // Required
  append(key: SessionKey, entries: SessionStoreEntry[]): Promise<void>;
  load(key: SessionKey): Promise<SessionStoreEntry[] | null>;
  // Optional — implement only the features you need
  listSessions?(
    projectKey: string,
  ): Promise<Array<{ sessionId: string; mtime: number }>>;
  delete?(key: SessionKey): Promise<void>;
  listSubkeys?(key: Omit<SessionKey, 'subpath'>): Promise<string[]>;
}
A SessionKey identifies one transcript. A main session has no subpath; each subagent transcript reuses the same projectKey and sessionId with a distinct subpath. Treat keys and entries as opaque — store and return them without interpreting the message content. The two required methods provide save and resume. Each optional method unlocks a feature:
MethodFeature enabled
append, loadMirror a session and resume it by ID — required
listSessionsResume the latest session with continue: true; list stored sessions
deleteDelete a session from the store
listSubkeysFully restore and inspect subagent transcripts

Implementation checklist

  • Preserve append order per key, and return the complete history from load. Replay depends on order.
  • Isolate keys. Never return one key's entries under another.
  • Make append idempotent. The SDK may retry a failed write with the same entries, so a retry must not duplicate history.
  • Return Unix epoch milliseconds in listSessions().mtime, and list main sessions only — those without a subpath.
  • Cascade deletes. Deleting a main session must also delete its subagent transcripts.
  • Return relative identifiers from listSubkeys — never absolute paths, and never a path containing . or ... These become storage keys, and traversal segments would let a transcript escape its namespace.
  • Serialize concurrent writers for the same session in the storage layer if more than one process can write it.
Run the SessionStore conformance tests in the SDK repository to check this common behavior, and add backend-specific concurrency and retry tests. Connection management, access control, encryption, backup, migrations, and retention remain your application's responsibility.

Operations

Failure handling. An external write failure does not stop the current conversation. After the final retry fails, the SDK emits a system/mirror_error message. Monitor it when mirror completeness matters — the conversation can succeed even when a write does not. Tuning.
  • External reads wait up to 60 seconds by default. Configure this with loadTimeoutMs.
  • sessionStoreFlush defaults to 'batched'. Set it to 'eager' to mirror entries sooner, at the cost of more storage requests.
Constraints.
  • If an explicitly resumed session is missing from the store, the SDK can still fall back to a local session with the same ID.
  • sessionStore cannot be combined with persistSession: false, file checkpointing, a custom transport provider, or the experimental Cloud Agent runtime.
  • The store is supported on the built-in Process and Worker transports.
  • A store holds session history only. It does not hold authentication state, application configuration, file checkpoints, or retention policy.
External Session Storage - Qoder