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

  1. Wait for the in-flight operation to finish, then retry the removal once
  2. Ensure only one remove/mount call per notebook is in flight; debounce UI triggers
  3. 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

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


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/8be01e71d9122e92. Report an issue: GitHub.