siyuan-note/siyuan · warning
Conf.language(239)
Error message
Conf.language(239)
What it means
RemoveBox refuses to run when the notebook is already locked by another remove/mount operation. boxLock.LoadOrStore(boxID, true) fails because the box ID is in the concurrent-operation map, so the localized i18n message (key 239, 'the operation is in progress, please wait') is returned. This is a concurrency guard against racing notebook removal and mounting.
Source
Thrown at kernel/model/mount.go:213
func collectBoxDeletedAttributeViewBlocks(boxID string) (ret map[string]map[string]struct{}, err error) {
rootIDs := treenode.GetRootBlockIDsByBoxID(boxID)
if 1 > len(rootIDs) {
return map[string]map[string]struct{}{}, nil
}
boundAVIDs, err := sql.QueryBoundBlockAVIDsInBox(nil, rootIDs, boxID)
if nil != err {
return nil, err
}
return groupDeletedAttributeViewBlocks(boundAVIDs), nil
}
func RemoveBox(boxID string) (err error) {
if !ast.IsNodeIDPattern(boxID) {
return errors.New("invalid notebook ID")
}
if _, loaded := boxLock.LoadOrStore(boxID, true); loaded {
err = errors.New(Conf.language(239))
return
}
defer boxLock.Delete(boxID)
if util.IsReservedFilename(boxID) {
return fmt.Errorf("can not remove [%s] caused by it is a reserved file", boxID)
}
FlushTxQueue()
sql.FlushQueue()
// 索引和笔记本目录删除后无法再读取 custom-avs,需提前收集;实际删除成功后再清理绑定行。
deletedAttrViewBlockIDs, err := collectBoxDeletedAttributeViewBlocks(boxID)
if nil != err {
return fmt.Errorf("query database-bound blocks in notebook [%s] failed: %w", boxID, err)
}
isUserGuide := IsUserGuide(boxID)
localPath := filepath.Join(util.DataDir, boxID)
if !filelock.IsExist(localPath) {View on GitHub (pinned to 8641553a1f)
Solutions
- Wait for the in-flight operation to finish, then retry the removal once
- Ensure only one remove/mount call per notebook is in flight; debounce UI triggers
- Check server logs for the overlapping operation that held the lock
Example fix
// before: parallel calls
fetchPost('/api/notebook/removeNotebook', {notebook: id});
fetchPost('/api/notebook/openNotebook', {notebook: id});
// after: sequential
await fetchPost('/api/notebook/removeNotebook', {notebook: id});
await fetchPost('/api/notebook/openNotebook', {notebook: id}); Defensive patterns
Strategy: retry
Validate before calling
// no public lock check; serialize calls yourself
let removing = false;
async function safeRemove(id){ if (removing) return; removing = true; try { await removeNotebook(id); } finally { removing = false; } } Try / catch
try { await removeNotebook(id); } catch (e) { if (isOperationInProgress(e)) await waitForIdleThenRetry(id); } Prevention
- Serialize all notebook mutations per notebook ID
- Debounce rapid UI delete/open actions
- Avoid concurrent scripts touching the same notebook
When it happens
Trigger: Calling /api/notebook/removeNotebook (RemoveBox) while the same box ID is locked by a concurrent Mount/mountBox, UnlockAndMountBox, or a second RemoveBox that has not yet released boxLock via defer boxLock.Delete.
Common situations: Double-clicking the notebook delete button firing two API calls; a UI auto-mount racing a manual removal; scripted API calls removing and opening the same notebook in quick succession.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Related operations are being processed, please try again lat
- 239
- Query notebook failed
- Create notebook [%s] folder [%s] failed: %s
- Move notebook [%s] file [%s] failed: %s
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/8be01e71d9122e92.
Report an issue: GitHub.