Defined in: packages/ai/src/activities/chat/middleware/run-store.ts:179
Durable store for run lifecycle records.
REQUIRED: createOrResume, update, get, findActiveRun. Every backend must implement all four — they are what the persistence middleware calls unconditionally. findActiveRun is required rather than feature-detected because a backend that has not implemented it is indistinguishable from one whose answer is legitimately null, so reconnect would silently do nothing instead of failing at build time. It was optional for exactly one release cycle and cost precisely that.
OPTIONAL: listByThread, listReclaimable. Each serves one higher-level feature (thread history, reclaim reaping) and callers feature-detect them, degrading gracefully when a backend omits them.
createOrResume: (input) => Promise<RunRecord>;Defined in: packages/ai/src/activities/chat/middleware/run-store.ts:188
Create a run record, or return the existing one unchanged if runId is already present.
INVARIANT (idempotency): an existing record is returned unchanged and the passed threadId/startedAt/status are ignored. This is what makes resuming a run safe. status defaults to 'running' on first creation.
Pick<RunRecord, "threadId" | "runId" | "startedAt"> & object
Promise<RunRecord>
findActiveRun: (threadId) => Promise<RunRecord | null>;Defined in: packages/ai/src/activities/chat/middleware/run-store.ts:255
The most recent 'running' run for threadId, or null if none is active.
REQUIRED. This resolves "does this thread have a live run to attach to?" from the STABLE thread id, which is the durable basis for reconnecting a client (a reload, or the same thread opened on another device) — independent of the ephemeral run id, which a single turn may mint several of. When more than one run is 'running', the one with the greatest startedAt wins.
A backend that stubs this to null turns reconnect off silently, because null is also the correct answer for an idle thread. A backend with no run lifecycle at all should omit the whole runs store instead — capability tiers belong at the store level, not the method level.
string
Promise<RunRecord | null>
get: (runId) => Promise<RunRecord | null>;Defined in: packages/ai/src/activities/chat/middleware/run-store.ts:216
Current record, or null when unknown.
string
Promise<RunRecord | null>
optional listByThread?: (threadId) => Promise<RunRecord[]>;Defined in: packages/ai/src/activities/chat/middleware/run-store.ts:221
Every run in a conversation, ascending by startedAt. OPTIONAL: only needed to render a thread's past agent activity. Consumers feature-detect.
string
Promise<RunRecord[]>
optional listReclaimable?: (opts) => Promise<RunRecord[]>;Defined in: packages/ai/src/activities/chat/middleware/run-store.ts:237
Runs that may be reclaimed: ALL THREE of status === 'running', detachedSince is set, and detachedSince <= now - ttlMs. The cutoff is inclusive — a run detached at exactly now - ttlMs IS reclaimable.
OPTIONAL: only needed by a reaper. Consumers feature-detect.
detachedSince is populated by withSandbox's detach path (see RunRecord.detachedSince). The sweep over the candidates this surfaces is @tanstack/ai-sandbox's reapDetachedRuns: it finalizes a run whose agent already finished, expires one past its TTL, and reclaims the sandbox. That is a function, not a scheduler — the application invokes it (cron, queue, alarm(), waitUntil) — and a backend that omits this method cannot be reaped at all.
number
number
Promise<RunRecord[]>
update: (runId, patch) => Promise<void>;Defined in: packages/ai/src/activities/chat/middleware/run-store.ts:199
Patch a record's mutable fields.
INVARIANT: updating an unknown runId is a no-op — it must not throw and must not create a record.
string
Partial<Pick<RunRecord, | "status" | "finishedAt" | "error" | "usage" | "sandboxKey" | "detachedSince" | "cancelRequested" | "driverEpoch">>
Promise<void>