Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions e2e/test_admin_vscode_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def startup(route):
response = route.fetch()
payload = response.json()
payload["startup"]["integrations"]["vscode-chat"] = dict(progress)
payload["startup"]["messaging"]["state"] = "starting"
route.fulfill(response=response, json=payload)

def status(route):
Expand All @@ -74,9 +75,10 @@ def retry(route):
page.route("**/admin/api/integrations/vscode-chat/refresh", retry)
page.goto(f"{admin_base_url}/admin/integrations")
# Startup reconciliation can temporarily disable Retry during the click.
# Exercise the action after initial polling has settled.
# Wait for this integration without waiting for unrelated startup work.
page.wait_for_function(
"state.startup && !state.startupRequest && state.startupTimer === null"
"state.startup?.startup?.integrations?.['vscode-chat']?.state === 'failed'"
" && !vscodeChatIntegration.busy"
)
main = page.locator("#openVSCodeChatIntegration")
secondary = page.locator("#retryVSCodeChatIntegration")
Expand Down
83 changes: 41 additions & 42 deletions src/free_claude_code/runtime/code_sessions_sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import json
import sqlite3
from collections.abc import Callable, Sequence
from contextlib import closing

from free_claude_code.application.code_sessions.models import (
ACTIVE_RUN_STATUSES,
Expand Down Expand Up @@ -376,7 +375,6 @@ def _write_prompt(connection: sqlite3.Connection, prompt: CodePrompt) -> None:
class SQLiteCodeStore:
def __init__(self, database: SQLiteDatabase) -> None:
self.database = database
self._path = database.path
self._started = False
self._lifecycle = asyncio.Lock()

Expand All @@ -386,7 +384,7 @@ async def start(self) -> None:
return
try:
await self.database.start()
await self.database.work(lambda: self._execute(self._recover))
await self._execute(self._recover, write=True)
except sqlite3.IntegrityError as exc:
raise CodeConflictError(
"Saved Code history conflicts with its schema."
Expand All @@ -401,24 +399,24 @@ async def close(self) -> None:
async with self._lifecycle:
self._started = False

def _execute[T](self, operation: Callable[[sqlite3.Connection], T]) -> T:
async def _execute[T](
self, operation: Callable[[sqlite3.Connection], T], *, write: bool
) -> T:
try:
with closing(sqlite3.connect(self._path, timeout=10)) as connection:
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA foreign_keys = ON")
with connection:
return operation(connection)
return await self.database.run(operation, write=write)
except sqlite3.IntegrityError as exc:
raise CodeConflictError(
"This Code operation conflicts with existing session state."
) from exc
except sqlite3.Error as exc:
raise CodeUnavailableError("Code session storage is unavailable.") from exc

async def _run[T](self, operation: Callable[[sqlite3.Connection], T]) -> T:
async def _run[T](
self, operation: Callable[[sqlite3.Connection], T], *, write: bool
) -> T:
if not self._started:
raise CodeUnavailableError("Code session storage is closed.")
return await self.database.work(lambda: self._execute(operation))
return await self._execute(operation, write=write)

def _recover(self, connection: sqlite3.Connection) -> None:
connection.execute(
Expand All @@ -435,7 +433,6 @@ def _recover(self, connection: sqlite3.Connection) -> None:

async def create(self, session: CodeSession) -> CodeSession:
def operation(connection: sqlite3.Connection) -> CodeSession:
connection.execute("BEGIN IMMEDIATE")
if connection.execute(
"SELECT 1 FROM code_deleted WHERE id = ?", (session.id,)
).fetchone():
Expand All @@ -455,10 +452,12 @@ def operation(connection: sqlite3.Connection) -> CodeSession:
_insert(connection, "code_sessions", session)
return session

return await self._run(operation)
return await self._run(operation, write=True)

async def get_session(self, session_id: str) -> CodeSession:
return await self._run(lambda connection: _session(connection, session_id))
return await self._run(
lambda connection: _session(connection, session_id), write=False
)

async def list_sessions(
self, cursor: tuple[int, str] | None, limit: int, query: str = ""
Expand All @@ -484,7 +483,7 @@ def operation(connection: sqlite3.Connection) -> CodePage:
last = sessions[-1] if sessions and len(rows) > limit else None
return CodePage(sessions, (last.updated_at, last.id) if last else None)

return await self._run(operation)
return await self._run(operation, write=False)

async def pending_deletions(self) -> tuple[CodeSession, ...]:
return await self._run(
Expand All @@ -493,7 +492,8 @@ async def pending_deletions(self) -> tuple[CodeSession, ...]:
for row in connection.execute(
"SELECT * FROM code_sessions WHERE status != 'ready'"
)
)
),
write=False,
)

async def is_deleted(self, session_id: str) -> bool:
Expand All @@ -503,7 +503,8 @@ async def is_deleted(self, session_id: str) -> bool:
"SELECT 1 FROM code_deleted WHERE id = ?", (session_id,)
).fetchone()
is not None
)
),
write=False,
)

