OpenCode - mac os x - полный расклад по занятому диску - shell - 2026

Материал из Wiki - Iphoster - the best ever hosting and support. 2005 - 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.

На реальной установке картина обычно такая: база сессий растёт монотонно и доминирует, кэш плагинов содержит дубли, а временный каталог незаметно становится третьей по величине статьёй расходов.

Источники

×
Реклама
← В главную
ИКС 88x31 - Visitor counter