api.py: route all sqlite3.connect() through helpers; enable synchronous=NORMAL per-conn
Followup to 4204806 (WAL + index + backup.sh). The previous commit
deferred synchronous=NORMAL because it's a per-connection PRAGMA and
api.py has 16 sqlite3.connect() call sites — setting it once at init
would have applied to nothing afterwards.
Adds three helpers near the *_DB constants:
- _connect(path): inner; sets PRAGMA synchronous=NORMAL and uses
timeout=5.0 (5000ms busy_timeout) on every new connection.
- _connect_conversations(), _connect_sessions(): named wrappers so call
sites read explicitly.
Mechanical replacement at all 16 call sites: 4 sessions, 12 conversations.
No semantic change beyond the PRAGMA + busy_timeout — every site still
opens-then-closes, no held-open connections.
busy_timeout=5000ms is cheap insurance: under WAL with api.py as sole
writer, contention should be near-zero, but the backup.sh online-backup
path briefly holds a read lock on the source, and any future second
writer would otherwise hit SQLITE_BUSY immediately on contention.
Combined effect with WAL: per-write fsync count drops from ~2 to ~1
(WAL alone) further reduced by synchronous=NORMAL deferring fsyncs to
checkpoint boundaries. No durability loss for the use case (single
host, app crash tolerated, OS crash gives at most one lost transaction).
Not included: foreign_keys=ON. Audit found 2 orphan rows in messages
(conversation_id pointing to deleted conversations) and untested write
paths that could begin raising IntegrityError. Tracked as separate
followup: inspect orphans, identify the delete path that didn't
cascade, clean up, then enable enforcement and test chat delete flow
end-to-end.
This commit is contained in:
+28
-16
@@ -38,6 +38,18 @@ load_dotenv(Path.home() / "aaronai" / ".env")
|
|||||||
|
|
||||||
MEMORY_PATH = Path.home() / "aaronai" / "memory.md"
|
MEMORY_PATH = Path.home() / "aaronai" / "memory.md"
|
||||||
CONVERSATIONS_DB = str(Path.home() / "aaronai" / "conversations.db")
|
CONVERSATIONS_DB = str(Path.home() / "aaronai" / "conversations.db")
|
||||||
|
|
||||||
|
def _connect(path):
|
||||||
|
conn = sqlite3.connect(path, timeout=5.0)
|
||||||
|
conn.execute("PRAGMA synchronous=NORMAL")
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def _connect_conversations():
|
||||||
|
return _connect(CONVERSATIONS_DB)
|
||||||
|
|
||||||
|
def _connect_sessions():
|
||||||
|
return _connect(SESSIONS_DB)
|
||||||
|
|
||||||
SETTINGS_PATH = Path.home() / "aaronai" / "settings.json"
|
SETTINGS_PATH = Path.home() / "aaronai" / "settings.json"
|
||||||
WATCHER_LOG = str(Path.home() / "aaronai" / "watcher.log")
|
WATCHER_LOG = str(Path.home() / "aaronai" / "watcher.log")
|
||||||
WATCHER_STATE = str(Path.home() / "aaronai" / "watcher_state.json")
|
WATCHER_STATE = str(Path.home() / "aaronai" / "watcher_state.json")
|
||||||
@@ -122,7 +134,7 @@ SESSION_PASSWORD = os.getenv("AARON_AI_PASSWORD", "changeme")
|
|||||||
SESSIONS_DB = str(Path.home() / "aaronai" / "sessions.db")
|
SESSIONS_DB = str(Path.home() / "aaronai" / "sessions.db")
|
||||||
|
|
||||||
def _init_sessions():
|
def _init_sessions():
|
||||||
conn = sqlite3.connect(SESSIONS_DB)
|
conn = _connect_sessions()
|
||||||
conn.execute("CREATE TABLE IF NOT EXISTS sessions (token TEXT PRIMARY KEY, created_at TEXT)")
|
conn.execute("CREATE TABLE IF NOT EXISTS sessions (token TEXT PRIMARY KEY, created_at TEXT)")
|
||||||
conn.execute("PRAGMA journal_mode=WAL")
|
conn.execute("PRAGMA journal_mode=WAL")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -137,19 +149,19 @@ def hash_password(password: str) -> str:
|
|||||||
return hashlib.sha256(password.encode()).hexdigest()
|
return hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
|
||||||
def save_session(token: str):
|
def save_session(token: str):
|
||||||
conn = sqlite3.connect(SESSIONS_DB)
|
conn = _connect_sessions()
|
||||||
conn.execute("INSERT OR REPLACE INTO sessions VALUES (?, ?)", (token, datetime.now().isoformat()))
|
conn.execute("INSERT OR REPLACE INTO sessions VALUES (?, ?)", (token, datetime.now().isoformat()))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def delete_session(token: str):
|
def delete_session(token: str):
|
||||||
conn = sqlite3.connect(SESSIONS_DB)
|
conn = _connect_sessions()
|
||||||
conn.execute("DELETE FROM sessions WHERE token = ?", (token,))
|
conn.execute("DELETE FROM sessions WHERE token = ?", (token,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def session_exists(token: str) -> bool:
|
def session_exists(token: str) -> bool:
|
||||||
conn = sqlite3.connect(SESSIONS_DB)
|
conn = _connect_sessions()
|
||||||
row = conn.execute("SELECT 1 FROM sessions WHERE token = ?", (token,)).fetchone()
|
row = conn.execute("SELECT 1 FROM sessions WHERE token = ?", (token,)).fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
return row is not None
|
return row is not None
|
||||||
@@ -164,7 +176,7 @@ def require_auth(request: Request):
|
|||||||
return token
|
return token
|
||||||
|
|
||||||
def init_conversations_db():
|
def init_conversations_db():
|
||||||
conn = sqlite3.connect(CONVERSATIONS_DB)
|
conn = _connect_conversations()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute('''CREATE TABLE IF NOT EXISTS conversations (
|
c.execute('''CREATE TABLE IF NOT EXISTS conversations (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@@ -253,7 +265,7 @@ def retrieve_context(query, n_results=8):
|
|||||||
return context_pieces, sources
|
return context_pieces, sources
|
||||||
|
|
||||||
def get_conversation_history(conversation_id, limit=20):
|
def get_conversation_history(conversation_id, limit=20):
|
||||||
conn = sqlite3.connect(CONVERSATIONS_DB)
|
conn = _connect_conversations()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute('''SELECT role, content FROM messages
|
c.execute('''SELECT role, content FROM messages
|
||||||
WHERE conversation_id = ?
|
WHERE conversation_id = ?
|
||||||
@@ -263,7 +275,7 @@ def get_conversation_history(conversation_id, limit=20):
|
|||||||
return [{"role": r[0], "content": r[1]} for r in reversed(rows)]
|
return [{"role": r[0], "content": r[1]} for r in reversed(rows)]
|
||||||
|
|
||||||
def save_message(conversation_id, role, content, sources=None):
|
def save_message(conversation_id, role, content, sources=None):
|
||||||
conn = sqlite3.connect(CONVERSATIONS_DB)
|
conn = _connect_conversations()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
msg_id = hashlib.md5(f"{conversation_id}{role}{datetime.now().isoformat()}".encode()).hexdigest()
|
msg_id = hashlib.md5(f"{conversation_id}{role}{datetime.now().isoformat()}".encode()).hexdigest()
|
||||||
timestamp = datetime.now().isoformat()
|
timestamp = datetime.now().isoformat()
|
||||||
@@ -277,7 +289,7 @@ def save_message(conversation_id, role, content, sources=None):
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
def create_conversation(title="New conversation"):
|
def create_conversation(title="New conversation"):
|
||||||
conn = sqlite3.connect(CONVERSATIONS_DB)
|
conn = _connect_conversations()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
conv_id = hashlib.md5(f"{datetime.now().isoformat()}".encode()).hexdigest()[:16]
|
conv_id = hashlib.md5(f"{datetime.now().isoformat()}".encode()).hexdigest()[:16]
|
||||||
now = datetime.now().isoformat()
|
now = datetime.now().isoformat()
|
||||||
@@ -412,7 +424,7 @@ async def update_settings(request: Request, auth: str = Depends(require_auth)):
|
|||||||
|
|
||||||
@app.get("/api/conversations")
|
@app.get("/api/conversations")
|
||||||
async def list_conversations(auth: str = Depends(require_auth)):
|
async def list_conversations(auth: str = Depends(require_auth)):
|
||||||
conn = sqlite3.connect(CONVERSATIONS_DB)
|
conn = _connect_conversations()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute('''SELECT id, title, created_at, updated_at, message_count
|
c.execute('''SELECT id, title, created_at, updated_at, message_count
|
||||||
FROM conversations ORDER BY updated_at DESC LIMIT 100''')
|
FROM conversations ORDER BY updated_at DESC LIMIT 100''')
|
||||||
@@ -432,7 +444,7 @@ async def new_conversation(request: Request, auth: str = Depends(require_auth)):
|
|||||||
|
|
||||||
@app.get("/api/conversations/{conv_id}/messages")
|
@app.get("/api/conversations/{conv_id}/messages")
|
||||||
async def get_messages(conv_id: str, auth: str = Depends(require_auth)):
|
async def get_messages(conv_id: str, auth: str = Depends(require_auth)):
|
||||||
conn = sqlite3.connect(CONVERSATIONS_DB)
|
conn = _connect_conversations()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute('''SELECT role, content, sources, timestamp FROM messages
|
c.execute('''SELECT role, content, sources, timestamp FROM messages
|
||||||
WHERE conversation_id = ? ORDER BY timestamp ASC''', (conv_id,))
|
WHERE conversation_id = ? ORDER BY timestamp ASC''', (conv_id,))
|
||||||
@@ -449,7 +461,7 @@ async def rename_conversation(conv_id: str, request: Request, auth: str = Depend
|
|||||||
title = data.get("title", "")
|
title = data.get("title", "")
|
||||||
if not title:
|
if not title:
|
||||||
return JSONResponse({"error": "Title required"}, status_code=400)
|
return JSONResponse({"error": "Title required"}, status_code=400)
|
||||||
conn = sqlite3.connect(CONVERSATIONS_DB)
|
conn = _connect_conversations()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("UPDATE conversations SET title = ? WHERE id = ?", (title, conv_id))
|
c.execute("UPDATE conversations SET title = ? WHERE id = ?", (title, conv_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -458,7 +470,7 @@ async def rename_conversation(conv_id: str, request: Request, auth: str = Depend
|
|||||||
|
|
||||||
@app.delete("/api/conversations/{conv_id}")
|
@app.delete("/api/conversations/{conv_id}")
|
||||||
async def delete_conversation(conv_id: str, auth: str = Depends(require_auth)):
|
async def delete_conversation(conv_id: str, auth: str = Depends(require_auth)):
|
||||||
conn = sqlite3.connect(CONVERSATIONS_DB)
|
conn = _connect_conversations()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("DELETE FROM messages WHERE conversation_id = ?", (conv_id,))
|
c.execute("DELETE FROM messages WHERE conversation_id = ?", (conv_id,))
|
||||||
c.execute("DELETE FROM conversations WHERE id = ?", (conv_id,))
|
c.execute("DELETE FROM conversations WHERE id = ?", (conv_id,))
|
||||||
@@ -503,14 +515,14 @@ async def chat_endpoint(request: Request, auth: str = Depends(require_auth)):
|
|||||||
save_message(conversation_id, "user", user_message)
|
save_message(conversation_id, "user", user_message)
|
||||||
|
|
||||||
# Auto-title conversation from first message
|
# Auto-title conversation from first message
|
||||||
conn = sqlite3.connect(CONVERSATIONS_DB)
|
conn = _connect_conversations()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("SELECT message_count, title FROM conversations WHERE id = ?", (conversation_id,))
|
c.execute("SELECT message_count, title FROM conversations WHERE id = ?", (conversation_id,))
|
||||||
row = c.fetchone()
|
row = c.fetchone()
|
||||||
conn.close()
|
conn.close()
|
||||||
if row and row[0] <= 1 and row[1] == "New conversation":
|
if row and row[0] <= 1 and row[1] == "New conversation":
|
||||||
auto_title = user_message[:60] + ("..." if len(user_message) > 60 else "")
|
auto_title = user_message[:60] + ("..." if len(user_message) > 60 else "")
|
||||||
conn = sqlite3.connect(CONVERSATIONS_DB)
|
conn = _connect_conversations()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("UPDATE conversations SET title = ? WHERE id = ?", (auto_title, conversation_id))
|
c.execute("UPDATE conversations SET title = ? WHERE id = ?", (auto_title, conversation_id))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
@@ -590,7 +602,7 @@ async def get_status(auth: str = Depends(require_auth)):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Conversation count
|
# Conversation count
|
||||||
conn = sqlite3.connect(CONVERSATIONS_DB)
|
conn = _connect_conversations()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("SELECT COUNT(*) FROM conversations")
|
c.execute("SELECT COUNT(*) FROM conversations")
|
||||||
conv_count = c.fetchone()[0]
|
conv_count = c.fetchone()[0]
|
||||||
@@ -973,7 +985,7 @@ async def reindex_status(auth: str = Depends(require_auth)):
|
|||||||
|
|
||||||
@app.delete("/api/conversations")
|
@app.delete("/api/conversations")
|
||||||
async def clear_all_conversations(auth: str = Depends(require_auth)):
|
async def clear_all_conversations(auth: str = Depends(require_auth)):
|
||||||
conn = sqlite3.connect(CONVERSATIONS_DB)
|
conn = _connect_conversations()
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute("DELETE FROM messages")
|
c.execute("DELETE FROM messages")
|
||||||
c.execute("DELETE FROM conversations")
|
c.execute("DELETE FROM conversations")
|
||||||
|
|||||||
Reference in New Issue
Block a user