Files
codex-app-server/cloud/memory.py
T

146 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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