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
- 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.
- If you only need idempotent initialization, use set() (unconditional upsert) instead of compareAndSet(...,0,...).
- 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.
- 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
- Use compareAndSet(...,0,...) only for create-if-absent; use set() for idempotent init.
- Wrap the insert in a try/catch that falls through to read-then-update.
- Serialize plugin initialization to avoid racing inserts.
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
- plugin.storage.quota_exceeded
- aria2 returned gid ${actualGid} instead of reserved gid ${re
- plugin.commands.id_out_of_namespace
- plugin.command.not_declared_in_manifest
- plugin.commands.not_found
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/34aa45a92baed62c.
Report an issue: GitHub.