async def get_run(self, session_id: str, run_id: str) -> CodeRun | None:
Expand All @@ -514,7 +515,7 @@ def operation(connection: sqlite3.Connection) -> CodeRun | None:
).fetchone()
return _record(CodeRun, row) if row else None

return await self._run(operation)
return await self._run(operation, write=False)

async def runs(self, session_id: str) -> tuple[CodeRun, ...]:
return await self._run(
Expand All @@ -524,7 +525,8 @@ async def runs(self, session_id: str) -> tuple[CodeRun, ...]:
"SELECT * FROM code_runs WHERE session_id = ? ORDER BY ordinal",
(session_id,),
)
)
),
write=False,
)

async def latest_run(self, session_id: str) -> CodeRun | None:
Expand All @@ -535,7 +537,7 @@ def operation(connection: sqlite3.Connection) -> CodeRun | None:
).fetchone()
return _record(CodeRun, row) if row else None

return await self._run(operation)
return await self._run(operation, write=False)

async def items(
self, session_id: str, before: tuple[int, int] | None, limit: int | None
Expand All @@ -546,10 +548,9 @@ async def item_page(
self, session_id: str, before: tuple[int, int] | None, limit: int | None
) -> CodeItemPage:
def operation(connection: sqlite3.Connection) -> CodeItemPage:
connection.execute("BEGIN")
return _item_page(connection, session_id, before, limit)

return await self._run(operation)
return await self._run(operation, write=False)

async def read_history(
self,
Expand All @@ -558,7 +559,6 @@ async def read_history(
include_item_ids: Sequence[str],
) -> CodeHistory:
def operation(connection: sqlite3.Connection) -> CodeHistory:
connection.execute("BEGIN")
session = _session(connection, session_id)
run = _latest_run(connection, session_id)
active = run is not None and run.status in ACTIVE_RUN_STATUSES
Expand Down Expand Up @@ -609,11 +609,10 @@ def operation(connection: sqlite3.Connection) -> CodeHistory:
page.next_before,
)

return await self._run(operation)
return await self._run(operation, write=False)

async def execution_seed(self, session_id: str) -> CodeExecutionSeed:
def operation(connection: sqlite3.Connection) -> CodeExecutionSeed:
connection.execute("BEGIN")
session = _session(connection, session_id)
run = _latest_run(connection, session_id)
sequence = connection.execute(
Expand All @@ -636,7 +635,7 @@ def operation(connection: sqlite3.Connection) -> CodeExecutionSeed:
_by_ids(connection, CodeRun, session_id, tuple(ids)),
)

return await self._run(operation)
return await self._run(operation, write=False)

async def get_native_item(
self, session_id: str, turn_id: str, item_id: str
Expand All @@ -648,7 +647,7 @@ def operation(connection: sqlite3.Connection) -> CodeItem | None:
).fetchone()
return _record(CodeItem, row) if row else None

return await self._run(operation)
return await self._run(operation, write=False)

async def run_items(self, session_id: str, run_id: str) -> tuple[CodeItem, ...]:
return await self._run(
Expand All @@ -658,14 +657,16 @@ async def run_items(self, session_id: str, run_id: str) -> tuple[CodeItem, ...]:
"SELECT * FROM code_items WHERE session_id = ? AND run_id = ? ORDER BY sequence",
(session_id, run_id),
)
)
),
write=False,
)

async def get_prompt(self, session_id: str, prompt_id: str) -> CodePrompt | None:
return await self._run(
lambda connection: next(
iter(_by_ids(connection, CodePrompt, session_id, (prompt_id,))), None
)
),
write=False,
)

async def has_prompt_request(
Expand All @@ -678,7 +679,8 @@ async def has_prompt_request(
(session_id, generation, json.dumps(request_id)),
).fetchone()
is not None
)
),
write=False,
)

async def has_native_turn(self, session_id: str, turn_id: str) -> bool:
Expand All @@ -689,7 +691,8 @@ async def has_native_turn(self, session_id: str, turn_id: str) -> bool:
(session_id, turn_id),
).fetchone()
is not None
)
),
write=False,
)

