"""Project memory store: short task summaries for prompt context. Zero third-party dependencies; Python standard library only. Design notes: - Summaries are the durable, compacted memory of a project. They are deliberately short (goal / conclusion / files changed / open issues) so that a prompt can carry recent context without replaying history. - ``build_prompt_context`` never returns the whole history: it returns at most ``top_k`` recent active summaries, formatted as a prompt prefix. - ``deprecate`` is a soft mark (status change, not deletion); ``delete`` is a physical removal reserved for user-initiated correction/forgetting. """ import json import sqlite3 from cloud.db import connect as _db_connect import time class MemoryStore: """SQLite-backed store for per-project task summaries.""" def __init__(self, db_path): self.db_path = db_path self.conn = _db_connect(db_path) self.conn.row_factory = sqlite3.Row self.conn.execute( """CREATE TABLE IF NOT EXISTS summaries( id INTEGER PRIMARY KEY AUTOINCREMENT, project_id TEXT, thread_id TEXT, goal TEXT, conclusion TEXT, files_changed TEXT, open_issues TEXT, status TEXT DEFAULT 'active', created_at REAL )""" ) self.conn.commit() def add_summary(self, project_id, thread_id, goal, conclusion, files_changed=None, open_issues=None): """Insert a summary; returns the new row id.""" cur = self.conn.execute( """INSERT INTO summaries (project_id, thread_id, goal, conclusion, files_changed, open_issues, status, created_at) VALUES (?, ?, ?, ?, ?, ?, 'active', ?)""", ( project_id, thread_id, goal, conclusion, json.dumps(list(files_changed) if files_changed is not None else []), json.dumps(list(open_issues) if open_issues is not None else []), time.time(), ), ) self.conn.commit() return cur.lastrowid def list_summaries(self, project_id, include_deprecated=False): """List summaries for one project, newest first. By default only ``status='active'`` rows are returned; pass ``include_deprecated=True`` to also see deprecated ones. """ if include_deprecated: rows = self.conn.execute( """SELECT * FROM summaries WHERE project_id = ? ORDER BY created_at DESC, id DESC""", (project_id,), ).fetchall() else: rows = self.conn.execute( """SELECT * FROM summaries WHERE project_id = ? AND status = 'active' ORDER BY created_at DESC, id DESC""", (project_id,), ).fetchall() return [self._row_to_dict(row) for row in rows] def deprecate(self, summary_id): """Mark a summary deprecated (soft mark, not a physical delete).""" self.conn.execute( "UPDATE summaries SET status = 'deprecated' WHERE id = ?", (summary_id,), ) self.conn.commit() def delete(self, summary_id): """Physically delete a summary. Deletion is a user-initiated action (correction / forgetting): it is the only way a summary is removed from storage, as opposed to :meth:`deprecate`, which merely hides it from default listings and prompt context while keeping the record. """ self.conn.execute( "DELETE FROM summaries WHERE id = ?", (summary_id,), ) self.conn.commit() def build_prompt_context(self, project_id, top_k=5): """Build a prompt prefix from the newest ``top_k`` active summaries. Each summary becomes one line:: - [thread_id] 目标:...;结论:...;改动文件:...;遗留:... Returns an empty string when the project has no active summaries. Never returns the full history: only the newest ``top_k`` rows (see :meth:`list_summaries`) are included. """ summaries = self.list_summaries(project_id)[:top_k] if not summaries: return "" lines = [] for s in summaries: files = "、".join(s["files_changed"]) if s["files_changed"] else "无" issues = "、".join(s["open_issues"]) if s["open_issues"] else "无" lines.append( "- [{thread_id}] 目标:{goal};结论:{conclusion};" "改动文件:{files};遗留:{issues}".format( thread_id=s["thread_id"], goal=s["goal"], conclusion=s["conclusion"], files=files, issues=issues, ) ) return "\n".join(lines) @staticmethod def _row_to_dict(row): d = dict(row) for key in ("files_changed", "open_issues"): try: d[key] = json.loads(d[key]) if d[key] else [] except (TypeError, ValueError): d[key] = [] return d