paperclipai/paperclip · error

Provider drain state is malformed.

Error message

Provider drain state is malformed.

What it means

providerDrainStateFromSnapshot converts a persisted provider drain snapshot into typed state and first validates its shape. It rejects snapshots whose pendingEvents/queuedEvents are not arrays, whose other fields are non-string-empty/null in unexpected ways, or whose ambiguousTurnStartPending is defined but not boolean. The error signals that the durable drain state cannot be trusted for event draining decisions.

Source

Thrown at packages/paperclip-runner/src/live/runnerd-codex-transport.ts:530

function providerDrainStateFromSnapshot(state: Record<string, unknown>): {
  pendingEventCount: number;
  activeProviderTurnId: string | null;
  providerSettled: boolean;
} {
  if (
    !Array.isArray(state.pendingEvents) ||
    (state.queuedEvents !== undefined && !Array.isArray(state.queuedEvents)) ||
    [state.activeProviderTurnId, state.activeTurnId].some(
      (value) =>
        value !== undefined &&
        value !== null &&
        (typeof value !== "string" || value.length === 0),
    ) ||
    (state.ambiguousTurnStartPending !== undefined &&
      typeof state.ambiguousTurnStartPending !== "boolean")
  )
    throw new Error("Provider drain state is malformed.");
  const pending = Array.isArray(state.pendingEvents)
    ? state.pendingEvents.length
    : 0;
  const queued = Array.isArray(state.queuedEvents)
    ? state.queuedEvents.length
    : 0;
  const activeProviderTurnId =
    [state.activeProviderTurnId, state.activeTurnId].find(
      (value): value is string => typeof value === "string" && value.length > 0,
    ) ?? null;
  return {
    pendingEventCount: pending + queued,
    activeProviderTurnId,
    providerSettled:
      activeProviderTurnId === null && state.ambiguousTurnStartPending !== true,
  };
}

View on GitHub (pinned to 01ad858492)

Solutions

  1. Delete or regenerate the malformed drain-state snapshot so the runner rebuilds it from a clean state
  2. Check for runner version upgrades and migrate old snapshots to the current schema
  3. Inspect the state file for truncation/corruption and restore from backup if available
  4. Add validation at write time (assert shape before persisting) to prevent future malformed snapshots

Example fix

// before
const state = providerDrainStateFromSnapshot(rawSnapshot); // throws on malformed shape
// after
if (!Array.isArray(rawSnapshot.pendingEvents) || !Array.isArray(rawSnapshot.queuedEvents)) {
  rawSnapshot = { pendingEvents: [], queuedEvents: [] }; // safe default
}
const state = providerDrainStateFromSnapshot(rawSnapshot);
Defensive patterns

Strategy: type-guard

Validate before calling

const isWellFormedDrainSnapshot = (s: unknown): boolean =>
  typeof s === "object" && s !== null &&
  (s.pendingEvents === undefined || Array.isArray(s.pendingEvents)) &&
  (s.queuedEvents === undefined || Array.isArray(s.queuedEvents)) &&
  (s.ambiguousTurnStartPending === undefined || typeof s.ambiguousTurnStartPending === "boolean");

Type guard

function isDrainSnapshot(v: unknown): v is ProviderDrainSnapshot {
  return typeof v === "object" && v !== null &&
    (v.pendingEvents === undefined || Array.isArray(v.pendingEvents)) &&
    (v.queuedEvents === undefined || Array.isArray(v.queuedEvents)) &&
    (v.ambiguousTurnStartPending === undefined || typeof v.ambiguousTurnStartPending === "boolean");
}

Try / catch

let drainState: ProviderDrainState;
try {
  drainState = providerDrainStateFromSnapshot(raw);
} catch (err) {
  if ((err as Error).message === "Provider drain state is malformed.") {
    drainState = providerDrainStateFromSnapshot({ pendingEvents: [], queuedEvents: [] });
  } else throw err;
}

Prevention

When it happens

Trigger: Calling providerDrainStateFromSnapshot (via provider or #providerDrainState) with a snapshot where: pendingEvents or queuedEvents is present but not an array, fields have empty-string/null/invalid values, or ambiguousTurnStartPending is set to a non-boolean value.

Common situations: Hand-edited or corrupted state files in the runner root; schema drift after upgrading the runner so old snapshots no longer match the expected shape; partial writes from a crash leaving truncated JSON-derived state; snapshots produced by a different runner version.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/8cb16a870b94d067. Report an issue: GitHub.