agalwood/Motrix · error · AppError
PluginManifestInvalid
PluginManifestInvalid
Error message
plugin.update.builtin_staging_not_found
What it means
Thrown by BuiltinUpdater.commit(stagingId) when the given stagingId is not present in the in-memory pending Map. pending is populated only by stage() and cleared by commit()/cancel(); it is not persisted. Used to refuse committing an unknown, already-committed, already-cancelled, or post-restart staging id.
Source
Thrown at src/core/plugin/update/builtin-updater.ts:142
pluginId: entry.id,
stagingDir,
})
return {
stagingId,
trustChanged: diff.changed,
added: diff.added,
newVersion: parsed.version,
}
} catch (e) {
await rm(stagingDir, { recursive: true, force: true })
throw e
}
}
async commit(stagingId: string): Promise<{ pluginId: string }> {
const staged = this.pending.get(stagingId)
if (!staged) {
throw new AppError(
ErrorCode.PluginManifestInvalid,
'plugin.update.builtin_staging_not_found'
)
}
const finalDir = path.join(this.opts.overlayDir, staged.pluginId)
const now = this.opts.now ?? Date.now
const backup = path.join(
this.opts.overlayDir,
`.bak-${staged.pluginId}-${now()}`
)
let hadPrevious = true
try {
await rename(finalDir, backup)
} catch {
hadPrevious = false
}
try {
await rename(staged.stagingDir, finalDir)View on GitHub (pinned to 1a708ee577)
Solutions
- Treat commit() as ephemeral: re-stage() after any process restart before committing.
- Track committed/cancelled stagingIds client-side and avoid double-commit.
- If the staging dir still exists on disk, do not call commit() — re-stage from scratch since pending state is gone.
Example fix
// before
// const { stagingId } = await updater.stage(entry, effective)
// // ... process restarts, pending Map is empty ...
// await updater.commit(stagingId) // throws builtin_staging_not_found
// after
// // pending does not survive restart; re-stage if needed, or commit before restart
// const result = await updater.stage(entry, effective)
// await updater.commit(result.stagingId) // same process lifetime Defensive patterns
Strategy: validation
Validate before calling
function isStaged(updater: BuiltinUpdater, stagingId: string): boolean {
// pending is private; track stagingIds you received from stage() in your own set
return receivedStagingIds.has(stagingId) && !consumedStagingIds.has(stagingId);
} Try / catch
try {
await updater.commit(stagingId);
} catch (e) {
if (e instanceof AppError && e.message === 'plugin.update.builtin_staging_not_found') {
// staging was already committed/cancelled or process restarted; re-stage if update is still wanted
} else throw e;
} Prevention
- Keep stage() and commit() within the same process lifetime — the pending Map is not persisted.
- Track stagingIds in your own state and mark them consumed after commit/cancel to avoid double-commit.
- If the app may restart between stage and commit, redesign to stage-and-commit in one step.
When it happens
Trigger: Calling commit(stagingId) where: stagingId was never returned by stage(); commit() or cancel() already removed it; or the process restarted (the pending Map is per-instance memory).
Common situations: Two-phase UI that calls stage() in one process and commit() in another (e.g. renderer requests commit after an Electron main restart); retry logic calling commit() twice for the same staging id; calling commit() after the user already hit cancel(); a stale stagingId read from a log/old state.
Related errors
- plugin.lifecycle.secrets_seed_missing
- plugin.lifecycle.deactivate_timeout
- PluginActivationCapExceeded
- plugin.fs.task.not_available_outside_hook
- plugin.metadata.not_available_outside_hook
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/db1695c9b99aaade.
Report an issue: GitHub.