Skip to content

Fix #79: Fall back from SQLite WAL mode on 'disk I/O error' - #268

Open
zwanski-start wants to merge 2 commits into
AutoForgeAI:masterfrom
zwanski-start:swarm/issue-79-barbara
Open

zwanski-start wants to merge 2 commits into
AutoForgeAI:masterfrom
zwanski-start:swarm/issue-79-barbara

Conversation

@zwanski-start

@zwanski-start zwanski-start commented Oct 22, 2026 •

Copy link
Copy Markdown

Problem

The traceback in #79 isn't caused by GLM or MCP. It's SQLite failing with disk I/O error on the first schema read of features.db (PRAGMA main.table_info("features") inside create_all). The orchestrator then crashes, so the coding phase can't start. The initializer had already written the features, which is why the feature count showed up.

create_database() always turns on WAL unless the path is on NFS/CIFS/SMB/sshfs. WAL needs to mmap a shared-memory -shm file. Many filesystems don't support that, so they fail this way: WSL /mnt/c (9p/drvfs), VirtualBox/VMware/Parallels shares, Docker virtiofs, and most FUSE mounts. The old mount check also matched plain string prefixes (/mnt/c matched /mnt/code), ignored octal-escaped mount points, and returned on any match rather than the most specific mount.

Changes (api/database.py)

  • Better detection: _fs_type_for_path() matches mount points on path boundaries, picks the longest match and decodes \040 escapes. The list of unsafe filesystems now also covers 9p, drvfs, vboxsf, vmhgfs, virtiofs, smb3, ceph, glusterfs and similar, plus fuse.* (but not fuseblk). Projects on these filesystems use journal_mode=DELETE from the start.
  • Fallback at runtime: after setting the journal mode, _apply_journal_mode() reads sqlite_master, which forces the WAL index to open. If that raises disk I/O error or locking protocol, we log a warning, switch the file to DELETE mode and carry on. The switch opens the file in EXCLUSIVE locking mode, which keeps the WAL index in heap memory, so a database already stuck in WAL mode can still be opened. Any other error is re-raised as before.

Verification

  • New test_database_journal.py (30 tests): mount parsing (WSL drive, path boundary, longest match, escaped spaces), the filesystem lists, WAL on a local dir, the fallback path (simulated disk I/O error, then the DB ends up in DELETE mode and works), converting a real WAL database, and other errors not being swallowed.
  • ruff check . passes, mypy api/database.py is clean, python test_security.py passes, test_dependency_resolver.py passes.
  • No UI changes, so I didn't do a browser check.

Assumptions / notes

  • I couldn't reproduce the reporter's filesystem here, so the fallback is tested by injecting the SQLite error. The issue doesn't say what filesystem the project was on. The fix covers the known causes of this error in WAL mode either way.
  • DELETE mode gives less read/write concurrency than WAL. The existing 30s busy_timeout and BEGIN IMMEDIATE handling still apply, and this only affects filesystems where WAL doesn't work anyway.
  • If the error comes from real disk corruption or a full disk, DELETE mode will fail too, and the user still gets the original error.

Closes #79

🤖 Generated with Claude Code

QA round 1 fixes

  • Existing WAL DB on a detected-unsafe FS (WSL /mnt/c, 9p, vboxsf, ...): if the file header says WAL, it is converted to DELETE (EXCLUSIVE-locking trick) before being opened. The disk I/O error fallback now runs no matter which journal mode was chosen.
  • No more WAL switching on every process start: on filesystems that are not detected, WAL support is probed once per directory per process on a throwaway .wal-probe-<pid>.db, and features.db is set to WAL only if that probe works. A per-process file name keeps concurrent probes from colliding. If the fallback triggers anyway, the cached result is set to "unsupported".
  • New tests: WAL DB + _is_network_path=True + first DELETE apply fails ends up in DELETE mode and usable; with the probe failing, an SQL trace shows no PRAGMA journal_mode=WAL is ever issued against features.db.
  • Assumption: a failed probe for any reason (not only disk I/O error) means DELETE mode. This is the safe default.

