Add read_file skill, session persistence, and update script
- 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>
This commit is contained in:
@@ -12,6 +12,7 @@ from collections import deque
|
||||
|
||||
from skills import discover_skills, execute_skill, set_logger as set_skills_logger
|
||||
from tools import load_memory, query_ollama, set_logger as set_tools_logger
|
||||
from sessions import init_db, save_message, load_recent, set_logger as set_sessions_logger
|
||||
|
||||
# ─── Config ──────────────────────────────────────────────────────────
|
||||
|
||||
@@ -63,6 +64,7 @@ def log(msg):
|
||||
# Inject logger into submodules
|
||||
set_skills_logger(log)
|
||||
set_tools_logger(log)
|
||||
set_sessions_logger(log)
|
||||
|
||||
# ─── Init ────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -70,6 +72,11 @@ AGENT_MEMORY = load_memory(WORKSPACE)
|
||||
TOOLS, SKILL_SCRIPTS = discover_skills(SKILL_DIRS)
|
||||
log(f"Loaded {len(TOOLS)} skills: {', '.join(SKILL_SCRIPTS.keys())}")
|
||||
|
||||
db_conn = init_db(f"{WORKSPACE}/sessions.db")
|
||||
for msg in load_recent(db_conn, CONTEXT_SIZE):
|
||||
recent.append(msg)
|
||||
log(f"Session: restored {len(recent)} messages")
|
||||
|
||||
|
||||
def reload_memory():
|
||||
global AGENT_MEMORY
|
||||
@@ -212,6 +219,7 @@ def handle_message(irc, source_nick, target, text):
|
||||
reply_to = source_nick if is_dm else target
|
||||
|
||||
recent.append({"nick": source_nick, "text": text, "channel": channel})
|
||||
save_message(db_conn, source_nick, channel, text)
|
||||
|
||||
if source_nick == NICK:
|
||||
return
|
||||
@@ -249,6 +257,7 @@ def handle_message(irc, source_nick, target, text):
|
||||
|
||||
irc.say(reply_to, "\n".join(lines))
|
||||
recent.append({"nick": NICK, "text": response[:200], "channel": channel})
|
||||
save_message(db_conn, NICK, channel, response[:200], full_text=response)
|
||||
except Exception as e:
|
||||
log(f"Error handling message: {e}")
|
||||
try:
|
||||
|
||||
87
agent/sessions.py
Normal file
87
agent/sessions.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""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})")
|
||||
@@ -166,7 +166,7 @@ def execute_skill(script_path, args, workspace, config):
|
||||
with open(filepath, "w") as f:
|
||||
f.write(output)
|
||||
preview = output[:1500]
|
||||
return f"{preview}\n\n[output truncated — full result ({len(output)} chars) saved to {filepath}. Use run_command to read it: cat {filepath}]"
|
||||
return f"{preview}\n\n[output truncated — full result ({len(output)} chars) saved to {filepath}. Use read_file to view it.]"
|
||||
|
||||
return output
|
||||
except subprocess.TimeoutExpired:
|
||||
|
||||
Reference in New Issue
Block a user