Слепки в историю по Save/по простою (вместо эвристики) + матчинг с агентами + share сохранённых версий #247

Open
opened 2026-06-28 03:56:28 +03:00 by Ghost · 0 comments

Постановка

  1. Перестать писать слепок в историю «эвристикой как сейчас», а писать его: (а) по явному Save и (б) по прошествии времени без изменений (например, 1 час).
  2. Свести это с агентами: у них тоже должны быть собственные сохранения в историю, и правки агента в рамках одной рабочей сессии тоже надо фиксировать.
  3. Дополнительно проработать вариант «share только сохранённых версий» и оценить сложность.

Как это устроено сейчас

Два независимых слоя: «живой контент» и «слепок»

Ключевой момент, на котором держится безопасность изменения — их нельзя путать:

  • Живой контентpages.content (ProseMirror JSON) + pages.ydoc (бинарь Yjs). Пишется автосейвом Hocuspocus: в apps/server/src/collaboration/collaboration.gateway.ts:48-51 задан debounce: 10000 / maxDebounce: 45000. Это слой долговечности и real-time-коллаборации — его трогать не нужно и опасно.
  • Слепок — строка в таблице page_history. Это «именованная точка восстановления». Именно правило её создания мы меняем.

Текущая «эвристика» создания слепка

После каждого onStoreDocument ставится отложенная задача BullMQ PAGE_HISTORY, задержка считается чистой функцией computeHistoryJob в apps/server/src/collaboration/extensions/persistence.extension.ts:74-88:

  • человек: delay = возраст_страницы < 5 мин ? 1 мин : 5 мин, jobId = pageId. Константы — apps/server/src/collaboration/constants.ts (HISTORY_INTERVAL=5м, HISTORY_FAST_INTERVAL=1м, HISTORY_FAST_THRESHOLD=5м).
  • агент: delay = 0, jobId = "${pageId}-agent".

За счёт дедупликации задач BullMQ по jobId это даёт «временны́е корзины»: при непрерывном редактировании — примерно один слепок раз в 1–5 минут. Это и есть «эвристика»: история засоряется промежуточными автосейвами.

Финальный гейт — apps/server/src/collaboration/processors/history.processor.ts:67-70: задача перечитывает страницу и пишет слепок только если !isDeepStrictEqual(lastHistory.content, page.content). Сам insert — saveHistory() в apps/server/src/database/repos/page/page-history.repo.ts:67.

Boundary-слепок (инвариант, который надо сохранить)

В apps/server/src/collaboration/extensions/persistence.extension.ts:227-240: когда агент впервые пишет в страницу авторства человека, состоя��ие человека немедленно пиннится отдельным слепком (lastUpdatedSource='user') до перезаписи. Гарантия «всегда можно откатиться к тому, что написал человек, до первой правки агента». Симметричного перехода агент→человек сейчас нет.

Атрибуция агента

Источник правки неподделываемый: подписанный claim actor в collab-токене + флаг users.is_agent. Маркеры в БД: pages.last_updated_source ('user'|'agent'), pages.last_updated_ai_chat_id; копируются в page_history при saveHistory. Агентские правки (MCP) идут не через REST, а через тот же collab-слой (Yjs), см. mutatePageContent в packages/mcp/src/lib/collaboration.ts:545. Понятия «сессии» агента нет — каждый стор атрибутируется отдельно; при delay=0 это ~слепок на каждое изменение контента → шумно.

Клиент

Кнопки «Save» / «сохранить версию» нет — всё на автосейве. История только читается: POST /pages/history и POST /pages/history/info (apps/server/src/core/page/page.controller.ts:501-536); список — apps/client/src/features/page-history/components/history-list.tsx.

Share

shares (миграция apps/server/src/database/migrations/20250408T191830-shares.ts) не ссылается ни на какую версию — только page_id + флаги (include_sub_pages, search_indexing). Весь публичный показ проходит через единственную точку resolveReadableSharePage (apps/server/src/core/share/share.service.ts:159-189), которая берёт живой контент: pageRepo.findById(pageId, {includeContent:true}) (apps/server/src/core/share/share.service.ts:176). Дальше updatePublicAttachmentsprepareContentForShare работают над page.content. page_history в share-пути не используется нигде.


Проектируемое изменение слепков

Принцип: меняем только триггер создания строки page_history, слой живого контента (Hocuspocus debounce) не трогаем. Риск потери данных не растёт: текущее состояние всегда лежит в pages.

Триггер A — явный Save (рекомендуется: через collab-stateless)

