moeru-ai/airi · error

[chat-session] persist task failed:

Error message

[chat-session] persist task failed:

What it means

session-store serializes persistence through a promise chain (enqueuePersist). Each task's rejection is logged here and swallowed so the chain survives for later writes. Failures originate in the IndexedDB layer behind the persist tasks: quota exceeded, database closed/version change mid-write, corruption, or an abort during tab shutdown. Because tasks return the original promise to awaiting callers, callers may also observe the rejection; this log is the queue-level record.

Source

Thrown at packages/stage-ui/src/stores/chat/session-store.ts:167

    return cloudMapper
  }

  /**
   * Append a write task to the persist queue. Tasks always run sequentially
   * regardless of whether prior tasks rejected — but rejections propagate to
   * the awaiting caller AND are surfaced via console for debugging. The
   * previous `then(task, task)` form silently swallowed prior rejections by
   * running the next task as the rejection handler, which masked IDB
   * failures from the cloud-sync cursor tracking that depends on them.
   */
  function enqueuePersist<T>(task: () => Promise<T>): Promise<T> {
    const next = persistQueue.then(task)
    // Keep the queue alive after a rejection but log it so silent IDB
    // failures (quota, corruption) surface during dev.
    persistQueue = next.then(
      () => undefined,
      (err) => {
        console.warn('[chat-session] persist task failed:', errorMessageFrom(err))
      },
    )
    return next
  }

  function snapshotMessages(messages: ChatHistoryItem[]) {
    return cloneDeep(messages)
  }

  function ensureSessionMessageIds(sessionId: string) {
    const current = sessionMessages.value[sessionId] ?? []
    let changed = false
    const next = current.map((message) => {
      if (message.id)
        return message
      changed = true
      return {
        ...message,

View on GitHub (pinned to f679616c34)

Solutions

  1. Check the logged message for QuotaExceededError — prune or export old chat history, and consider trimming snapshot payloads.
  2. Handle versionchange/close events in the IDB wrapper so writes are not attempted on a dead connection.
  3. Ensure message objects are structured-clone-safe (no functions/DOM refs) before persisting.
  4. If in private/restricted mode, warn users that chat persistence is unavailable.
Defensive patterns

Strategy: try-catch

Validate before calling

function isStructuredCloneSafe(value: unknown): boolean {
  try {
    structuredClone(value)
    return true
  }
  catch {
    return false
  }
}
// validate message payloads before enqueueing persist tasks

Type guard

function isQuotaError(err: unknown): boolean {
  const name = (err as { name?: string })?.name
  return name === 'QuotaExceededError' || errorMessageFrom(err).includes('quota')
}

Try / catch

function enqueuePersist<T>(task: () => Promise<T>): Promise<T> {
  const next = persistQueue.then(task)
  persistQueue = next.then(
    () => undefined,
    (err) => {
      if (isQuotaError(err)) {
        // prune history / notify user instead of silently retrying forever
      }
      console.warn('[chat-session] persist task failed:', errorMessageFrom(err))
    },
  )
  return next
}

Prevention

When it happens

Trigger: Any IDB write inside a persist task rejects — large chat histories hitting storage quota, IDB version upgrade closing connections, private-mode/restricted storage (ITP evicting or blocking), or structured-clone failure on non-cloneable message payloads.

Common situations: Long-lived chats accumulating megabytes of history in Safari/WebKit quota; browser data-clearing while the app is open; Firefox private browsing where IDB writes fail; storing non-cloneable values (functions, DOM nodes) accidentally attached to messages.

Related errors


AI-assisted analysis of moeru-ai/airi@f679616c34 (2026-08-18). Data as JSON: /api/errors/ca732e9db2ab2b2f. Report an issue: GitHub.