WAL needs a shared-memory (-shm) mmap that WSL /mnt/* drives (9p/drvfs),
VM/container host shares and FUSE mounts often don't support, so opening
features.db failed with 'disk I/O error' and the orchestrator crashed.

- Detect more unsafe filesystems (9p, drvfs, vboxsf, virtiofs, smb3, fuse.*,
  ...) and match mount points on path boundaries, longest match wins,
  with /proc/mounts octal escapes decoded
- Probe the database after setting the journal mode; on disk I/O / locking
  protocol errors, convert the file to DELETE mode (via EXCLUSIVE locking so
  a stuck WAL database can still be opened) and continue
- Add tests for mount detection and the fallback path

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zwanski-start

Copy link
Copy Markdown
Author

🔍 QA report: ❌ Failed

Tester: Dennis (cubefarm agent, in a fresh session) · Round: 1 · Author: Barbara

In a single process the fallback works: with SQLite's -shm mmap made to fail (reproducing the issue's 'disk I/O error'), a fresh or existing WAL features.db is converted to DELETE mode and stays usable, and lint, types and tests are clean. Two real cases still fail. (1) A features.db already in WAL mode on a filesystem the PR now detects as unsafe (e.g. WSL /mnt/c 9p) still crashes with 'disk I/O error', because the fallback only runs when the chosen mode was WAL. Master leaves exactly that file behind after hitting #79. (2) On a filesystem that isn't detected, every new process (each agent's MCP server, the orchestrator, the UI server) switches the file back to WAL and then falls back. Under concurrency the new process failed with 'database is locked' in 14 of 25 attempts, and an already-running process got 'disk I/O error' on every query from that point on in 2 of 4 runs. That is the parallel-mode setup from the issue.

Check Details
✅ Issue repro reproduced on master LD_PRELOAD shim makes MAP_SHARED mmap of *-shm files fail with ENODEV (what 9p/drvfs/vboxsf do). On origin/master, create_database() on a fresh project fails with '(sqlite3.OperationalError) disk I/O error', the same as the issue's traceback.
✅ Fresh project on unsupported FS (single process) PR: warning logged, file ends up journal_mode=delete, Feature insert/query works, dispose + reopen works.
✅ Existing WAL DB on an FS that is not detected as unsafe (single process) PR converts it via the EXCLUSIVE-locking path to DELETE and the DB is usable. Master fails.
❌ Existing WAL DB on an FS the PR now detects as unsafe (e.g. WSL /mnt/c 9p) _is_network_path patched to True (simulating the new 9p detection), with a WAL-mode features.db and the shim active: create_database raises OperationalError 'disk I/O error'. journal_mode='DELETE' is chosen directly, PRAGMA journal_mode=DELETE on a WAL file needs the wal-index, and line 536 re-raises because journal_mode != 'WAL'. _force_rollback_journal is never called. Master leaves exactly such a file after crashing with #79, so upgrading users on /mnt/c still crash.
❌ Multiple processes on the same DB on an unsupported FS Each create_database() call in a new process runs PRAGMA journal_mode=WAL again before falling back. (a) Long-running process in a tight query loop while 25 new processes call create_database: 14 of 25 fail immediately with 'database is locked' at _apply_journal_mode:387. (b) Long-running process querying every 100 ms while 5 new processes open the DB: in 1 of 3 runs the existing process got 'disk I/O error' on every query from about 2.5 s to the end of the 6 s probe. In a separate run with a 50 ms reader during 25 openers, 0 of 159 queries succeeded. The orchestrator, the FastAPI server and every agent's MCP server (mcp_server/feature_mcp.py:113) all call create_database, so parallel mode (the reporter's setup) is exposed.
✅ Normal local filesystem keeps WAL Without the shim the journal mode stays wal. Concurrent opener and reader processes ran with no errors (40,088 queries).
✅ Mount parsing / FS classification Reviewed _fs_type_for_path (path-boundary match, longest mount wins, octal unescape) and _is_unsafe_wal_fs_type; behaviour is correct and covered by the new unit tests.
✅ Other errors not swallowed Errors other than disk I/O or locking protocol are re-raised (unit test plus code review).
✅ No regressions in existing tests, lint or types The full pytest run has the same 21 failures as origin/master (test_devserver_security.py). The mypy . error count is identical to master (24, none in api/database.py). ruff is clean.
✅ Server boot and UI smoke uvicorn server.main:app on 127.0.0.1:5247: /api/health 200, /api/projects 200, / 200. SPA renders in Playwright with 0 console errors. No UI files changed, so UI build, lint and e2e were not run.
✅ Security / backward compatibility security.py and the localhost middleware are untouched. No schema change. The warning log contains only the DB path and the SQLite error, no secrets.
🧪 Commands run
Command Result
python -m pytest test_database_journal.py -q 30 passed
ruff check . All checks passed
mypy api/database.py Success: no issues found
mypy . (PR vs origin/master) 24 errors in 9 files on both, pre-existing, none in api/database.py
python test_security.py ALL TESTS PASSED
python -m pytest -q (PR vs origin/master worktree) PR: 21 failed, 174 passed; master: 21 failed, 144 passed; identical failure list (test_devserver_security.py, pre-existing)
gcc -shared -fPIC noshm.c -o noshm.so; LD_PRELOAD=noshm.so NOSHM=1 python probe.py <master|PR> <local|unsafe> <dir> master: disk I/O error in all 3 shm-failure scenarios; PR: fresh and existing-WAL-undetected pass, existing-WAL-on-detected-unsafe-FS fails with disk I/O error
LD_PRELOAD=noshm.so: reader.py / reader2.py long-running process + repeated opener2.py create_database processes 14/25 openers fail 'database is locked'; long-running reader got disk I/O error on every query after an opener's WAL switch in 2 of 4 runs (rest of a 6 s window; all 159 queries in another run)
uvicorn server.main:app --host 127.0.0.1 --port 5247 + curl /api/health, /api/projects, / 200 / 200 / 200; server stopped afterwards

🔧 What needs fixing

In api/database.py create_database():

  1. Make the recovery independent of the chosen mode. When journal_mode is 'DELETE' (unsafe FS detected), call _force_rollback_journal(db_path) before _apply_journal_mode(engine, 'DELETE'), or catch _is_wal_io_error for both modes and run engine.dispose(); _force_rollback_journal(db_path); _apply_journal_mode(engine, 'DELETE'). Then an existing WAL-mode features.db on WSL /mnt/c, 9p or vboxsf opens instead of crashing.
  2. Don't switch an existing features.db to WAL on every process start when WAL has already been shown not to work. Recommended: probe WAL support once per directory on a throwaway file (e.g. /.wal-probe.db: set journal_mode=WAL, run SELECT count(*) FROM sqlite_master, then close and delete it along with its -wal/-shm). Only set WAL on features.db if the probe succeeds, and cache the result in-process. Otherwise use DELETE (after the rollback conversion in 1). This stops each agent's MCP server, the orchestrator and the UI server from switching the shared file to WAL, which currently produces 'database is locked' in the new process and 'disk I/O error' on every later query in already-running processes.
  3. Add tests: (a) a WAL-mode DB with _is_network_path patched to True and the DELETE apply simulated to fail on the first call ends up in DELETE mode and is usable; (b) when the WAL probe fails, features.db is never set to WAL (assert no PRAGMA journal_mode=WAL is issued against features.db).

No browser screenshots were taken in this round.

Posted by cubefarm · sent back to the developer for fixes

… and probe WAL on a throwaway file

- Run the WAL->DELETE recovery regardless of the chosen journal mode, and
  convert a features.db whose header says WAL before opening it in DELETE
  mode (e.g. one left behind on WSL /mnt/c by an older version).
- Probe WAL support once per directory per process on a throwaway
  .wal-probe-<pid>.db instead of switching the shared features.db to WAL
  on every process start, which caused 'database is locked' and
  'disk I/O error' for concurrent processes in parallel mode.
- Add tests for both cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zwanski-start

Copy link
Copy Markdown
Author

🔍 QA report: ✅ Passed

Tester: Ada (cubefarm agent, in a fresh session) · Round: 2 · Author: Ken

Both round-1 failures are fixed. A WAL-mode features.db on a filesystem detected as unsafe is now converted to DELETE mode and opens with its data intact; master crashes with 'disk I/O error' on the same file. On an undetected filesystem where WAL fails, a one-time probe on a throwaway file means features.db is never switched to WAL, and a 25-process concurrency run against a live process had 0 errors in any process (round 1: 'database is locked' in 14/25 and 'disk I/O error' in already-running processes). Lint, tests, server and UI smoke tests are clean, and the requested tests are present and pass.

Check Details
✅ Issue repro on master (simulated filesystem without -shm support) An LD_PRELOAD shim makes mmap fail on *-shm files in one directory. Master's create_database() fails with sqlite3.OperationalError 'disk I/O error' and leaves features.db in WAL mode (header bytes 2,2) with -wal/-shm files. This matches the issue.
✅ Round-1 #1: existing WAL DB on a detected-unsafe FS (_is_network_path=True) WAL-mode features.db with data plus the shim plus _is_network_path forced True. Master: 'disk I/O error'. PR: logs 'Converting ... from WAL to journal_mode=DELETE', count=1, journal_mode=delete.
✅ Existing WAL DB left by master, undetected FS The PR opened master's leftover WAL file in the shimmed directory: converted, row inserted, mode=delete.
✅ Uncheckpointed WAL contents preserved during conversion 50 rows inserted with autocheckpoint off and the process killed with os._exit (28KB -wal left). Opening it under the shim with the PR converted it to DELETE and all 51 rows were present.
✅ Fresh DB on undetected unsafe FS The probe fails, so DELETE mode is used from the start. The DB works, is reopened by a second process, and no .wal-probe files remain.
✅ Round-1 #2: concurrency on undetected unsafe FS One process looped reads and writes for 25s while 25 new processes (in batches of 5) ran create_database and a query. Holder: ok=1763 errs=0. All 25 new processes: OK, mode=delete. No WAL switching.
✅ Regression: concurrency on a normal local FS Same run without the shim. Holder: ok=2072 errs=0. All 25 new processes: OK, mode=wal. No probe files left behind.
✅ Requested unit tests (3a, 3b) test_delete_apply_failure_recovers and test_failed_probe_never_sets_wal_on_features_db (SQL trace) are present and pass, along with probe cleanup and caching tests.
✅ Backend smoke (FastAPI) uvicorn on 127.0.0.1:5571 with an isolated HOME. GET /api/projects returned 200. Project creation, feature create, feature list and /features/graph worked. An invalid feature body returned a Pydantic 'Field required' error. The local project uses WAL as before.
✅ UI smoke in browser Selected the project. Dashboard shows 0/1 progress, Live. The Kanban board shows the feature in Pending. 0 console errors or warnings.
✅ Security / process cleanup security.py and the middleware are untouched, and both security test scripts pass. The server was stopped (health check refused afterwards). No orphan processes.
🧪 Commands run
Command Result
ruff check . All checks passed
mypy . 25 errors in 10 files; master shows the same 25 (other files), none in api/database.py
python -m pytest -q test_database_journal.py test_client.py test_dependency_resolver.py test_rate_limit_utils.py 92 passed
python test_security.py ALL TESTS PASSED
python test_security_integration.py ALL INTEGRATION TESTS PASSED
LD_PRELOAD=shmfail.so python probe.py <dir> [insert|network] (master vs PR) master: disk I/O error; PR: converted to DELETE and usable in all scenarios
holder.py + 25 concurrent probe.py processes (unsafe and safe dirs) 0 errors in all processes; DELETE on the unsafe dir, WAL on the safe dir
uvicorn server.main:app --host 127.0.0.1 --port 5571 + curl REST routes Expected responses; invalid feature body rejected with 'Field required'

No browser screenshots were taken in this round.

Posted by cubefarm · merges automatically once GitHub's checks pass

cabana8471-arch pushed a commit to cabana8471-arch/autoforge that referenced this pull request Nov 8, 2026
…' and probe WAL on a throwaway file

Adaptations: the journal-mode choice and fallback are now public helpers in api/database.py (select_journal_mode, fall_back_to_rollback_journal, is_wal_io_error), used by create_database and by structured_logging._create_log_schema, so logs.db follows the same WAL rules as features.db (probe, stuck-WAL conversion, disk I/O fallback). Resolved the sqlalchemy import conflict with my-features' typed ORM imports. New tests for logs.db.

Integrates AutoForgeAI#268

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AeH26pcsqoxPb3CDs8g1VN
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MCP Tools not working with GLM 4.7

1 participant