Hocuspocus живёт в отдельном процессе (collab-main.ts) и держит свежий ydoc в памяти. Самый корректный путь (без гонок с 10-секундным debounce):

  1. Клиент по Cmd/Ctrl+S (перехват браузерного Save) или кнопке в тулбаре шлёт provider.sendStateless({type:'save-version'}).
  2. Новый обработчик onStateless в расширении collab берёт свежайший ydoc из памяти (TiptapTransformer.fromYdoc(document,'default')), пере-использует существующую логику стора (запись в pages), затем пишет строку page_history с kind='manual', и делает document.broadcastStateless({type:'version.saved'}) — у соавторов обновляется панель истории + тост.

Прецедент для stateless уже есть (document.broadcastStateless в persistence.extension.ts:264), sendStateless есть на клиентском провайдере.

Альтернатива (проще, но с гонкой): REST POST /pages/history/save, сн��мающий слепок из pages.content. Минус — может зафиксировать состояние, отстающее до ~10 c, если пользователь только что печатал. Подходит как MVP.

Триггер B — idle-flush (по простою, напр. 1 час)

Нужен trailing-edge debounce: слепок через IDLE_INTERVAL после последней правки. Дедупликация BullMQ по jobId так не умеет (держит время первой правки). Решение — на каждом onStoreDocument:

  1. удалить отложенную idle-задачу для страницы (если ещё не активна);
  2. поставить заново с delay = IDLE_INTERVAL (напр. 1 ч).

При активном редактировании задача всё время отодвигается (никаких слепков в середине сессии — ровно то, что просили), а через час тишины срабатывает один раз и фиксирует финал брошенной сессии. Обрабатывает её тот же HistoryProcessor с тем же гейтом isDeepStrictEqual.

Триггер C — boundary-слепки (сделать симметричными)

  • Оставляем user→agent (persistence.extension.ts:227-240).
  • Добавляем agent→user: когда человек начинает править после агентской сессии — пиннить финал агента слепком до перезаписи. Результат агента не потеряется, даже если агент не вызвал явный save.

Новая колонка page_history.kind

Добавить nullable kind enum('manual'|'idle'|'boundary'|'agent') (старые строки = null/legacy, обратная совместимость). Нужна для: (а) осмысленных меток в UI истории, (б) фичи «share сохранённых версий» — шарить только намеренные точки (manual/agent), а не idle-автосейвы.

Дедупликация

Все пути (явный save, idle, boundary) проходят общий гейт isDeepStrictEqual(lastHistory.content, page.content) — он не даст создать дубль. Уже есть в процессоре и boundary-логике; новые пути обязаны его использовать.


Матчинг с агентами

Делаем модель агента симметричной человеческой — это и есть «сведение»:

Человек Агент
Явный save Cmd/Ctrl+S / кнопка → stateless новый MCP-инструмент save_page_version (агент сам решает точки сохранения)
Idle-flush через IDLE_INTERVAL тишины тот же механизм (можно отдельный, более короткий интервал)
Boundary user→agent agent→user (новый) + сохраняем user→agent

«Сессия агента» = непрерывный батч правок одного aiChatId. Поскольку каждый MCP-инструмент — отдельный connect/mutate/disconnect, естественный терминатор сессии — idle-debounce (накопить правки, снять один слепок при затишье) плюс явный save_page_version для немедленной фиксации логического результата. Это закрывает «они хотят собственные сохранения» и «правки одной сессии надо сохранить»: вместо ~слепка-на-микроп��авку (текущее delay=0) — один осмысленный слепок на сессию, тег kind='agent' + aiChatId, плюс boundary-слепок человеческого базиса до начала сессии.

Из computeHistoryJob уходит ветка «agent → delay 0», агент встаёт в общий конвейер триггеров.


Доп. вариант: share только сохранённых версий — оценка сложности

Сложность: НИЗКАЯ–СРЕДНЯЯ. В share-пути ровно одна точка чтения контента (resolveReadableSharePage), поэтому ядро фичи крошечное.

Что нужно:

  1. Схема: добавить в shares nullable shared_history_id (FK→page_history.id). null = «живой» режим (дефолт, как сейчас), не-null = запиненная версия. Обратная совместимость полная.
  2. DTO/UI: в apps/client/src/features/share/components/share-modal.tsx — выбор «Делиться текущей версией» / «Делиться сохранённой версией» + пикер версий (реюз эндпоинта истории; ограничить выбор kind in ('manual','agent')).
  3. Резолв (1 ветка): в resolveReadableSharePage — если shared_history_id задан, брать контент/title/icon из pageHistoryRepo.findById(historyId, {includeContent:true}) вместо pages. Весь downstream (prepareContentForShare: токены вложений, вырезание комментов/htmlEmbed) работает над page.content без изменений. SEO-контроллер идёт через ту же точку → наследует автоматически.

