- New read_file skill: paginated file reading with line ranges, path restricted to /workspace, binary detection, directory listing - Session persistence via SQLite + FTS5: conversation history survives agent restarts, last N messages restored into deque on boot, auto-prune to 1000 messages - Update truncation hint to reference read_file instead of run_command - New scripts/update.sh for patching rootfs + rebuilding snapshot Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
88 lines
2.6 KiB
Python
88 lines
2.6 KiB
Python
"""Session persistence — SQLite + FTS5 for conversation history."""
|
|
|
|
import sqlite3
|
|
import threading
|
|
import time
|
|
|
|
|
|
def log(msg):
|
|
print(f"[sessions] {msg}", flush=True)
|
|
|
|
|
|
def set_logger(fn):
|
|
global log
|
|
log = fn
|
|
|
|
|
|
_write_lock = threading.Lock()
|
|
|
|
|
|
def init_db(db_path):
|
|
"""Create/open the session database. Returns a connection."""
|
|
conn = sqlite3.connect(db_path, check_same_thread=False)
|
|
conn.execute("PRAGMA journal_mode=WAL")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ts REAL NOT NULL,
|
|
nick TEXT NOT NULL,
|
|
channel TEXT NOT NULL,
|
|
text TEXT NOT NULL,
|
|
full_text TEXT
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_messages_channel_ts
|
|
ON messages(channel, ts)
|
|
""")
|
|
# FTS5 virtual table for full-text search
|
|
conn.execute("""
|
|
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts
|
|
USING fts5(text, content=messages, content_rowid=id)
|
|
""")
|
|
conn.commit()
|
|
_prune(conn)
|
|
log(f"Session DB ready: {db_path}")
|
|
return conn
|
|
|
|
|
|
def save_message(conn, nick, channel, text, full_text=None):
|
|
"""Persist a message to the session database."""
|
|
with _write_lock:
|
|
cur = conn.execute(
|
|
"INSERT INTO messages (ts, nick, channel, text, full_text) VALUES (?, ?, ?, ?, ?)",
|
|
(time.time(), nick, channel, text, full_text),
|
|
)
|
|
conn.execute(
|
|
"INSERT INTO messages_fts(rowid, text) VALUES (?, ?)",
|
|
(cur.lastrowid, text),
|
|
)
|
|
conn.commit()
|
|
|
|
|
|
def load_recent(conn, limit=20):
|
|
"""Load the last N messages for boot recovery.
|
|
Returns list of {"nick", "text", "channel"} dicts in chronological order."""
|
|
rows = conn.execute(
|
|
"SELECT nick, text, channel FROM messages ORDER BY id DESC LIMIT ?",
|
|
(limit,),
|
|
).fetchall()
|
|
return [{"nick": r[0], "text": r[1], "channel": r[2]} for r in reversed(rows)]
|
|
|
|
|
|
def _prune(conn, keep=1000):
|
|
"""Delete old messages beyond the last `keep`. Runs once at init."""
|
|
count = conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0]
|
|
if count <= keep:
|
|
return
|
|
deleted = count - keep
|
|
conn.execute("""
|
|
DELETE FROM messages WHERE id NOT IN (
|
|
SELECT id FROM messages ORDER BY id DESC LIMIT ?
|
|
)
|
|
""", (keep,))
|
|
# Rebuild FTS index after bulk delete
|
|
conn.execute("INSERT INTO messages_fts(messages_fts) VALUES('rebuild')")
|
|
conn.commit()
|
|
log(f"Pruned {deleted} old messages (kept {keep})")
|