Summary
The editor reliably SIGABRTs in ScriptServer::remove_global_class_by_path() when many .gd files are created in rapid succession and each creation triggers EditorFileSystem::scan(). Overlapping scans re-queue the same background tasks (update_scripts_classes, update_script_paths_documentation) before the previous pass finishes, the global-class registry is left in an inconsistent state, and the next idle filesystem-scan notification aborts.
This was surfaced by a concurrency stress harness (stormtest) driving the godot-ai MCP plugin: many parallel clients call script_create (each does FileAccess write + get_resource_filesystem().scan()). It reproduces with no plugin reload involved — pure rapid scan() churn is sufficient.
Filed on my fork as a record; not yet reported upstream.
Environment
- Godot 4.6.3.stable (mono), macOS
- Reproduced twice, identical stack. Local crash reports:
~/Library/Logs/DiagnosticReports/Godot-2026-06-01-102559.ips
~/Library/Logs/DiagnosticReports/Godot-2026-05-31-195549.ips
Crash backtrace
SIGABRT (Abort trap: 6) — main thread
abort
ScriptServer::remove_global_class_by_path(String const&)
EditorFileSystem::_register_global_class_script(String const&, String const&, EditorFileSystem::ScriptClassInfoUpdate const&)
EditorFileSystem::_update_script_classes()
EditorFileSystem::_update_scan_actions()
EditorFileSystem::_notification(int)
Object::_notification_forward(int)
SceneTree::_process_group(SceneTree::ProcessGroup*, bool)
Pre-crash editor errors (the tell)
ERROR: Task 'update_scripts_classes' already exists.
ERROR: Task 'update_script_paths_documentation' already exists.
ERROR: Condition "!tasks.has(p_task)" is true.
[2] ScriptServer::remove_global_class_by_path(String const&) (in Godot) + 44
Also observed during the same window: UndoRedo history mismatch: expected 0, got 2.
Repro (high level)
- Run an editor with a tool that issues many
EditorFileSystem::scan() calls in quick succession — e.g. the godot-ai plugin under the stormtest harness: 8 concurrent MCP clients, each rapidly calling script_create (writes a .gd then scan()).
- Within ~150–200 rapid file writes the editor aborts with the stack above. No plugin reload, no play-mode — just the idle filesystem scan re-entering while scan tasks are still queued.
Reproduces on both main and a feature branch of godot-ai, so it is not specific to any plugin change — it tracks the rate of scan() calls, not plugin logic.
Suspected cause
EditorFileSystem::scan() (and/or the WorkerThreadPool tasks it enqueues — update_scripts_classes, update_script_paths_documentation) is not safe to call again while a previous scan's actions are still pending. The duplicate-task guard fires (Task ... already exists / !tasks.has(p_task)), the scan-action / global-class update proceeds against an inconsistent registry, and _update_script_classes() → remove_global_class_by_path() aborts.
A defensive guard (skip/queue a scan() when is_scanning() is already true, or make the global-class update idempotent against duplicate task enqueues) would likely prevent the abort.
Workaround (consumer side)
For tools that create files, prefer EditorFileSystem::update_file(path) (single-file registration) over a full scan() per write — it avoids the recursive scan-action batch entirely. (This is the mitigation being applied in the godot-ai plugin, whose script_create was the only hot path still calling full scan() per write.)
Summary
The editor reliably
SIGABRTs inScriptServer::remove_global_class_by_path()when many.gdfiles are created in rapid succession and each creation triggersEditorFileSystem::scan(). Overlapping scans re-queue the same background tasks (update_scripts_classes,update_script_paths_documentation) before the previous pass finishes, the global-class registry is left in an inconsistent state, and the next idle filesystem-scan notification aborts.This was surfaced by a concurrency stress harness (
stormtest) driving the godot-ai MCP plugin: many parallel clients callscript_create(each doesFileAccesswrite +get_resource_filesystem().scan()). It reproduces with no plugin reload involved — pure rapidscan()churn is sufficient.Filed on my fork as a record; not yet reported upstream.
Environment
~/Library/Logs/DiagnosticReports/Godot-2026-06-01-102559.ips~/Library/Logs/DiagnosticReports/Godot-2026-05-31-195549.ipsCrash backtrace
Pre-crash editor errors (the tell)
Also observed during the same window:
UndoRedo history mismatch: expected 0, got 2.Repro (high level)
EditorFileSystem::scan()calls in quick succession — e.g. the godot-ai plugin under thestormtestharness: 8 concurrent MCP clients, each rapidly callingscript_create(writes a.gdthenscan()).Reproduces on both
mainand a feature branch of godot-ai, so it is not specific to any plugin change — it tracks the rate ofscan()calls, not plugin logic.Suspected cause
EditorFileSystem::scan()(and/or the WorkerThreadPool tasks it enqueues —update_scripts_classes,update_script_paths_documentation) is not safe to call again while a previous scan's actions are still pending. The duplicate-task guard fires (Task ... already exists/!tasks.has(p_task)), the scan-action / global-class update proceeds against an inconsistent registry, and_update_script_classes()→remove_global_class_by_path()aborts.A defensive guard (skip/queue a
scan()whenis_scanning()is already true, or make the global-class update idempotent against duplicate task enqueues) would likely prevent the abort.Workaround (consumer side)
For tools that create files, prefer
EditorFileSystem::update_file(path)(single-file registration) over a fullscan()per write — it avoids the recursive scan-action batch entirely. (This is the mitigation being applied in the godot-ai plugin, whosescript_createwas the only hot path still calling fullscan()per write.)