[feature][ai-chat] Уведомлять агента о правках пользователя в открытой странице (per-turn diff в системном промпте) #274
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Проблема
Агент в AI-chat не знает, что пользователь вручную правил открытую страницу между его ответами, и работает от своей устаревшей копии.
Сценарий:
Сейчас лечится только вручную («перечитай страницу»). Нужно автоматически сообщать агенту, что открытая страница была изменена пользователем с момента его последнего хода.
Причина (по коду)
AiChatMessageRepo.findAllByChat→rowToUiMessage→convertToModelMessages(apps/server/src/core/ai-chat/ai-chat.service.ts:366-379). В транскрипте лежат прошлые tool-выводыgetPage(Markdown-контент страницы), и модель реконструирует страницу из них.openPageи валидируется вresolveOpenPageContext(ai-chat.service.ts:271-296), но её контент не сравнивается с тем, что агент видел раньше.Решение
Per-turn эфемерная заметка в системном промпте с unified-diff в Markdown — прямой близнец уже существующего
INTERRUPT_NOTE(apps/server/src/core/ai-chat/ai-chat.prompt.ts:67-73, флагinterrupted): вставляется в context-секцию внутри safety-сэндвича, живёт один ход и самоочищается, потому что промпт строится заново каждый ход.Формат — Markdown (агент и читает
getPage, и пишетupdatePageContentстраницу в Markdown), оба конца диффа рендерятся одним и тем же путём (exportPageMarkdown), иначе дифф зашумится форматированием.Референс диффа: «состояние на конец прошлого хода»
База для диффа — не «что агент прочитал», а состояние страницы на конец прошлого хода агента (снимок). Тогда
diff(снимок, текущее)= ровно то, что наменял кто-то другой между ходами, а собственные правки агента исключ��ются по построению (они уже вшиты в снимок). Провенанс-фильтры (pages.last_updated_source/last_updated_ai_chat_id) для корректности не нужны — остаются опциональным быстрым guard'ом.Жизненный цикл:
(chatId, pageId); еслиsnapshot.pageUpdatedAt !== page.updatedAt→ рендерим текущий Markdown, считаем дифф; непустой → взводим флаг + текст диффа для промпта.onFinish): upsert снимка = текущий Markdown после всех правок агента. Это и есть self-clearing: тот же пользовательский правок на следующем ходу уже не повторится (снимок догнал текущее). Здесь же сидинг снимка на первом ходу.Пример заметки (в context-секции системного промпта):
Изменения по файлам
Миграция (новая)
database/migrations/<ts>-ai-chat-page-snapshot.ts(по образцу20260409T132415-ai-chat.ts):Плюс объявить таблицу в
database/types/db.d.tsиentity.types.Репозиторий (новый)
database/repos/ai-chat/ai-chat-page-snapshot.repo.ts:findByChatPage(chatId, pageId, wsId)+upsert(...). Рядом.spec.ts.Чистый util диффа (новый)
core/ai-chat/page-change/page-change.util.ts:computePageChange(snapshotMd, currentMd) → { changed, diff }— unified diff, нормализация пробелов, капа по размеру (≈4–8 КБ, дальше «diff truncated — use getPage to read the full current page»). Переиспользовать существующую diff-зависимость (та, что заdiffPageVersions/dry-run), новую не добавлять. Юнит-тестируемо →.spec.ts.Промпт
core/ai-chat/ai-chat.prompt.ts— близнецINTERRUPT_NOTE:BuildSystemPromptInputдобавитьpageChanged?: { title: string; diff: string } | null;PAGE_CHANGED_NOTE+ блок<page_changed>в context-секции (внутри safety-сэндвича, какinterrupted);ai-chat.prompt.spec.ts.resolveOpenPageContext(ai-chat.service.ts:271-296) — расширить возврат наupdatedAt(строкаpageуже загружена).Сборка хода (
ai-chat.service.ts:366-462) — вычислитьpageChangedпо циклу детекции и передать вbuildSystemPromptрядом сinterrupted. Текущий Markdown брать тем же путём, что тулы (client.exportPageMarkdown/getPageчерезtools/docmost-client.loader.ts).Обновление снимка — в терминальном
onFinish(рядом сcloseExternalClients/finalize,ai-chat.service.ts:478-531): upsert снимка текущим Markdown +updatedAt; сидинг на первом ходу.Краевые случаи
updatedAtне изменился → быстрый путь, без рендера и диффа.getPage.(chat, page), свой ряд на страницу.Тесты
page-change.util.spec.ts: чистая функция диффа (изменение/пусто/нормализация/капа).ai-chat.prompt.spec.ts: рендер блока<page_changed>по флагу и его отсутствие без флага.ai-chat-page-snapshot.repo.spec.ts: upsert/find.onFinish; заметка появляется ровно на один ход и самоочищается.Вне scope / на будущее
last_updated_source/last_updated_ai_chat_id) как быстрый пре-чек.