siyuan-note/siyuan · error
%s
Error message
%s
What it means
This is the generic failure path of beginBlockToolScope: the internal fail helper releases the held notebook read lock and returns a formatted error with whatever message the surrounding block tool supplied. The message is dynamic ("%s"), so the actual text comes from the specific validation that called fail() — typically a per-block-ID check (e.g. block not found) inside the scope setup loop.
Source
Thrown at kernel/mcp/tools/block.go:651
release = func() {}
notebook, _ := args["notebook"].(string)
notebook = strings.TrimSpace(notebook)
encrypted := notebook != "" && model.IsEncryptedBox(notebook)
if encrypted {
model.HoldBoxReadLock(notebook)
if !model.IsBoxUnlocked(notebook) {
model.ReleaseBoxReadLock(notebook)
return "", release, fmt.Errorf("encrypted notebook is locked, please unlock it first")
}
release = func() {
model.ReleaseBoxReadLock(notebook)
}
boxID = notebook
}
fail := func(format string, values ...any) (string, func(), error) {
release()
return "", func() {}, fmt.Errorf(format, values...)
}
for _, id := range ids {
if id == "" {
continue
}
queryBoxID := ""
if encrypted {
queryBoxID = notebook
}
bt := treenode.GetBlockTreeInExactBox(id, queryBoxID)
if bt == nil || (notebook != "" && bt.BoxID != notebook) {
if notebook == "" {
return fail("block %s was not found in a normal notebook; provide notebook for encrypted targets", id)
}
return fail("block %s does not belong to notebook %s", id, notebook)
}
if mutation && encrypted {
if treenode.GetBlockTreeInExactBox(id, "") != nil {View on GitHub (pinned to 8641553a1f)
Solutions
- Read the concrete error text returned after the %s substitution and address the underlying cause (usually a missing/invalid block ID).
- Verify the block ID exists (e.g. via SQL or blockGet on a known-good ID) before calling the tool.
- Refresh any cached IDs from your client; blocks may have been removed or their documents changed.
- Trim/normalize IDs on the caller side to avoid lookup failures.
Defensive patterns
Strategy: try-catch
Validate before calling
const known = await queryBlockIDs(); // e.g. via SQL tool
for (const id of ids) {
if (!/^[0-9]{14}-[a-z0-9]{7}$/.test(id || '')) throw new Error('bad id: ' + id);
if (!known.has(id)) throw new Error('unknown block id: ' + id);
} Type guard
function isBlockID(v) { return typeof v === 'string' && /^[0-9]{14}-[a-z0-9]{7}$/.test(v); } Try / catch
try {
await mcp.call("blockInsert", { ... });
} catch (e) {
// message is dynamic: log it fully, then validate/re-fetch the offending ID
console.error(e.message); // e.g. "block not found: 2024...-xxx"
} Prevention
- Always pass trimmed, well-formed SiYuan block IDs
- Re-fetch block IDs after any document mutation; never cache across sessions
- Log the full dynamic message — it names the concrete underlying problem
When it happens
Trigger: Any of the block tools using beginBlockScope fail() during setup: an empty/invalid id resolution, a block ID that cannot be located, or other per-ID validation inside the ids loop of the scope builder.
Common situations: MCP callers passing block IDs that do not exist (deleted or from another notebook); IDs with surrounding whitespace that fail lookup; stale IDs cached by an agent after document changes.
Understand the failure class
Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.
Related errors
- unsupported server type: %s
- name is empty
- invalid name %q
- duplicate inherited variable %q
- variable %q contains NUL
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/19424a69ac8b1dd8.
Report an issue: GitHub.