Ограничения MVP (это и есть «вся сложность»):

  • includeSubPages: пин — одна строка истории одной страницы. Пиннить поддерево = «версия каждого ребёнка на момент T» — заметно сложнее (резолв «история-строка на/до timestamp» для каждого потомка). Рекомендация: в запиненном режиме запретить includeSubPages (только одиночная страница) в MVP.
  • Трансклюзии: сейчас берутся из live-кэша page_transclusions. В запиненной версии останутся «живыми» (мелкая несогласованность) — для MVP допустимо, задокументировать; «трансклюзии как на момент версии» — отдельная тяжёлая задача.
  • Вложения и токены работают как есть (история ссылается на те же attachment-id, файлы не версионируются).

Фича естественно ложится на основную: как только появляются именованные «сохранённые версии», «поделиться версией» = сослаться на одну из них.


Edge cases, инварианты, риски

  • Долговечность не меняется: Hocuspocus-автосейв pages/ydoc остаётся → между слепками данные не теряются. Меняется только частота «точек восстановления».
  • Trade-off (принять явно): при «save + idle(1ч)» окно отката между сохранениями шире, чем при автосейве каждые 1–5 мин. Idle(1ч) ограничивает худший случай (брошенная сессия зафиксируется). Это ровно то, что просили.
  • Гонка явного save с debounce — снимается, если идти через collab-stateless (свежий ydoc из памяти), а не REST.
  • idle remove+re-add: если задача уже активна и её нельзя удалить — добавляем новую; общий гейт isDeepStrictEqual защитит от дубля.
  • Кросс-процессность: collab-процесс уже кладёт задачи в historyQueue; idle-задачу обрабатывает существующий HistoryProcessor. Новых очередей не нужно.
  • Contributors: Redis-набор контрибьюторов поппится при слепке; при более редких слепках версия агрегирует всех, кто правил с прошлой версии, — корректнее.
  • Права: явный save требует edit-права; у агента право несёт его токен.
  • Миграция: старые page_history строки живут как есть; kind=null = legacy.

Объём работ (точечно, по файлам)

  • apps/server/src/collaboration/constants.ts — добавить IDLE_INTERVAL (+ опц. агентский), вывести из обихода *_FAST_*.
  • apps/server/src/collaboration/extensions/persistence.extension.ts — заменить computeHistoryJob на idle-debounce (remove+re-add), добавить onStateless('save-version'), добавить agent→user boundary.
  • apps/server/src/collaboration/processors/history.processor.ts — проставлять kind для idle/manual/boundary; гейт оставить.
  • apps/server/src/database/repos/page/page-history.repo.tssaveHistory принимает kind; новая миграция +kind.
  • apps/server/src/core/page/page.controller.ts — (если REST-вариант save) POST /pages/history/save.
  • Клиент — кнопка Save + Cmd/Ctrl+S + sendStateless; метки kind и обновление панели по version.saved в apps/client/src/features/page-history/components/history-list.tsx.
  • MCP — новый инструмент save_page_version в packages/mcp.
  • Share — миграция +shared_history_id в shares, ветка в apps/server/src/core/share/share.service.ts, поле в apps/server/src/core/share/dto/share.dto.ts, пикер в apps/client/src/features/share/components/share-modal.tsx.

Открытые продуктовые развилки (с рекомендациями)

  • Путь явного Save: рекомендую collab-stateless (корректность, единый путь записи, как у агентов); REST — более простой MVP.
  • Терминатор агентской сессии: idle-debounce + явный save_page_version + agent→user boundary (группировки по соединению нет).
  • Share-версии: MVP = одиночная страница, live-трансклюзии, выбор только из kind in (manual, agent); поддерево и версионные трансклюзии — отдельные тяжёлые итерации.
