Память и чекпоинты
Два персистентных хранилища: memory — факты о пользователе и проекте, загружаемые в каждый новый контекст сессии; checkpoint — git-снапшоты рабочего дерева для отката изменений.
Память (Memory)
File-based key-value хранилище фактов для самообучающегося цикла агента. Факты загружаются в системный промпт каждой новой сессии, позволяя агенту «помнить» предпочтения и договорённости между сессиями.
Stores
Два независимых scope, каждый — отдельный файл:
| Scope | Путь | Назначение |
|---|---|---|
profile |
~/.config/creo/memory/profile |
Факты о пользователе (предпочтения, привычки, конвенции) |
environment |
<cwd>/.creo/memory/environment |
Факты о проектном окружении (tooling, структура, соглашения) |
Каждый scope — независимый инстанс Store, создаваемый через NewStore(scope, path, maxTokens).
Формат хранения
Построчно, один файл на scope:
indent_style: tabs
go_version: 1.24
use_tools_directly: Пользователь раздражается когда пишу тестовые файлы
Формат: key: value\n. Валидация:
- Key: только
[a-z0-9_]+— regex^[a-z0-9_]+$ - Value: одна строка (без
\n)
Парсинг: strings.Cut(line, ":"), trim пробелов, пропуск пустых строк и строк без :.
Если файл не существует — Load() возвращает nil, nil (не ошибка).
Token caps
Ограничение объёма фактов в системном промпте:
1func EstimateTokens(text string) int {
2 n := len(text) / 4 // 4 символа на токен — консервативная оценка
3 if n == 0 { n = 1 }
4 return n
5}
При каждом Set() после добавления записи вычисляется usedTokens = entriesTokens(entries).
Если used > cap — возврат CapOverError без записи:
1type CapOverError struct {
2 Store Scope
3 Entries []Entry // текущее содержимое (для решения об eviction)
4 UsedTokens int
5 Cap int
6}
CapOverError несёт текущие entries — вызывающая сторона может решить, какие факты удалить.
Per-file mutexes
Глобальная sync.Map мьютексов для сериализации доступа к одному файлу:
1var pathLocks sync.Map
2
3func pathMutex(path string) *sync.Mutex {
4 actual, _ := pathLocks.LoadOrStore(path, &sync.Mutex{})
5 return actual.(*sync.Mutex)
6}
Позволяет сериализовать доступ из разных горутин (main-агент + dream reviewer одновременно).
Разные файлы (profile vs environment) не блокируют друг друга.
API
| Метод | Сигнатура | Поведение |
|---|---|---|
NewStore |
NewStore(scope, path, maxTokens) *Store |
Конструктор |
Load |
Load() ([]Entry, error) |
Читает файл, парсит, возвращает entries |
Set |
Set(key, value string) error |
Add or replace. CapOverError при превышении |
Delete |
Delete(key string) error |
Удаляет по ключу. Ошибка если не найден |
List |
List() (entries []Entry, used int, err error) |
Entries + текущее потребление токенов |
Cap |
Cap() int |
Token cap |
Загрузка в системный промпт
Пакет memory — storage-only. Инъекция в промпт — ответственность вызывающей стороны:
Load()→ получение[]Entry- Сериализация в текст
- Подстановка в system prompt template (
internal/prompts/system.gotmpl)
Чекпоинты (Checkpoint)
Bare-git-repo-backed snapshot store. Каждое изменение файлов агентом автоматически снимается как git-снапшот. Откат в один клик — safety net для YOLO-режима.
Bare git repo архитектура
project/
└── .creo/
├── .gitignore # содержит "*" — всё внутри .creo игнорируется
└── checkpoints/ # bare git repository (без working tree)
├── HEAD # symbolic ref → refs/heads/main
└── objects/ # blob, tree, commit objects
Bare repository (gogit.PlainInit(path, true)) — репозиторий без working directory.
Объекты хранятся напрямую в objects/, нет index и worktree. Преимущества:
- Хранение snapshots без подкаталогов в рабочей директории
- Работа через go-git API без конфликтов с пользовательским
.git
HEAD → refs/heads/main через symbolic reference.
.creo/.gitignore с "*\n" — защита от попадания в пользовательский git.
Создание чекпоинта
flowchart TD
A[Store.Create] --> B[createLocked — mutex]
B --> C[collectFiles — walk project dir]
C --> D[Исключить: .git, .creo, node_modules, vendor, ...]
D --> E[Каждый файл → blob → Storer.SetEncodedObject]
E --> F[writeTree — построение nested tree objects]
F --> G[commit — parent=HEAD, message=JSON metadata]
G --> H[SetReference — refs/heads/main → new hash]
H --> I[Checkpoint added to s.items]
Шаг 1 — Сбор файлов (collectFiles)
filepath.WalkDir обходит рабочую директорию:
- Пропуск excluded dirs:
.git,.creo,node_modules,vendor,build,dist,bin,.next,__pycache__,target,obj,.gradle - Symlinks: читает target path как blob content
- Regular files: читает содержимое
- Executable:
mode = filemode.Executable - Каждый файл →
blob→Storer.SetEncodedObject→ hash
Результат: map[string]blobEntry{path → {hash, mode}}
Шаг 2 — Построение дерева (writeTree)
Из flat path-to-blobEntry map рекурсивно строятся nested tree objects:
- Промежуточный тип
tnodeсentries []TreeEntryиchildren map[string]*tnode - Рекурсивный
build(): сначала children → subtree hashes → entries текущего узла - Сортировка entries: директории как будто имеют trailing slash (git convention)
- Возвращает hash корневого tree object
Шаг 3 — Коммит
commitMeta(JSON в message)- Signature:
Name: "Creo", Email: "creo@checkpoint" - Parent: текущий HEAD (если существует) — цепочка коммитов
object.Commit→ encode →Storer.SetEncodedObject→ hashSetReference→ обновлениеrefs/heads/main
Metadata в commit message
Каждый checkpoint commit имеет message в формате однострочного JSON:
1{"turn":7,"trigger_tool":"write","trigger_args":"main.go","pre_rollback":false}
1type commitMeta struct {
2 Turn int `json:"turn"`
3 TriggerTool string `json:"trigger_tool"`
4 TriggerArgs string `json:"trigger_args"`
5 PreRollback bool `json:"pre_rollback"`
6}
| Поле | Описание |
|---|---|
turn |
Номер хода агента |
trigger_tool |
Инструмент, вызвавший чекпоинт (write, edit, terminal, rollback) |
trigger_args |
Краткое описание аргументов (путь файла, команда) |
pre_rollback |
true для auto-checkpoint перед откатом |
Restore процесс
sequenceDiagram
participant U as User/Agent
participant S as Store
participant R as Bare Repo
participant FS as Filesystem
U->>S: Restore(idOrPrefix)
S->>S: Lookup checkpoint
S->>S: Pre-rollback checkpoint (createLocked, preRollback=true)
S->>R: Get tree by checkpoint hash
S->>FS: cleanDir — удалить всё кроме .git и .creo
S->>FS: extractTree — распаковать файлы из tree
S->>S: Return restored checkpoint
Pre-rollback checkpoint — перед восстановлением автоматически сохраняется текущее состояние. Это позволяет откатить откат.
cleanDir — удаляет всё кроме .git и .creo. Если файл был в snapshot — он будет
восстановлен. Если файла не было — он будет удалён.
extractTree — для каждого файла в tree:
- Создаёт родительские директории
- Symlink:
os.Symlink(target, path) - Executable: mode
0o755 - Regular: mode
0o644
Store lifecycle
| Операция | Метод | Описание |
|---|---|---|
| Init | checkpoint.Init(projectDir) |
Создаёт .creo/, .gitignore, bare repo. Idempotent |
| Open | checkpoint.NewStore(projectDir) |
Открывает/создаёт repo, loadCommits() |
| Create | Store.Create(tool, args, turn) |
Snapshot → commit. Захватывает mutex |
| List | Store.List() |
[]Checkpoint, отсортированы newest-first |
| Lookup | Store.Lookup(idOrPrefix) |
Полный hash или prefix (≥8 chars) |
| Count | Store.Count() |
Количество чекпоинтов |
| Restore | Store.Restore(idOrPrefix) |
Pre-rollback → clean → extract |
loadCommits итерирует repo.Log от HEAD → все commits, парсит commitMeta,
сортировка sortNewestFirst.
Checkpoint struct
1type Checkpoint struct {
2 Hash plumbing.Hash
3 CreatedAt time.Time
4 TriggerTool string
5 TriggerArgs string
6 Turn int
7 PreRollback bool
8}
ID()→ full hex hashShortID()→ 8-char prefix
Файлы
Memory
| Файл | Назначение |
|---|---|
internal/memory/store.go |
Store, NewStore, Load, Set, Delete, List, pathMutex |
internal/memory/tokens.go |
EstimateTokens, CapOverError |
Checkpoint
| Файл | Назначение |
|---|---|
internal/checkpoint/checkpoint.go |
Checkpoint struct, commitMeta, Init |
internal/checkpoint/store.go |
Store, NewStore, Create, List, Count, Lookup |
internal/checkpoint/repo.go |
openOrInitRepo, loadCommits, setMainHead |
internal/checkpoint/tree.go |
collectFiles, writeTree — сбор и построение дерева |
internal/checkpoint/restore.go |
Restore, cleanDir, extractTree |