@@ -59,6 +59,31 @@ def _get_state_dir(repo_path: Path) -> Path:
5959 return repo_path / CODEFRAME_DIR
6060
6161
62+ def _open_db (db_path : str | Path ) -> sqlite3 .Connection :
63+ """Open a workspace SQLite connection with concurrency safeguards.
64+
65+ Mirrors ``codeframe/platform_store/database.py``. The substantive change is
66+ enabling **WAL journaling**: readers no longer block writers, which removes
67+ the rollback-journal case where a writer hits ``database is locked``
68+ immediately (the busy handler is skipped for that reader/writer deadlock).
69+ WAL is a persistent, database-level setting, so applying it on every
70+ connection is idempotent. ``busy_timeout`` is set to 5000ms to match
71+ platform_store and make the value explicit (Python's ``sqlite3.connect``
72+ already defaults to a 5s timeout). Matters under parallel batch execution
73+ where multiple processes and background agent threads write the same DB.
74+
75+ The caller is responsible for closing the connection.
76+ """
77+ # NOTE: pass ``db_path`` through unchanged (sqlite3.connect accepts both str
78+ # and PathLike). Do NOT wrap in ``str()`` — that would coerce a non-path
79+ # (e.g. a test's MagicMock) into a literal filename and silently create a
80+ # junk DB file instead of raising, diverging from the prior connect call.
81+ conn = sqlite3 .connect (db_path )
82+ conn .execute ("PRAGMA journal_mode = WAL" )
83+ conn .execute ("PRAGMA busy_timeout = 5000" )
84+ return conn
85+
86+
6287def _init_database (db_path : Path ) -> None :
6388 """Initialize the workspace SQLite database with v2 schema.
6489
@@ -70,7 +95,7 @@ def _init_database(db_path: Path) -> None:
7095 - blockers: Human-in-the-loop blockers
7196 - checkpoints: State snapshots
7297 """
73- conn = sqlite3 . connect (db_path )
98+ conn = _open_db (db_path )
7499 cursor = conn .cursor ()
75100
76101 # Workspace metadata
@@ -408,7 +433,7 @@ def _ensure_schema_upgrades(db_path: Path) -> None:
408433 This function is idempotent and adds any new tables/columns
409434 that were added after the initial schema creation.
410435 """
411- conn = sqlite3 . connect (db_path )
436+ conn = _open_db (db_path )
412437 cursor = conn .cursor ()
413438
414439 # Check if batch_runs table exists, if not create it
@@ -767,7 +792,7 @@ def create_or_load_workspace(repo_path: Path, tech_stack: Optional[str] = None)
767792 workspace_id = str (uuid .uuid4 ())
768793 now = _utc_now ().isoformat ()
769794
770- conn = sqlite3 . connect (db_path )
795+ conn = _open_db (db_path )
771796 cursor = conn .cursor ()
772797 cursor .execute (
773798 "INSERT INTO workspace (id, repo_path, tech_stack, created_at, updated_at) VALUES (?, ?, ?, ?, ?)" ,
@@ -807,7 +832,7 @@ def get_workspace(repo_path: Path) -> Workspace:
807832 # Ensure schema is up to date for existing workspaces
808833 _ensure_schema_upgrades (db_path )
809834
810- conn = sqlite3 . connect (db_path )
835+ conn = _open_db (db_path )
811836 cursor = conn .cursor ()
812837 cursor .execute ("SELECT id, repo_path, tech_stack, created_at FROM workspace LIMIT 1" )
813838 row = cursor .fetchone ()
@@ -836,7 +861,17 @@ def get_db_connection(workspace: Workspace) -> sqlite3.Connection:
836861 Returns:
837862 SQLite connection
838863 """
839- return sqlite3 .connect (workspace .db_path )
864+ return _open_db (workspace .db_path )
865+
866+
867+ def get_db_connection_by_path (db_path : str | Path ) -> sqlite3 .Connection :
868+ """Open a workspace DB connection from a raw path (WAL + busy_timeout).
869+
870+ Same connection setup as :func:`get_db_connection`, for callers that hold a
871+ path rather than a :class:`Workspace` (e.g. the costs router's helpers that
872+ tolerate fresh/locked DBs). The caller is responsible for closing it.
873+ """
874+ return _open_db (db_path )
840875
841876
842877def workspace_exists (repo_path : Path ) -> bool :
@@ -875,7 +910,7 @@ def update_workspace_tech_stack(repo_path: Path, tech_stack: Optional[str]) -> W
875910
876911 now = _utc_now ().isoformat ()
877912
878- conn = sqlite3 . connect (db_path )
913+ conn = _open_db (db_path )
879914 cursor = conn .cursor ()
880915 cursor .execute (
881916 "UPDATE workspace SET tech_stack = ?, updated_at = ?" ,
0 commit comments