OpenCode - mac os x - полный расклад по занятому диску - shell - 2026
Published: 2026-09-27
openCode на macOS разбрасывает свои файлы по шести каталогам, и штатной команды «сколько места занято» у него нет. Инструмент opencode stats показывает токены и стоимость, а не байты на диске; opencode uninstall --dry-run перечисляет удаляемое, но не разбирает, что именно внутри. Из-за этого самый тяжёлый файл — база opencode.db на 976 МБ — остаётся незамеченным, пока не приходит счёт за место.
Ниже — готовый скрипт на Python, который раскладывает файлы openCode по полочкам: шесть корней, рекурсивная разбивка каталогов, разбор SQLite через виртуальную таблицу dbstat, логи, крупнейшие файлы. Скрипт работает строго на чтение: он не удаляет, не вакуумит и никак не меняет данные.
Главный вывод по реальному замеру на macOS: из 2,9 ГБ примерно треть — временные файлы, треть — база сессий, а в кэше плагинов каждый пакет лежит дважды.
Обзор
Скрипт opencode_disk_report.py — самодостаточная утилита на чистом Python 3 без внешних зависимостей. Она вызывает opencode debug paths, чтобы узнать актуальные пути, обходит их рекурсивно и печатает отчёт с гистограммами.
| Параметр | Значение |
|---|---|
| Файл | opencode_disk_report.py |
| Язык | Python 3.9 и выше, только стандартная библиотека |
| Платформа | macOS (проверено на Darwin/arm64), Linux |
| Режим | только чтение, ничего не изменяет |
| Ключи | --top, --depth, --sessions, --json |
| ОС | macOS 15.x, openCode 1.18.32 |
| Данные актуальны | 27.09.2026 |
Замер сделан на macOS с openCode версии 1.18.32. Абсолютные цифры зависят от числа сессий и установленных плагинов, но пропорции у типичной установки повторяются.
Где openCode хранит файлы
Шесть корней, которые стоит учитывать при разборе. Первые пять возвращает opencode debug paths, шестой (~/.opencode) в выводе отсутствует, хотя там лежит бинарь и node_modules плагинов.
| Корневой каталог | Что внутри |
|---|---|
| ~/.local/share/opencode | база opencode.db, логи, tool-output, storage, snapshot |
| ~/.cache/opencode | кэш пакетов плагинов, models.json, ripgrep |
| ~/.config/opencode | конфиг, плагины, skill, инструменты |
| ~/.opencode | бинарь opencode и node_modules |
| ~/.local/state/opencode | состояние (обычно почти пусто) |
| /tmp/opencode | временные файлы рабочих черновиков |
ℹ️ Информация
На macOS временный каталог /tmp/opencode легко обгоняет по размеру саму базу данных — в замере это 776 МБ из 2,9 ГБ.
Проверить пути и то, что uninstall собирается удалять, можно без всякого скрипта:
opencode debug paths opencode uninstall --dry-run opencode db path
Быстрая прикидка по шести корням делается штатным du:
du -sh ~/.local/share/opencode ~/.cache/opencode ~/.opencode \
~/.config/opencode ~/.local/state/opencode /tmp/opencode
Скрипт opencode_disk_report.py
Полный текст утилиты. Запуск — обычный python3, ключи необязательны.
#!/usr/bin/env python3
"""Полная разбивка дискового пространства, занимаемого opencode.
Только чтение: ничего не удаляет, не вакуумит, не меняет.
Использование:
python3 opencode_disk_report.py
python3 opencode_disk_report.py --top 40 --depth 3
python3 opencode_disk_report.py --json # машинный вывод
"""
from __future__ import annotations
import argparse
import json
import os
import sqlite3
import subprocess
import sys
import time
from collections import defaultdict
UNITS = ["B", "KB", "MB", "GB", "TB"]
def human(n: float) -> str:
n = float(n)
for u in UNITS:
if abs(n) < 1024 or u == UNITS[-1]:
if u == "B":
return f"{int(n)} B"
return f"{n:.1f} {u}"
n /= 1024
return f"{n:.1f} TB"
def dir_size(path: str) -> tuple[int, int]:
"""(allocated_bytes, apparent_bytes) для каталога. allocated = реально на диске."""
allocated = 0
apparent = 0
stack = [path]
while stack:
cur = stack.pop()
try:
with os.scandir(cur) as it:
for e in it:
try:
st = e.stat(follow_symlinks=False)
except OSError:
continue
if e.is_dir(follow_symlinks=False):
stack.append(e.path)
allocated += st.st_blocks * 512
apparent += st.st_size
except (OSError, PermissionError):
continue
return allocated, apparent
def opencode_paths() -> dict[str, str]:
"""Пути от `opencode debug paths`, с фолбэком на дефолты XDG."""
home = os.path.expanduser("~")
fallback = {
"home": home,
"data": os.path.join(home, ".local/share/opencode"),
"cache": os.path.join(home, ".cache/opencode"),
"config": os.path.join(home, ".config/opencode"),
"state": os.path.join(home, ".local/state/opencode"),
"tmp": "/tmp/opencode",
"log": os.path.join(home, ".local/share/opencode/log"),
}
try:
out = subprocess.run(
["opencode", "debug", "paths"],
capture_output=True, text=True, timeout=20,
).stdout
parsed = dict()
for line in out.splitlines():
parts = line.split()
if len(parts) >= 2:
parsed[parts[0]] = parts[1]
if parsed:
fallback.update(parsed)
except (OSError, subprocess.SubprocessError):
pass
# каталог самого бинаря/плагинов: ~/.opencode (в выводе opencode его нет)
fallback.setdefault("install", os.path.join(home, ".opencode"))
return fallback
def breakdown(root: str, depth: int, top: int) -> list[dict]:
"""Список (path, allocated, apparent) по подкаталогам, до уровня depth."""
root_alloc, root_app = dir_size(root)
rows: list[dict] = list()
def walk(path: str, level: int) -> None:
try:
entries = sorted(os.scandir(path), key=lambda e: e.name)
except (OSError, PermissionError):
return
subs: list[tuple[str, int, int]] = list()
files_alloc = files_app = 0
nfiles = 0
for e in entries:
try:
st = e.stat(follow_symlinks=False)
except OSError:
continue
if e.is_dir(follow_symlinks=False):
a, ap = dir_size(e.path)
subs.append((e.path, a, ap))
else:
files_alloc += st.st_blocks * 512
files_app += st.st_size
nfiles += 1
for sp, a, ap in sorted(subs, key=lambda x: -x[1]):
rows.append({
"path": sp, "level": level,
"allocated": a, "apparent": ap,
"share": a / root_alloc if root_alloc else 0,
})
if level < depth:
walk(sp, level + 1)
if nfiles:
rows.append({
"path": path + "/*.files", "level": level,
"allocated": files_alloc, "apparent": files_app,
"share": files_alloc / root_alloc if root_alloc else 0,
"files": nfiles,
})
walk(root, 1)
rows.sort(key=lambda r: -r["allocated"])
return rows[:top] if top else rows
def biggest_files(roots: list[str], top: int) -> list[dict]:
out = list()
for root in roots:
for dirpath, _dirnames, filenames in os.walk(root, onerror=lambda _: None):
for fn in filenames:
fp = os.path.join(dirpath, fn)
try:
st = os.lstat(fp)
except OSError:
continue
out.append({
"path": fp,
"allocated": st.st_blocks * 512,
"apparent": st.st_size,
"mtime": st.st_mtime,
})
out.sort(key=lambda r: -r["allocated"])
return out[:top]
def connect_ro(db_path: str):
"""Read-only подключение. Если БД занята — читаем через временную копию."""
try:
con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=5)
con.execute("pragma busy_timeout=3000")
con.execute("select count(*) from sqlite_master")
return con, "прямое чтение"
except sqlite3.Error:
pass
import shutil
import tempfile
tmp = tempfile.mkdtemp(prefix="oc-disk-ro-")
copy = os.path.join(tmp, "db.sqlite")
shutil.copy2(db_path, copy)
for suffix in ("-wal", "-shm"):
side = db_path + suffix
if os.path.exists(side):
shutil.copy2(side, copy + suffix)
con = sqlite3.connect(f"file:{copy}?mode=ro", uri=True)
return con, "через временную копию (БД была занята)"
def db_report(db_path: str) -> dict | None:
if not os.path.exists(db_path):
return None
res: dict = {"path": db_path}
st = os.stat(db_path)
res["allocated"] = st.st_blocks * 512
res["apparent"] = st.st_size
try:
con, how = connect_ro(db_path)
except Exception as e: # noqa: BLE001
res["error"] = str(e)
return res
res["read_mode"] = how
q = lambda sql, args=(): con.execute(sql, args).fetchall() # noqa: E731
def one(sql):
try:
r = q(sql)
return r[0][0] if r and r[0] else None
except sqlite3.Error:
return None
try:
page_size = one("pragma page_size") or 4096
page_count = one("pragma page_count") or 0
free = one("pragma freelist_count") or 0
res["page_size"] = page_size
res["page_count"] = page_count
res["freelist_count"] = free
res["logical_bytes"] = page_size * page_count
res["free_bytes"] = page_size * free
res["journal_mode"] = one("pragma journal_mode")
res["tables"] = [
{"name": n, "bytes": b, "rows": one(f'select count(*) from "{n}"') or 0}
for n, b in q(
"select name, sum(pgsize) from dbstat group by name order by 2 desc limit 20"
)
]
res["row_counts"] = {
t: one(f'select count(*) from "{t}"')
for t in ("session", "message", "part", "event", "project", "todo",
"session_share", "credential", "permission")
}
res["sessions_by_dir"] = [
{"directory": d or "?", "sessions": s, "bytes": b}
for d, s, b in q(
"""select directory, count(distinct s.id), coalesce(sum(length(p.data)),0)
from session s left join part p on p.session_id = s.id
group by directory order by 3 desc limit 15"""
)
]
res["top_sessions"] = [
{
"id": sid, "title": (title or "")[:70],
"directory": (d or "")[:48],
"parts": n, "bytes": b,
"created": tc,
}
for sid, title, d, n, b, tc in q(
"""select s.id, s.title, s.directory, count(p.id),
coalesce(sum(length(p.data)),0), s.time_created
from session s join part p on p.session_id = s.id
group by s.id order by 5 desc limit 20"""
)
]
res["part_types"] = [
{"type": t, "count": c, "bytes": b}
for t, c, b in q(
"""select json_extract(data,'$.type'), count(*), sum(length(data))
from part group by 1 order by 3 desc limit 15"""
)
]
oldest = one("select min(time_created) from session")
newest = one("select max(time_created) from session")
if oldest and newest:
res["sessions_range"] = {
"oldest": time.strftime("%Y-%m-%d", time.localtime(oldest / 1000)),
"newest": time.strftime("%Y-%m-%d", time.localtime(newest / 1000)),
}
res["archived_sessions"] = one(
"select count(*) from session where time_archived is not null"
)
con.close()
except sqlite3.Error as e:
res["error"] = str(e)
return res
def logs_report(log_dir: str) -> dict | None:
if not os.path.isdir(log_dir):
return None
files = list()
for fn in os.listdir(log_dir):
fp = os.path.join(log_dir, fn)
try:
st = os.stat(fp)
except OSError:
continue
files.append({"path": fp, "allocated": st.st_blocks * 512,
"mtime": st.st_mtime, "apparent": st.st_size})
if not files:
return {"dir": log_dir, "count": 0, "allocated": 0, "files": list()}
files.sort(key=lambda f: -f["allocated"])
return {
"dir": log_dir,
"count": len(files),
"allocated": sum(f["allocated"] for f in files),
"oldest": time.strftime("%Y-%m-%d", time.localtime(min(f["mtime"] for f in files))),
"newest": time.strftime("%Y-%m-%d", time.localtime(max(f["mtime"] for f in files))),
"files": files[:10],
}
def bar(frac: float, width: int = 28) -> str:
frac = max(0.0, min(1.0, frac or 0.0))
n = int(round(frac * width))
return "█" * n + "·" * (width - n)
def h(title: str) -> None:
print(f"\033[1m{title}\033[0m\n" + "─" * 78)
def main() -> int:
ap = argparse.ArgumentParser(description="Разбивка места, занимаемого opencode")
ap.add_argument("--top", type=int, default=25, help="сколько строк в таблицах")
ap.add_argument("--depth", type=int, default=2, help="глубина разбора каталогов")
ap.add_argument("--json", action="store_true", help="вывод в JSON")
ap.add_argument("--sessions", type=int, default=15, help="сколько топ-сессий показать")
args = ap.parse_args()
paths = opencode_paths()
roots = ["data", "cache", "install", "config", "state", "tmp"]
root_sizes = dict()
for key in roots:
p = paths.get(key)
if p and os.path.exists(p):
a, ap_ = dir_size(p)
root_sizes[key] = {"path": p, "allocated": a, "apparent": ap_}
total_alloc = sum(r["allocated"] for r in root_sizes.values())
data = {
"generated": time.strftime("%Y-%m-%d %H:%M:%S"),
"paths": paths,
"roots": root_sizes,
"total_allocated": total_alloc,
"total_apparent": sum(r["apparent"] for r in root_sizes.values()),
}
for key, depth in (("data", args.depth), ("cache", args.depth), ("install", args.depth)):
if key in root_sizes:
data[f"breakdown_{key}"] = breakdown(root_sizes[key]["path"], depth, args.top)
data["biggest_files"] = biggest_files(
[root_sizes[k]["path"] for k in ("data", "cache", "install")], args.top
)
data["logs"] = logs_report(paths.get("log", ""))
data["db"] = db_report(
os.path.join(paths.get("data", ""), "opencode.db")
if paths.get("data") else ""
)
if args.json:
print(json.dumps(data, indent=2, ensure_ascii=False, default=str))
return 0
print("\033[1m╔══ opencode — разбивка по диску ══╗\033[0m")
print(f"сгенерировано: {data['generated']} (режим: только чтение)")
h("1. ИТОГО ПО КОРНЯМ")
for key in roots:
if key not in root_sizes:
continue
r = root_sizes[key]
print(f"{key:9s} {human(r['allocated']):>10s} {bar(r['allocated']/total_alloc if total_alloc else 0, 22)} "
f"{r['allocated']/total_alloc*100 if total_alloc else 0:5.1f}% {r['path']}")
print(f"{'ВСЕГО':9s} {human(data['total_allocated']):>10s} "
f"(apparent: {human(data['total_apparent'])})")
for key, label in (("data", "2. КАТАЛОГ ДАННЫХ"),
("cache", "3. КЭШ"),
("install", "4. УСТАНОВКА И ПЛАГИНЫ (~/.opencode)")):
rows = data.get(f"breakdown_{key}")
if not rows:
continue
h(label)
for r in rows:
ind = " " * (r["level"] - 1)
extra = f" ({r['files']} файлов)" if r.get("files") else ""
print(f"{human(r['allocated']):>10s} {bar(r['share'], 18)} {r['share']*100:5.1f}% "
f"{ind}{os.path.basename(r['path'])}{extra}")
lg = data.get("logs")
if lg and lg.get("count"):
h("5. ЛОГИ")
print(f"каталог: {lg['dir']}")
print(f"файлов: {lg['count']} всего: {human(lg['allocated'])} "
f"период: {lg['oldest']} … {lg['newest']}")
for f in lg["files"]:
print(f" {human(f['allocated']):>9s} "
f"{time.strftime('%Y-%m-%d %H:%M', time.localtime(f['mtime']))} "
f"{os.path.basename(f['path'])}")
d = data.get("db")
if d:
h("6. БАЗА ДАННЫХ (SQLite)")
print(f"файл: {d['path']}")
print(f"на диске:{human(d['allocated']):>10s} "
f"логический размер: {human(d.get('logical_bytes', 0))} "
f"свободных страниц: {d.get('free_bytes', 0)}")
print(f"страниц: {d.get('page_count')} × {d.get('page_size')} B "
f"journal_mode: {d.get('journal_mode')} чтение: {d.get('read_mode')}")
if d.get("error"):
print(f"ОШИБКА: {d['error']}")
if d.get("row_counts"):
print("\nстроки в таблицах:")
for t, c in d["row_counts"].items():
if c:
print(f" {t:16s} {c:>10,}")
if d.get("sessions_range"):
r = d["sessions_range"]
print(f"\nсессии: {d['row_counts'].get('session', 0)} шт. "
f"({r['oldest']} … {r['newest']}), архивных: {d.get('archived_sessions', 0)}")
if d.get("tables"):
print("\nразмер по таблицам/индексам (dbstat):")
for t in d["tables"]:
print(f" {human(t['bytes']):>10s} {t['name']}")
if d.get("part_types"):
print("\nтипы частей сообщений (part.data):")
for p in d["part_types"]:
print(f" {human(p['bytes']):>10s} {p['type']} ({p['count']} шт.)")
if d.get("sessions_by_dir"):
print("\nпо каталогам проектов:")
for s in d["sessions_by_dir"]:
print(f" {human(s['bytes']):>10s} {s['sessions']:>4d} сессий {s['directory']}")
if d.get("top_sessions"):
print(f"\nтоп-{min(args.sessions, len(d['top_sessions']))} самых тяжёлых сессий:")
for s in d["top_sessions"][: args.sessions]:
created = time.strftime("%Y-%m-%d", time.localtime(s["created"] / 1000)) if s["created"] else "?"
print(f" {human(s['bytes']):>10s} {s['parts']:>5d} част. {created} {s['title'] or '(без названия)'}")
bf = data.get("biggest_files")
if bf:
h("7. САМЫЕ КРУПНЫЕ ФАЙЛЫ")
for f in bf:
print(f" {human(f['allocated']):>10s} "
f"{time.strftime('%Y-%m-%d', time.localtime(f['mtime']))} {f['path']}")
h("ГОТОВО")
print("Скрипт ничего не удалял и не изменял. Если нужно — скажите, что почистить.")
return 0
if __name__ == "__main__":
sys.exit(main())
Запуск и полезные ключи:
python3 ~/.config/opencode/scripts/opencode_disk_report.py python3 ~/.config/opencode/scripts/opencode_disk_report.py --top 40 --depth 3 python3 ~/.config/opencode/scripts/opencode_disk_report.py --sessions 25 python3 ~/.config/opencode/scripts/opencode_disk_report.py --json > oc-disk.json
Ключи отчёта:
- --top N — сколько строк в каждой таблице (по умолчанию 25);
- --depth N — глубина рекурсивного разбора каталогов (по умолчанию 2);
- --sessions N — сколько самых тяжёлых сессий показать;
- --json — машинный вывод со всеми числами, пригоден для сравнения двух запусков.
Код возврата всегда 0: скрипт ничего не удаляет, поэтому ломать ему не на чем.
Как считается размер
Ключевая деталь, из-за которой du и скрипт могут давать разные цифры: размер файла (st_size) и реально занятые на диске блоки (st_blocks * 512) — разные вещи. Скрипт считает именно блоки, потому что вопрос «сколько места занято» про блоки, а не про логическую длину. Оба значения выводятся рядом — «на диске» и «apparent».
Каталоги обходятся итеративно, стеком, через os.scandir, без рекурсии Python — это снимает ограничение на глубину вложенности в node_modules. Симлинки не разыменовываются (follow_symlinks=False), чтобы не зациклиться и не завысить счёт.
| Метрика | Откуда берётся | Зачем |
|---|---|---|
| Размер на диске | os.stat_result.st_blocks × 512 !! сколько реально занято | |
| Apparent-размер | os.stat_result.st_size !! логическая длина | |
| Пропорция в баре | доля от размера корня !! быстрый визуальный поиск жирного | |
| Страницы SQLite | pragma page_count, page_size, freelist_count !! сколько занято и сколько свободно |
Разбор базы SQLite
База opencode.db — самый тяжёлый артефакт, и одна только цифра размера файла ничего не говорит о том, что внутри. Виртуальная таблица dbstat возвращает статистику по каждой btree-странице, поэтому можно точно сказать, какая таблица и какой индекс сколько занимают.
Скрипт открывает базу в режиме только для чтения через URI file:...?mode=ro. Если база занята работающим openCode, скрипт копирует её вместе с -wal и -shm во временный каталог и читает уже оттуда — так отчёт не блокирует и не ломает живой процесс.
Что показывает отчёт по базе:
- размер файла, логический размер и число свободных страниц;
- страниц × размер страницы и режим журналирования;
- строки в таблицах session, message, part, event, project;
- период жизни сессий и число архивных;
- разбивка по таблицам и индексам через
dbstat; - разбивка
part.dataпо типам частей сообщений; - объём по каталогам проектов и топ самых тяжёлых сессий.
Типы частей сообщений считаются через JSON-функцию SQLite — json_extract(data,'$.type'). Это и есть главный ответ на вопрос «что именно раздувает базу».
Если нужно лишь посмотреть структуру, не запуская скрипт:
opencode db "select name from sqlite_master where type='table'" opencode db "select name, sum(pgsize) from dbstat group by name order by 2 desc limit 10" opencode db "pragma page_count" opencode db "pragma freelist_count"
Разбор реального замера
Ниже — сокращённый вывод скрипта на macOS, openCode 1.18.32, 27.09.2026. Общий объём по шести корням — 2,9 ГБ.
| Корень | Размер | Доля |
|---|---|---|
| /tmp/opencode | 776,2 МБ | 26,5 % |
| ~/.local/share/opencode | 1,0 ГБ | 35,3 % |
| ~/.cache/opencode | 675,7 МБ | 23,0 % |
| ~/.config/opencode | 250,0 МБ | 8,5 % |
| ~/.opencode | 194,7 МБ | 6,6 % |
| ~/.local/state/opencode | 8,0 КБ | 0,0 % |
Внутри каталога данных практически всё место — это база: 976,0 МБ из 1,0 ГБ. Логи занимают 29,4 МБ, каталог tool-output — 26,1 МБ, storage и snapshot вместе меньше мегабайта.
Разбивка базы по таблицам и индексам:
| Таблица или индекс | Размер |
|---|---|
| event | 594,7 МБ |
| part | 286,5 МБ |
| event_aggregate_type_seq_idx | 22,2 МБ |
| event_aggregate_seq_idx | 14,4 МБ |
| sqlite_autoindex_event_1 | 12,5 МБ |
| message | 10,6 МБ |
| остальные индексы part и message | 15,4 МБ |
| session и её индексы | 0,4 МБ |
Строки: 1 154 сессии, 24 623 сообщения, 91 515 частей, 295 182 события. Период сессий — с 19.05.2026 по 27.09.2026, архивных нет ни одной.
По типам частей сообщений видно, из чего именно собраны 286 МБ таблицы part:
| Тип части | Размер | Количество |
|---|---|---|
| tool (вывод инструментов) | 174,4 МБ | 22 523 |
| file (вложенные файлы) | 21,3 МБ | 32 |
| reasoning (размышления модели) | 20,2 МБ | 14 900 |
| text (текст ответа) | 9,6 МБ | 13 391 |
| step-finish | 2,9 МБ | 20 176 |
| step-start | 417,5 КБ | 20 343 |
Самая большая единица — вывод инструментов: 174 МБ на 22 тысячи вызовов. Восемь мегабайт в одном файле tool-output и по 3 МБ на фрагмент в кэше плагинов — это следы одних и тех же выводов.
Сравнение способов разбора
Встроенных средств почти нет, поэтому инструменты приходится комбинировать. У каждого своя сильная сторона и своя слепая зона.
| Инструмент | Что показывает | Чего не показывает |
|---|---|---|
| opencode stats | токены, стоимость, число вызовов | байты на диске вовсе |
| opencode debug paths | только пути, без размеров !! вообще ничего о весе | |
| opencode uninstall --dry-run | что будет удалено при сносе | что именно внутри и кто виноват |
| du -sh по шести корням | верхний уровень | что внутри базы и в кэше |
| sqlite3 и dbstat | точные байты по таблицам | размеры каталогов и логов |
| opencode_disk_report.py | всё вышеперечисленное вместе !! ничего: работает на чтение | |
| Finder «Свойства» | размер папки глазами с навигацией вручную !! воспроизводимость и повторяемость |
FAQ
Зачем считать блоки, а не размер файла?
Файлы базы и кэш плагинов состоят в основном из длинных записей. st_size показывает логическую длину, а st_blocks * 512 — реально выделенные блоки с учётом округления до границы. Для ответа на вопрос «сколько занимает» нужен второй показатель.
Скрипт безопасен во время работы openCode?
Да. База открывается через file:...?mode=ro, а при блокировке читается из копии во временном каталоге. Каталоги обходятся только на чтение, ничего не удаляется и не модифицируется.
Почему один и тот же плагин показан дважды в кэше?
В ~/.cache/opencode/packages лежат и opencode-ops, и opencode-ops@latest — по 124 МБ каждый. Плюс четыре копии bun по ~60 МБ. В сумме на дубли уходит около четверти кэша.
Чем отличается freelist_count от размера файла?
freelist_count — число страниц, освобождённых после удаления строк, но не выданных файлу. Эти страницы лежат в файле и занимают место. В замере их было 0, то есть база не «раздута» дырками, а растёт честно.
Что делать, если нужно освободить место?
Сначала посмотрите раздел с типами частей сообщений: если раздувание даёт tool, помогает регулярная чистка старых сессий через opencode session delete. Если виноваты packages с дублями @latest, лишние копии можно удалить вручную. Сам скрипт ничего не удаляет.
Итог
У openCode нет встроенного отчёта по диску, но задача решается связкой из opencode debug paths для путей, du для верхнего уровня и dbstat для содержимого базы. Скрипт opencode_disk_report.py объединяет всё это в один отчёт, который можно запускать хоть вручную, хоть по расписанию, и сравнивать запуски в JSON.
На реальной установке картина обычно такая: база сессий растёт монотонно и доминирует, кэш плагинов содержит дубли, а временный каталог незаметно становится третьей по величине статьёй расходов.
Источники
- официальная документация openCode CLI — команды debug, db, stats, uninstall
- исходный код openCode на GitHub
- конфигурация openCode и переменные окружения
- DBSTAT Virtual Table — официальная документация SQLite
- команда VACUUM в SQLite
- модуль os в Python 3 — os.scandir и os.stat_result
- модуль sqlite3 в Python 3
- модуль shutil в Python 3 — копирование файлов
- man-страница du из macOS
