agalwood/Motrix · critical · AggregateError

plugin locale commit and rollback both failed

Error message

plugin locale commit and rollback both failed

What it means

Thrown by PluginRegistry.commitHostLanguage: the in-memory locale switch was already applied (currentLang and per-plugin manifests/dictionaries updated, registryRevision bumped), the caller's onCommit() threw, the registry then attempted onRollback(previousLanguage), and the rollback itself also threw. Because both halves failed, the registry wraps the original error and the rollback error in an AggregateError so neither fault is hidden.

Source

Thrown at src/core/plugin/plugin-registry.ts:373

    try {
      onCommit()
    } catch (error) {
      this.currentLang = previousLanguage
      for (const {
        pluginId,
        indexed,
        manifest,
        dictionaries,
      } of previousEntries) {
        indexed.manifest = manifest
        if (dictionaries) this.localeDictionaries.set(pluginId, dictionaries)
        else this.localeDictionaries.delete(pluginId)
      }
      this.registryRevision += 1
      try {
        await onRollback(previousLanguage)
      } catch (rollbackError) {
        throw new AggregateError(
          [error, rollbackError],
          'plugin locale commit and rollback both failed'
        )
      }
      throw error
    }
    return true
  }

  /** Atomically switch registry-only consumers such as manifest list tests. */
  async setHostLanguage(language: SupportedLocale): Promise<void> {
    await this.setHostLanguageTransaction(language, {
      commitHostLocale: () => {},
      rollbackHostLocale: () => {},
    })
  }

  /** Prepare asynchronously, then atomically commit registry + host locale. */

View on GitHub (pinned to 1a708ee577)

Solutions

  1. Inspect AggregateError.errors[0] (original commit error) and AggregateError.errors[1] (rollback error) to identify the shared root cause.
  2. Fix the underlying resource both callbacks depend on (free disk, restore the IPC channel, fix permissions on userDataDir).
  3. Make onRollback defensive so it cannot throw for the same reason onCommit did (e.g. rollback writes to a fallback location).
  4. After recovery, reconcile registry state: call setHostLanguage with the desired locale again, since the registry may be left at an inconsistent revision.

Example fix

// before — onCommit and onRollback both throw on disk write
commitHostLocale: () => fs.writeFileSync(indexPath, buf),
rollbackHostLocale: () => fs.writeFileSync(indexPath, prevBuf)

// after — rollback tolerates write failure and logs instead of throwing
rollbackHostLocale: () => { try { fs.writeFileSync(indexPath, prevBuf) } catch (e) { log.error(e) } }
Defensive patterns

Strategy: try-catch

Type guard

function isAggregateError(e: unknown): e is AggregateError {
  return e instanceof Error && Array.isArray((e as AggregateError).errors)
}

Try / catch

try {
  await registry.commitHostLanguage(prepared, onCommit, onRollback)
} catch (e) {
  if (e instanceof AggregateError) {
    const [commitErr, rollbackErr] = e.errors
    log.error('locale commit AND rollback failed', { commitErr, rollbackErr })
    // registry may be inconsistent at registryRevision; reconcile by re-applying the target locale
    await registry.setHostLanguage(targetLanguage).catch(() => null)
  } else throw e
}

Prevention

When it happens

Trigger: commitHostLanguage() is called; onCommit() rejects/throws; the onRollback callback supplied by the caller also rejects. Reproduces whenever a host locale switch's commit hook and rollback hook both depend on the same broken resource (e.g. both write to an unwritable disk).

Common situations: Disk full or userDataDir read-only so both commit persistence and rollback persistence fail. A host-side IPC channel died, so both the commit and rollback IPC calls reject. Concurrent locale switches racing the same callbacks.

Related errors


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