133 lines
4.5 KiB
Python
Executable File
133 lines
4.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""Cursor code5 稳定化:保留最近 N 条对话气泡,其余仅保留列表元数据。
|
||
|
||
根因(社区+本机):
|
||
- 切标签时一次性加载过多 bubbleId/composerData → 渲染进程 OOM → code 5
|
||
- AgentAnalytics SQLite 嵌套事务加剧不稳定
|
||
- 全量 restore 后 1 万+ bubble 是复发主因
|
||
|
||
策略:
|
||
- 最近 keep 条:保留 bubbleId + fullConversationHeadersOnly
|
||
- 更早的:删 bubbleId,清空 headers(列表仍可见,点开可从 Mongo 再 restore --name)
|
||
- 安全清 composer.content/ofsContent/agentKv 等可再生缓存
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import sqlite3
|
||
from pathlib import Path
|
||
|
||
DB = Path.home() / "Library/Application Support/Cursor/User/globalStorage/state.vscdb"
|
||
|
||
SAFE_DELETE_PREFIXES = (
|
||
"composer.content.%",
|
||
"ofsContent:%",
|
||
"agentKv:blob:%",
|
||
"checkpointId:%",
|
||
"codeBlockPartialInlineDiffFates:%",
|
||
"codeBlockDiff:%",
|
||
"inlineDiff:%",
|
||
"patch-graph:%",
|
||
"messageRequestContext:%",
|
||
"expectedContent-v1-%",
|
||
"composerVirtualRowHeights:%",
|
||
)
|
||
|
||
|
||
def load_composers(cur: sqlite3.Cursor) -> list[tuple[str, int, dict]]:
|
||
rows: list[tuple[str, int, dict]] = []
|
||
for key, value in cur.execute(
|
||
"SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'"
|
||
):
|
||
cid = key.replace("composerData:", "")
|
||
try:
|
||
data = json.loads(value)
|
||
except (json.JSONDecodeError, TypeError):
|
||
continue
|
||
ts = int(data.get("lastUpdatedAt") or data.get("createdAt") or 0)
|
||
rows.append((cid, ts, data))
|
||
rows.sort(key=lambda x: x[1], reverse=True)
|
||
return rows
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="Cursor code5 稳定化")
|
||
parser.add_argument("--keep", type=int, default=120, help="保留完整气泡的最近 N 条对话")
|
||
args = parser.parse_args()
|
||
|
||
if not DB.is_file():
|
||
print(f"❌ 未找到 {DB}")
|
||
return 1
|
||
|
||
before_mb = DB.stat().st_size // (1024 * 1024)
|
||
conn = sqlite3.connect(DB, timeout=300)
|
||
cur = conn.cursor()
|
||
cur.execute("PRAGMA busy_timeout=300000")
|
||
|
||
composers = load_composers(cur)
|
||
keep_ids = {cid for cid, _, _ in composers[: args.keep]}
|
||
prune_ids = [cid for cid, _, _ in composers[args.keep :]]
|
||
|
||
pruned_bubbles = 0
|
||
pruned_headers = 0
|
||
for cid in prune_ids:
|
||
cur.execute("DELETE FROM cursorDiskKV WHERE key LIKE ?", (f"bubbleId:{cid}:%",))
|
||
pruned_bubbles += cur.rowcount
|
||
row = cur.execute(
|
||
"SELECT value FROM cursorDiskKV WHERE key = ?", (f"composerData:{cid}",)
|
||
).fetchone()
|
||
if not row:
|
||
continue
|
||
try:
|
||
data = json.loads(row[0])
|
||
except (json.JSONDecodeError, TypeError):
|
||
continue
|
||
if data.get("fullConversationHeadersOnly"):
|
||
data["fullConversationHeadersOnly"] = []
|
||
data["hasLoaded"] = False
|
||
cur.execute(
|
||
"UPDATE cursorDiskKV SET value = ? WHERE key = ?",
|
||
(json.dumps(data, ensure_ascii=False), f"composerData:{cid}"),
|
||
)
|
||
pruned_headers += 1
|
||
|
||
cache_deleted = 0
|
||
for pat in SAFE_DELETE_PREFIXES:
|
||
if pat.endswith("%") and ":" not in pat.replace(".%", ""):
|
||
q = f"DELETE FROM cursorDiskKV WHERE key LIKE '{pat}'"
|
||
else:
|
||
q = f"DELETE FROM cursorDiskKV WHERE key LIKE '{pat}'"
|
||
cur.execute(q)
|
||
cache_deleted += cur.rowcount
|
||
|
||
cur.execute("DELETE FROM ItemTable WHERE key = 'browserAutomation.history'")
|
||
cur.execute("DELETE FROM ItemTable WHERE key = 'aiCodeTrackingLines'")
|
||
|
||
conn.commit()
|
||
print(f"📋 composerData 总数: {len(composers)}")
|
||
print(f"✅ 保留完整气泡: {len(keep_ids)} 条")
|
||
print(f"🗂️ 仅保留列表元数据: {len(prune_ids)} 条")
|
||
print(f"🧹 删除旧 bubbleId: {pruned_bubbles} 行")
|
||
print(f"🧹 清空旧 headers: {pruned_headers} 条")
|
||
print(f"🧹 删除可再生缓存: {cache_deleted} 行")
|
||
|
||
print("🔧 WAL checkpoint…")
|
||
cur.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||
print(f" ✅ {cur.fetchone()}")
|
||
print("🔧 VACUUM…")
|
||
cur.execute("VACUUM")
|
||
conn.commit()
|
||
cur.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||
conn.close()
|
||
|
||
after_mb = DB.stat().st_size // (1024 * 1024)
|
||
print(f"📊 {before_mb} MB → {after_mb} MB")
|
||
print("💡 旧对话仍在列表;点开若空白:")
|
||
print(' python3 agent_sync_restore.py restore --name "对话关键词"')
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|