async def prompts(self, session_id: str) -> tuple[CodePrompt, ...]:
Expand All @@ -700,14 +703,14 @@ async def prompts(self, session_id: str) -> tuple[CodePrompt, ...]:
"SELECT * FROM code_prompts WHERE session_id = ? ORDER BY id",
(session_id,),
)
)
),
write=False,
)

async def update_settings(
self, session: CodeSession, expected_revision: int
) -> CodeSession:
def operation(connection: sqlite3.Connection) -> CodeSession:
connection.execute("BEGIN IMMEDIATE")
previous = _session(connection, session.id)
if previous.status != "ready":
raise CodeConflictError("This session is being deleted.")
Expand All @@ -720,13 +723,12 @@ def operation(connection: sqlite3.Connection) -> CodeSession:
_write_session(connection, session, expected_revision, settings=True)
return _session(connection, session.id)

return await self._run(operation)
return await self._run(operation, write=True)

async def admit_run(
self, session: CodeSession, run: CodeRun, item: CodeItem, expected_revision: int
) -> tuple[CodeSession, CodeRun]:
def operation(connection: sqlite3.Connection) -> tuple[CodeSession, CodeRun]:
connection.execute("BEGIN IMMEDIATE")
previous = _session(connection, session.id)
row = connection.execute(
"SELECT * FROM code_runs WHERE session_id = ? AND id = ?",
Expand Down Expand Up @@ -768,13 +770,12 @@ def operation(connection: sqlite3.Connection) -> tuple[CodeSession, CodeRun]:
_insert(connection, "code_items", item)
return _session(connection, session.id), admitted

return await self._run(operation)
return await self._run(operation, write=True)

async def claim_prompt(
self, session_id: str, prompt_id: str, response_id: str, generation: str
) -> CodePrompt:
def operation(connection: sqlite3.Connection) -> CodePrompt:
connection.execute("BEGIN IMMEDIATE")
row = connection.execute(
"SELECT * FROM code_prompts WHERE session_id = ? AND id = ?",
(session_id, prompt_id),
Expand All @@ -794,7 +795,7 @@ def operation(connection: sqlite3.Connection) -> CodePrompt:
_write_prompt(connection, claimed)
return claimed

return await self._run(operation)
return await self._run(operation, write=True)

async def save_progress(
self,
Expand All @@ -806,7 +807,6 @@ async def save_progress(
prompts: Sequence[CodePrompt] = (),
) -> None:
def operation(connection: sqlite3.Connection) -> None:
connection.execute("BEGIN IMMEDIATE")
_write_session(connection, session, expected_revision, settings=False)
if run is not None:
if run.session_id != session.id:
Expand All @@ -821,14 +821,13 @@ def operation(connection: sqlite3.Connection) -> None:
raise CodeConflictError("This prompt belongs to another session.")
_write_prompt(connection, prompt)

await self._run(operation)
await self._run(operation, write=True)

async def delete(self, session_id: str) -> None:
def operation(connection: sqlite3.Connection) -> None:
connection.execute("BEGIN IMMEDIATE")
connection.execute(
"INSERT OR IGNORE INTO code_deleted(id) VALUES (?)", (session_id,)
)
connection.execute("DELETE FROM code_sessions WHERE id = ?", (session_id,))

await self._run(operation)
await self._run(operation, write=True)
13 changes: 10 additions & 3 deletions src/free_claude_code/runtime/sqlite_database.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Offline relocation and transactional schema initialization of FCC's database.
"""Connection, transaction, and lifetime ownership of FCC's shared database.

The application holds the shared owner lock until both features close. Connections
opened here never escape initialization, including when migration fails.
The application holds the owner lock until both features close. Each connection
is opened, used, and closed within initialization or one synchronous transaction.
"""

import asyncio
Expand Down Expand Up @@ -228,6 +228,12 @@ def _initialize(self) -> None:
def execute[T](
self, operation: Callable[[sqlite3.Connection], T], *, write: bool = True
) -> T:
"""Run one transaction inside an admitted worker, closing its connection.

Callbacks consume the connection synchronously and return materialized
results. They must not begin or finish the outer transaction themselves.
Legacy import uses this primitive for several transactions in one worker.
"""
with closing(_connect(self.path)) as connection:
connection.execute("BEGIN IMMEDIATE" if write else "BEGIN")
try:
Expand All @@ -242,6 +248,7 @@ def execute[T](
async def run[T](
self, operation: Callable[[sqlite3.Connection], T], *, write: bool = True
) -> T:
"""Admit and supervise one transaction through committed result delivery."""
return await self.work(lambda: self.execute(operation, write=write))

async def work[T](self, operation: Callable[[], T]) -> T:
Expand Down
Loading
Loading