PAIPAI

Storage

Durable thread state provider.

Storage is required for production.

import { createProductionThreadStore } from "./thread-storage";

export const storage = createProductionThreadStore({
  url: process.env.DATABASE_URL!,
});

Use it with createAgentRuntime():

export const runtime = createAgentRuntime({
  agent,
  storage,
  scopeKey: (identity) => identity.workspaceId,
});

What Storage Owns

Storage persists:

  • thread records, status, ThreadVersion, and metadata;
  • discriminated run/admin leases on thread records;
  • run lifecycle records for accepted work;
  • transcript messages;
  • queued items;
  • tool message parts on the open tail assistant message;
  • immutable incarnation ids and monotonic per-incarnation revisions.

Scope Constraint

Production storage must load and mutate threads by scope plus thread id.

readThread({ scopeKey, threadId });

Never rely on globally loading by plain threadId.

See Persisted Data Model for the logical record shapes and storage invariants expected from ThreadStore.

Runtime Facades

Application and lifecycle code should not receive ThreadStore directly. Raw storage is a provider-level API: it can read and mutate any scoped record the provider exposes, so it is too broad for ordinary agent behavior.

The runtime exposes narrower server-side facades instead:

  • lifecycle hooks receive a current-thread thread facade;
  • backend tools may receive scoped runtime helpers;
  • runtime.admin() exposes trusted server/admin operations;
  • browser clients receive the smaller remote Thread API.

The facade still writes through the storage provider, but it preserves runtime invariants:

  • current thread scope;
  • active run ownership or explicit runtime lock;
  • complete-version compare-and-swap updates;
  • realtime/watch invalidation;
  • message visibility metadata;
  • provider portability.

For example, context compaction should use the lifecycle utility, which keeps the original transcript and message visibility authoritative:

const compaction = createContextCompaction({
  threshold: { maxEstimatedTokens: 24_000 },
  retain: { maxMessages: 8, maxEstimatedTokens: 12_000 },
  summarize: async ({ messages }) => summarize(messages),
});

Explicit product-driven visibility changes may use the scoped lifecycle or trusted runtime facade. Lifecycle code should not call raw storage directly.

On this page