agalwood/Motrix · warning · StorageError

plugin.storage.cas_mismatch

plugin.storage.cas_mismatch

Error message

plugin.storage.cas_mismatch: key already exists (expectedVersion=0)

What it means

Thrown by compareAndSet() when expectedVersion === 0 (the 'insert only if absent' intent) but a row for (pluginId, key) already exists. The implementation uses INSERT ... ON CONFLICT DO NOTHING RETURNING version; if it returns no row, the key was present, so the optimistic-insert precondition failed. No version is returned.

Source

Thrown at src/core/plugin/capabilities/storage.ts:240

    const now = Date.now()

    if (expectedVersion === 0) {
      // Insert only if absent
      const row = this.db
        .prepare<
          [string, string, string, number, number],
          { version: number } | undefined
        >(
          `INSERT INTO plugin_storage (plugin_id, key, value, size, version, updated_at)
           VALUES (?, ?, ?, ?, 1, ?)
           ON CONFLICT(plugin_id, key) DO NOTHING
           RETURNING version`
        )
        .get(pluginId, key, json, size, now)

      if (!row) {
        throw new StorageError(
          'plugin.storage.cas_mismatch',
          'plugin.storage.cas_mismatch: key already exists (expectedVersion=0)'
        )
      }
      return { version: row.version }
    }

    // Update only when stored version matches expectedVersion
    const row = this.db
      .prepare<
        [string, number, number, string, string, number],
        { version: number } | undefined
      >(
        `UPDATE plugin_storage
         SET value = ?, size = ?, version = version + 1, updated_at = ?
         WHERE plugin_id = ? AND key = ? AND version = ?
         RETURNING version`
      )

View on GitHub (pinned to 1a708ee577)

Solutions

  1. Treat this as a normal CAS conflict, not a crash: read the current version with get() and switch to an update with the real expectedVersion.
  2. If you only need idempotent initialization, use set() (unconditional upsert) instead of compareAndSet(...,0,...).
  3. For create-if-absent semantics with a follow-up, catch the StorageError, inspect .code === 'plugin.storage.cas_mismatch', and fall through to a read-then-update flow.
  4. Guard concurrency at a higher level (a lock/mutex around init) so only one writer attempts the version-0 insert.

Example fix

// before
await host.compareAndSet(pluginId, 'cfg', 0, defaults)

// after
try {
  await host.compareAndSet(pluginId, 'cfg', 0, defaults)
} catch (e) {
  if (e.code === 'plugin.storage.cas_mismatch') {
    const { version } = await host.get(pluginId, 'cfg')
    await host.compareAndSet(pluginId, 'cfg', version, merged)
  } else throw e
}
Defensive patterns

Strategy: try-catch

Validate before calling

const { version } = await host.get(pluginId, key)
if (version !== 0) { /* key exists; use update path, not insert */ }

Try / catch

try { await host.compareAndSet(pluginId, key, 0, value) }
catch (e) { if (e.code === 'plugin.storage.cas_mismatch') { const { version } = await host.get(pluginId, key); await host.compareAndSet(pluginId, key, version, merged) } else throw e }

Prevention

When it happens

Trigger: Calling host.compareAndSet(pluginId, key, 0, value) when any prior set/compareAndSet already created the row (version >= 1). The intended-create path is the only one that uses expectedVersion=0.

Common situations: A plugin uses compareAndSet(id, 0, ...) to initialize a value and races with another activation that already created it; a developer calls set() to seed a default and then compareAndSet(...,0,...) expecting to own the insert; retry logic re-runs an init step that already succeeded.

Related errors


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/34aa45a92baed62c. Report an issue: GitHub.