## Постановка 1. Перестать писать слепок в историю «эвристикой как сейчас», а писать его: **(а) по явному Save** и **(б) по прошествии времени без изменений** (например, 1 час). 2. Свести это с **агентами**: у них тоже должны быть собственные сохранения в историю, и правки агента в рамках одной рабочей сессии тоже надо фиксировать. 3. Дополнительно проработать вариант **«share только сохранённых версий»** и оценить сложность. --- ## Как это устроено сейчас ### Два независимых слоя: «живой контент» и «слепок» Ключевой момент, на котором держится безопасность изменения — их нельзя путать: - **Живой контент** — `pages.content` (ProseMirror JSON) + `pages.ydoc` (бинарь Yjs). Пишется автосейвом Hocuspocus: в `apps/server/src/collaboration/collaboration.gateway.ts:48-51` задан `debounce: 10000` / `maxDebounce: 45000`. Это слой долговечности и real-time-коллаборации — **его трогать не нужно и опасно**. - **Слепок** — строка в таблице `page_history`. Это «именованная точка восстановления». Именно правило её создания мы меняем. ### Текущая «эвристика» создания слепка После каждого `onStoreDocument` ставится отложенная задача BullMQ `PAGE_HISTORY`, задержка считается чистой функцией `computeHistoryJob` в `apps/server/src/collaboration/extensions/persistence.extension.ts:74-88`: - **человек:** `delay = возраст_страницы < 5 мин ? 1 мин : 5 мин`, `jobId = pageId`. Константы — `apps/server/src/collaboration/constants.ts` (`HISTORY_INTERVAL=5м`, `HISTORY_FAST_INTERVAL=1м`, `HISTORY_FAST_THRESHOLD=5м`). - **агент:** `delay = 0`, `jobId = "${pageId}-agent"`. За счёт дедупликации задач BullMQ по `jobId` это даёт **«временны́е корзины»**: при непрерывном редактировании — примерно один слепок раз в 1–5 минут. Это и есть «эвристика»: история засоряется промежуточными автосейвами. Финальный гейт — `apps/server/src/collaboration/processors/history.processor.ts:67-70`: задача перечитывает страницу и пишет слепок только если `!isDeepStrictEqual(lastHistory.content, page.content)`. Сам insert — `saveHistory()` в `apps/server/src/database/repos/page/page-history.repo.ts:67`. ### Boundary-слепок (инвариант, который надо сохранить) В `apps/server/src/collaboration/extensions/persistence.extension.ts:227-240`: когда **агент впервые** пишет в страницу авторства человека, состоя��ие человека немедленно пиннится отдельным слепком (`lastUpdatedSource='user'`) **до** перезаписи. Гарантия «всегда можно откатиться к тому, что написал человек, до первой правки агента». **Симметричного перехода агент→человек сейчас нет.** ### Атрибуция агента Источник правки неподделываемый: подписанный claim `actor` в collab-токене + флаг `users.is_agent`. Маркеры в БД: `pages.last_updated_source` (`'user'|'agent'`), `pages.last_updated_ai_chat_id`; копируются в `page_history` при `saveHistory`. Агентские правки (MCP) идут **не через REST, а через тот же collab-слой** (Yjs), см. `mutatePageContent` в `packages/mcp/src/lib/collaboration.ts:545`. **Понятия «сессии» агента нет** — каждый стор атрибутируется отдельно; при `delay=0` это ~слепок на каждое изменение контента → шумно. ### Клиент Кнопки «Save» / «сохранить версию» **нет** — всё на автосейве. История только читается: `POST /pages/history` и `POST /pages/history/info` (`apps/server/src/core/page/page.controller.ts:501-536`); список — `apps/client/src/features/page-history/components/history-list.tsx`. ### Share `shares` (миграция `apps/server/src/database/migrations/20250408T191830-shares.ts`) **не ссылается ни на какую версию** — только `page_id` + флаги (`include_sub_pages`, `search_indexing`). Весь публичный показ проходит через **единственную точку** `resolveReadableSharePage` (`apps/server/src/core/share/share.service.ts:159-189`), которая берёт **живой** контент: `pageRepo.findById(pageId, {includeContent:true})` (`apps/server/src/core/share/share.service.ts:176`). Дальше `updatePublicAttachments` → `prepareContentForShare` работают над `page.content`. `page_history` в share-пути **не используется нигде**. --- ## Проектируемое изменение слепков Принцип: **меняем только триггер создания строки `page_history`**, слой живого контента (Hocuspocus debounce) не трогаем. Риск потери данных не растёт: текущее состояние всегда лежит в `pages`. ### Триггер A — явный Save (рекомендуется: через collab-stateless) Hocuspocus живёт в отдельном процессе (`collab-main.ts`) и держит свежий ydoc в памяти. Самый корректный путь (без гонок с 10-секундным debounce): 1. Клиент по `Cmd/Ctrl+S` (перехват браузерного Save) или кнопке в тулбаре шлёт `provider.sendStateless({type:'save-version'})`. 2. Новый обработчик `onStateless` в расширении collab берёт свежайший ydoc из памяти (`TiptapTransformer.fromYdoc(document,'default')`), пере-использует существующую логику стора (запись в `pages`), затем пишет строку `page_history` с `kind='manual'`, и делает `document.broadcastStateless({type:'version.saved'})` — у соавторов обновляется панель истории + тост. Прецедент для stateless уже есть (`document.broadcastStateless` в `persistence.extension.ts:264`), `sendStateless` есть на клиентском провайдере. > Альтернатива (проще, но с гонкой): REST `POST /pages/history/save`, сн��мающий слепок из `pages.content`. Минус — может зафиксировать состояние, отстающее до ~10 c, если пользователь только что печатал. Подходит как MVP. ### Триггер B — idle-flush (по простою, напр. 1 час) Нужен **trailing-edge debounce**: слепок через `IDLE_INTERVAL` после *последней* правки. Дедупликация BullMQ по `jobId` так не умеет (держит время *первой* правки). Решение — на каждом `onStoreDocument`: 1. удалить отложенную idle-задачу для страницы (если ещё не активна); 2. поставить заново с `delay = IDLE_INTERVAL` (напр. 1 ч). При активном редактировании задача всё время отодвигается (никаких слепков в середине сессии — ровно то, что просили), а через час тишины срабатывает один раз и фиксирует финал брошенной сессии. Обрабатывает её тот же `HistoryProcessor` с тем же гейтом `isDeepStrictEqual`. ### Триггер C — boundary-слепки (сделать симметричными) - Оставляем user→agent (`persistence.extension.ts:227-240`). - **Добавляем agent→user**: когда человек начинает править после агентской сессии — пиннить финал агента слепком *до* перезаписи. Результат агента не потеряется, даже если агент не вызвал явный save. ### Новая колонка `page_history.kind` Добавить nullable `kind enum('manual'|'idle'|'boundary'|'agent')` (старые строки = `null`/legacy, обратная совместимость). Нужна для: (а) осмысленных меток в UI истории, (б) фичи «share сохранённых версий» — шарить только намеренные точки (`manual`/`agent`), а не idle-автосейвы. ### Дедупликация Все пути (явный save, idle, boundary) проходят **общий гейт** `isDeepStrictEqual(lastHistory.content, page.content)` — он не даст создать дубль. Уже есть в процессоре и boundary-логике; новые пути обязаны его использовать. --- ## Матчинг с агентами Делаем модель агента **симметричной человеческой** — это и есть «сведение»: | | Человек | Агент | |---|---|---| | Явный save | `Cmd/Ctrl+S` / кнопка → stateless | **новый MCP-инструмент** `save_page_version` (агент сам решает точки сохранения) | | Idle-flush | через `IDLE_INTERVAL` тишины | тот же механизм (можно отдельный, более короткий интервал) | | Boundary | user→agent | **agent→user (новый)** + сохраняем user→agent | «Сессия агента» = непрерывный батч правок одного `aiChatId`. Поскольку каждый MCP-инструмент — отдельный connect/mutate/disconnect, естественный терминатор сессии — **idle-debounce** (накопить правки, снять один слепок при затишье) **плюс** явный `save_page_version` для немедленной фиксации логического результата. Это закрывает «они хотят собственные сохранения» и «правки одной сессии надо сохранить»: вместо ~слепка-на-микроп��авку (текущее `delay=0`) — один осмысленный слепок на сессию, тег `kind='agent'` + `aiChatId`, плюс boundary-слепок человеческого базиса до начала сессии. Из `computeHistoryJob` уходит ветка «agent → delay 0», агент встаёт в общий конвейер триггеров. --- ## Доп. вариант: share только сохранённых версий — оценка сложности **Сложность: НИЗКАЯ–СРЕДНЯЯ.** В share-пути ровно **одна** точка чтения контента (`resolveReadableSharePage`), поэтому ядро фичи крошечное. Что нужно: 1. **Схема:** добавить в `shares` nullable `shared_history_id` (FK→`page_history.id`). `null` = «живой» режим (дефолт, как сейчас), не-null = запиненная версия. Обратная совместимость полная. 2. **DTO/UI:** в `apps/client/src/features/share/components/share-modal.tsx` — выбор «Делиться текущей версией» / «Делиться сохранённой версией» + пикер версий (реюз эндпоинта истории; ограничить выбор `kind in ('manual','agent')`). 3. **Резолв (1 ветка):** в `resolveReadableSharePage` — если `shared_history_id` задан, брать контент/`title`/`icon` из `pageHistoryRepo.findById(historyId, {includeContent:true})` вместо `pages`. Весь downstream (`prepareContentForShare`: токены вложений, вырезание комментов/htmlEmbed) работает над `page.content` без изменений. SEO-контроллер идёт через ту же точку → наследует автоматически. **Ограничения MVP (это и есть «вся сложность»):** - **includeSubPages:** пин — одна строка истории одной страницы. Пиннить поддерево = «версия каждого ребёнка на момент T» — заметно сложнее (резолв «история-строка на/до timestamp» для каждого потомка). Рекомендация: в запиненном режиме **запретить includeSubPages** (только одиночная страница) в MVP. - **Трансклюзии:** сейчас берутся из live-кэша `page_transclusions`. В запиненной версии останутся «живыми» (мелкая несогласованность) — для MVP допустимо, задокументировать; «трансклюзии как на момент версии» — отдельная тяжёлая задача. - Вложения и токены работают как есть (история ссылается на те же attachment-id, файлы не версионируются). Фича естественно ложится на основную: как только появляются именованные «сохранённые версии», «поделиться версией» = сослаться на одну из них. --- ## Edge cases, инварианты, риски - **Долговечность не меняется:** Hocuspocus-автосейв `pages`/`ydoc` остаётся → между слепками данные не теряются. Меняется только частота «точек восстановления». - **Trade-off (принять явно):** при «save + idle(1ч)» окно отката между сохранениями шире, чем при автосейве каждые 1–5 мин. Idle(1ч) ограничивает худший случай (брошенная сессия зафиксируется). Это ровно то, что просили. - **Гонка явного save с debounce** — снимается, если идти через collab-stateless (свежий ydoc из памяти), а не REST. - **idle remove+re-add:** если задача уже активна и её нельзя удалить — добавляем новую; общий гейт `isDeepStrictEqual` защитит от дубля. - **Кросс-процессность:** collab-процесс уже кладёт задачи в `historyQueue`; idle-задачу обрабатывает существующий `HistoryProcessor`. Новых очередей не нужно. - **Contributors:** Redis-набор контрибьюторов поппится при слепке; при более редких слепках версия агрегирует всех, кто правил с прошлой версии, — корректнее. - **Права:** явный save требует edit-права; у агента право несёт его токен. - **Миграция:** старые `page_history` строки живут как есть; `kind=null` = legacy. --- ## Объём работ (точечно, по файлам) - `apps/server/src/collaboration/constants.ts` — добавить `IDLE_INTERVAL` (+ опц. агентский), вывести из обихода `*_FAST_*`. - `apps/server/src/collaboration/extensions/persistence.extension.ts` — заменить `computeHistoryJob` на idle-debounce (remove+re-add), добавить `onStateless('save-version')`, добавить agent→user boundary. - `apps/server/src/collaboration/processors/history.processor.ts` — проставлять `kind` для idle/manual/boundary; гейт оставить. - `apps/server/src/database/repos/page/page-history.repo.ts` — `saveHistory` принимает `kind`; новая миграция `+kind`. - `apps/server/src/core/page/page.controller.ts` — (если REST-вариант save) `POST /pages/history/save`. - Клиент — кнопка Save + `Cmd/Ctrl+S` + `sendStateless`; метки `kind` и обновление панели по `version.saved` в `apps/client/src/features/page-history/components/history-list.tsx`. - MCP — новый инструмент `save_page_version` в `packages/mcp`. - Share — миграция `+shared_history_id` в `shares`, ветка в `apps/server/src/core/share/share.service.ts`, поле в `apps/server/src/core/share/dto/share.dto.ts`, пикер в `apps/client/src/features/share/components/share-modal.tsx`. --- ## Открытые продуктовые развилки (с рекомендациями) - **Путь явного Save:** рекомендую **collab-stateless** (корректность, единый путь записи, как у агентов); REST — более простой MVP. - **Терминатор агентской сессии:** idle-debounce + явный `save_page_version` + agent→user boundary (группировки по соединению нет). - **Share-версии:** MVP = одиночная страница, live-трансклюзии, выбор только из `kind in (manual, agent)`; поддерево и версионные трансклюзии — отдельные тяжёлые итерации.
Ghost added the enhancement label 2026-06-28 03:56:28 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#247