9a8671c3af5cfc7bc0ee5cd14e25eeaddd2bb22c
195 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2a951df096 |
feat(mcp): drawio стадия 3 — семантические тулы drawioFromGraph/FromMermaid/EditCells (#425)
Сырой XML остаётся escape-hatch, но для 90% случаев модель не видит ни координат, ни style-строк — весь класс ошибок лейаута и иконок уходит by construction: эти решения принимает сервер, а не LLM. Стоит на #443, переиспользует конвейер #423 и shape-index/elkjs/линтер #424. - drawioFromGraph(pageId, where, graph, direction?, preset?, layout?): граф узлов/групп/связей → резолв иконок (shape-index #424; неизвестная → generic по kind с подписью, не пустой квадрат), стили из пресета (kind→палитра), elkjs-layered с compound-группами, ассемблер XML (линтер-чистый by construction: зазоры >=150, прозрачные контейнеры, относительные координаты детей, cross-container рёбра parent=1, эскейп меток). Хинты pinned/sameLayerAs/layer и layout none/full/incremental — детерминированным post-pass'ом (ELK-констрейнты оказались ненадёжны). Пресеты default/dark/colorblind-safe (Okabe-Ito) — данные. - drawioFromMermaid(pageId, where, mermaid): чистый парсер flowchart (без браузера/CLI) → graph → тот же конвейер. Формы/направление/пунктир=async/ subgraph→группы/цепочки; не-flowchart отвергает внятно. - drawioEditCells(pageId, node, operations, baseHash): ID-based add/update/delete, delete каскадит на детей контейнера и связанные рёбра; baseHash обязателен (optimistic lock как drawioUpdate); сентинелы 0/1 от delete защищены. DoS-границы (LLM-вход): MAX_GRAPH_EDGES=1000/GROUPS=500 в validateGraph (узлы уже 500), mermaid MAX_CHARS=200k/LINES=20k/GROUPS=500, chain 500 — все с быстрым throw ДО лейаута/ассемблера (ассемблер и маппер вне ELK-таймаута, иначе OOM воркера). incremental сохраняет неперечисленные существующие ячейки (mergeExisting Cells) — «добавь узел» не стирает ручную расстановку. sameLayerAs/layer после снапа раскладываются по перпендикулярной оси с зазором >=150 → 0 quality-warnings; pinned — точные пользовательские координаты (clamp>=0, могут дать warning, гарантия «0 by construction» относится к авто-лейауту). Регистрация: 3 shared-spec на оба хоста (camelCase, execute-in-spec, inlineBoth Hosts не понадобился), DocmostClientLike/Method += 3, contract, routing-проза (fromGraph→архитектуры/облака, fromMermaid→стандартные flowchart, raw xml→экзотика). Тесты: mcp node --test 782/782 (57 новых) — приёмка (15+ узлов/2 вложенные группы/ AWS-иконки→0 lint/0 warnings/иконки резолвятся, hints, incremental без сдвига, edit_cells update/cascade/stale-baseHash, снапшоты пресетов + colorblind-safe, mermaid ветвление+subgraph) + регрессии на DoS-границы. tsc чисто; server jest (contract + ai-chat) 290/290. closes #425. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5dc7a2703f |
feat(mcp): getPageContext — «где я / что вокруг» по pageId одним вызовом (#443, часть 3/3)
Финальная из трёх частей #443. Есть pageId (из поиска, из ссылки) — нужно понять местоположение и окружение. Раньше это get_page + полное дерево. Только чтение, только метаданные (без контента). closes #443. - client.ts getPageContext(pageId): resolvePageId (slugId→uuid, для uuid без round-trip) → POST /pages/breadcrumbs + listSidebarPages(spaceId, uuid). Два запроса для uuid-входа (spaceId берётся из ответа breadcrumbs), +1 для slugId. Выход {page:{pageId,title,spaceId}, breadcrumbs:[{pageId,title}] (root→parent; [] для корня), children:[{pageId,title,hasChildren}]}. - Порядок breadcrumbs выверен по серверному источнику (getPageBreadCrumbs: CTE сидится на childPageId, идёт вверх, .reverse() → root→page; страница — ПОСЛЕДНИЙ элемент). page = chain[last], breadcrumbs = chain.slice(0,-1); корень → []. Проекция явная — наружу только pageId (UUID), slugId/icon/position не утекают. Пустой chain / 404 / 403 → явная ошибка, не пустой объект. - children — прямые дети через listSidebarPages (cursor-пагинация #442/#451, страница с >20 детьми отдаёт всех без дублей, порядок по position, hasChildren). - Общий реестр: getPageContext на ОБОИХ хостах через цикл спеков (mcpName=== inAppKey==="getPageContext", camelCase); DocmostClientLike/DocmostClientMethod += it; compile-time contract; TOOL_FAMILY READ + routing-проза. Тесты: get-page-context.test.mjs +6 (3-й уровень split, no-slugId-leak + 2 запроса, корень→[], slugId-resolve, >20 детей без cap/дублей, плохой id→ошибка, пустой chain→throw), tool-specs +1. mcp node --test 725/725, tsc чисто; server jest (contract + ai-chat) 265/265. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bfb4c8d8d0 |
feat(mcp): getTree — иерархия пространства/поддерева одним вызовом (#443, часть 2/3)
Раньше единственный способ получить дерево — listPages tree:true: BFS по sidebar-эндпоинту, десятки-сотни HTTP-вызовов и молчаливая потеря страниц (#442). Бэкенд форка уже отдаёт всё одним POST /pages/tree (getSidebarPagesTree, merged #442/#451), так что это MCP-сторона + общий реестр. Только чтение. - lib/tree.ts: buildPageTree(nodes) аддитивно расширен до buildPageTree(nodes, options?) с {shape?: "lean"|"getTree"; maxDepth?}. Дефолт/{} — байт-идентичный lean {id,slugId,title,children?} (существующие вызыватели listPages tree:true и subtree-BFS не тронуты, есть регрессионный тест на точный набор полей). shape:"getTree" проецирует {pageId, title, children?, hasChildren?} (id→pageId; slugId/icon/position/parentPageId не утекают ни на одной глубине). - client.ts: getTree(spaceId, rootPageId?, maxDepth?) — тот же код-путь, что listPages tree:true: один enumerateSpacePages(spaceId, rootPageId) (единичный /pages/tree + cursor-BFS фолбэк для stock upstream) → buildPageTree(pages, {shape:"getTree", maxDepth}). listPages tree:true помечен DEPRECATED в JSDoc (BFS-фолбэк на месте, поведение не тронуто). - maxDepth/hasChildren: полное дерево строится, потом обрезается. Корни = глубина 1; maxDepth:1 → только корни; hasChildren:true ТОЛЬКО на срезанном узле с серверным hasChildren (source of truth), опущен у листьев и раскрытых узлов. Схема тула min(1) отбраковывает 0/отрицательные. - tool-specs.ts: shared-спек getTree (оба хоста через цикл реестра), DocmostClientLike Pick += getTree, listPages desc/catalogLine — про депрекацию. server-instructions.ts: getTree:"READ" + routing-проза. Loader DocmostClientMethod += getTree, compile-time client-call contract += getTree. Циклы/self-ref безопасны: project рекурсирует только от корней, циклический компонент недостижим из корней (нет бесконечной рекурсии). Orphan (родитель отфильтрован пермишенами) всплывает как корень. Тесты: tree.test.mjs +9 (nesting+порядок+no-leak, maxDepth:1/2 + hasChildren, orphan→root, seeded rootPageId, ≤0/нефинитный = без среза, lean-байт-идентичность), tool-specs.test.mjs (схема getTree + депрекация listPages). mcp node --test 718/718, tsc чисто; server jest (shared-tool-specs.contract + ai-chat) 263/263. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d269bd9efe |
feat(mcp): markdown — формат по умолчанию для блочных getNode/patchNode/insertNode (#413)
Канонический конвертер (#293/#345/#351) зрел для блочного уровня: markdown становится дефолтом чтения/записи ОДНОГО блока, PM JSON — опция «для тонких работ». Новых тулов нет, поверхность не растёт. Имена уже camelCase (стоит на #412). - getNode(pageId, nodeId, format='markdown'): дефолт markdown — обёртка {type:doc,content:[node]} → convertProseMirrorToMarkdown, comment-якоря (ВКЛЮЧАЯ resolved) сохранены (это чтение под редактирование, не getPage). format:'json' — сырой сабтри. Авто-фолбэк не-топ-левел типов (tableRow/Cell/ Header по #<index>) в JSON через canBeDocChild = docmostSchema.nodes.doc. contentMatch.matchType (не рукописный список); поле format на каждом ответе. - patchNode/insertNode: XOR-вход {markdown?|node?} (оба optional в схеме, XOR на рантайме). markdown → импорт фрагмента → 1→N сплайс: первый блок наследует id цели, остальные свежие; dry replaceNodeById для #159-ambiguity ДО сплайса; соседние блоки byte-identical. Guard findUnrepresentableTableAttrs: цель со span/colwidth/backgroundColor → отказ с указанием на table-тулы/node-JSON. insertNode — insertNodesRelative (N блоков по порядку); голый tableRow/Cell JSON-only. - Сноски: ^[...] во фрагменте → канон-импортёр; importMarkdownFragment делит блоки от footnotesList, РЕМАПИТ id сносок фрагмента в свежие uuid (fn-1 фрагмента не коллизит с fn-1 страницы), mergeFootnoteDefinitions добавляет через ту же машинерию appendDefinition→normalizeAndMergeFootnotes→ canonicalizeFootnotes, что insertFootnote. Сырые JSON-пути не тронуты. - node-ops: replaceNodeByIdWithMany/insertNodesRelative (сплайс массива) в prosemirror-markdown/node-ops (канон после #414) + barrel. - ROUTING_PROSE (READ/EDIT), compile-time client-call contract, CHANGELOG (getNode default→markdown, breaking для внешних клиентов, в окне #411/#412). Тесты: сходимость (patchNode(markdown) блок docsCanonicallyEqual полному импорту — нет «второго канона»); id-нить 1→N (первый наследует, остальные свежие, соседи byte-identical); XOR; getNode markdown/json/non-top-level-fallback; сохранение comment-якорей (active+resolved); ^[...]→хвостовой список+перенумерация; guard. mcp node --test 697/697; pmd vitest 736; tsc чисто; server jest 273. Третий линк breaking-окна, стоит на #412 (#411→#412→ЭТОТ→#415). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7cb3199d09 |
refactor(mcp)!: BREAKING — все имена MCP-тулов snake_case → camelCase (унификация с in-app) (#412)
Один логический тул жил под двумя именами: внешний MCP snake_case (edit_page_text), in-app camelCase (editPageText) — дублирование доков, путаница при переносе промптов/скиллов, помеха шарингу спек (#294). Решение владельца: единый camelCase везде, включая внешний MCP. После этого mcpName === inAppKey. - tool-specs.ts: mcpName ВЫВЕДЕН из ключа спеки (mcpName == inAppKey) для всех 43 shared-спек — раньше divergent snake, теперь равен ключу (проверено: mcpName читается только структурно — цикл регистрации, генератор <tool_inventory>, TOOL_FAMILY). +5 inline-регистраций (tableGet/updateComment/deleteComment/ docmostTransform; search без изменений). Рантайм: 47 тулов, все camelCase, ноль подчёркиваний. - Контракт-конвенция ИНВЕРТИРОВАНА: shared-tool-specs.contract.spec `mcpName === toSnake(inAppKey)` → `mcpName === inAppKey`; tool-specs.test и tool-inventory.test обновлены. - ROUTING_PROSE/TOOL_FAMILY/INLINE_MCP_INVENTORY (server-instructions.ts) → camelCase (105 замен). ai-chat.prompt/guard уже на in-app camelCase-ключах — без изменений (guard прошёл). comment-signal EXCLUDED_TOOLS схлопнут с дублей snake+camel до camelCase. - Некоторое неочевидное: assertUnambiguousMatch(op: "patch_node"|"delete_node") в prosemirror-markdown/node-ops — op интерполируется в model-facing ошибку; литерал-юнион + call-sites → "patchNode"|"deleteNode". - Все snake-имена в описаниях/error-строках/комментах/тестах/доках → camelCase (whole-token, longest-match-first). CHANGELOG: BREAKING-таблица 46 строк + миграция (allowlists mcp__gitmost-*__get_node→__getNode, промпты/скиллы, .mcp.json, метрики по tool-label); релизится вместе с #411. Внутренние имена методов (PageService.updatePageContent и т.п.) НЕ тронуты — переименованы только ИМЕНА ТУЛОВ. Гейт: mcp node --test 677/677; tsc -p apps/server чисто; jest ai-chat-tools. service + shared-tool-specs.contract + tool-tiers + ai-chat.prompt + comment-signal-inapp → 323. Второй линк breaking-окна (#411→ЭТОТ→#413→#415). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3a521ada4d |
refactor(tools): updatePageContent → updatePageMarkdown; внешний MCP: +updatePageMarkdown, −import_page_markdown (#411)
Поверхности записи «целым телом» были несимметричны: у in-app агента полная
замена тела markdown называлась updatePageContent (имя не про формат, тогда как
парный updatePageJson — про JSON), а у внешнего MCP голого plain-body-replace
не было вовсе (только import_page_markdown — на деле парсер round-trip к
export_page_markdown, не plain-replace). Пара должна быть updatePageMarkdown /
updatePageJson.
Пост-Фаза-1б архитектура (реестр + циклы по обоим хостам):
- новая shared-спека updatePageMarkdown (mcpName update_page_markdown, inAppKey
updatePageMarkdown, tier как у updatePageJson) с execute (client, {pageId,
content, title}) => client.updatePage(...) — тот же путь updatePageContentRealtime
→ markdownToProseMirrorCanonical, ^[...]-сноски парсятся. Реестровый цикл
регистрирует её на ОБОИХ хостах автоматически. Добавлен 'updatePage' в
Pick DocmostClientLike.
- import_page_markdown убран с внешнего MCP через inAppOnly:true у спеки
importPageMarkdown — MCP-цикл и генератор инвентаря её пропускают, in-app
агент сохраняет importPageMarkdown; спека и client-метод НЕ удалены.
- удалён inline in-app updatePageContent tool (теперь из реестра под inAppKey
updatePageMarkdown) + его INLINE_TOOL_TIERS-энтри.
- ROUTING_PROSE: bulk-rewrite ссылается на update_page_markdown|update_page_json;
убрано упоминание import_page_markdown; инвентарь генерируется из catalogLine.
- лейбл-мапы chat-markdown.util (en/ru), человекочитаемые метки не тронуты.
- НЕ тронуты одноимённые внутренности: PageService.updatePageContent,
updatePageContentRealtime, collaboration.handler — переименовано только имя тула.
Тесты: updatePageMarkdown на обеих поверхностях с идентичной схемой, forward в
client.updatePage; import_page_markdown ОТСУТСТВУЕТ на MCP, присутствует in-app;
^[...]→сноски покрыт через collaboration.test. CHANGELOG BREAKING + миграция;
README/README.ru пакета обновлены. Гейт: mcp node --test 646/646, server jest
259, tsc чисто. Первый линк breaking-окна #416 (#411→#412→#413→#415).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
ddb37376a4 |
refactor(mcp): вписать drawio-тулы в реестровый цикл (Фаза 1б смержена) (#440)
Фаза 1б (#445/#446/#447/#448) влилась ПЕРЕД этим PR, поэтому явная проводка drawio через registerShared/sharedTool конфликтовала с реестровыми циклами. Фолд: - drawioGet/Create/Update → канонический execute в спеке (это client-методы): execute возвращает сырой результат, цикл оборачивает jsonContent на MCP и отдаёт как есть in-app — байт-в-байт как старые тела, overrides не нужны. layout сохранён: добавлен в buildShape create/update + 5-м аргументом (layout as 'elk'|undefined) в обоих execute. - drawioShapes/drawioGuide → остаются inline на ОБОИХ хостах. Гайд архитектора «execute в спеке с импортом searchShapes/getGuideSection в tool-specs.ts» оказался невозможен: drawio-shapes.ts использует import.meta.url, а tool-specs.ts тайпчекается из исходника под module:commonjs (contract-спека) → TS1343 на статический value-import, а import.meta не индиректится. Введён флаг спеки inlineBothHosts: спеки остаются в SHARED_TOOL_SPECS (contract пинит имя/описание/схему), но без execute; ОБА цикла их пропускают (добавлен симметричный guard в MCP-цикл), каждый хост регистрирует их inline через чистые хелперы — поведение байт-в-байт как до ребейза. - docmost-client.loader: взята develop-форма DocmostClientLike = Pick< DocmostClient> (#446); ручное зеркало убрано, паритет layout наследуется из реальной сигнатуры client. - ROUTING_PROSE: drawio intent-подсказки (shapes-first, guide, layout:elk); инвентарные строки убраны (генерируются из catalogLine). Гейт: mcp node --test 674/674; server jest ai-chat-tools.service + contract (211, все 5 drawio на обоих хостах, идентичная схема) + tool-tiers → 264; tsc чисто; layout-passthrough тест 3/3. ELK DoS-кап и layout-фикс из round 3/4 сохранены. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dc58974b31 |
fix(ai-chat): пробросить layout:elk в in-app drawioCreate/drawioUpdate — паритет с MCP (ревью #440)
In-app хендлеры drawio_create/drawio_update деструктурировали args БЕЗ layout и не передавали его клиенту 5-м аргументом → layout:"elk" (схема его принимает — общий buildShape) ТИХО терялся, ELK-автолейаут работал только по MCP-хосту. Корень: ручное зеркало DocmostClientLike (loader) отстало от реального client.ts — у его drawioCreate/drawioUpdate не было параметра layout (то, что #446 чинит деривацией типа, но #446 ещё не влит). Добавил layout?:'elk' в обе сигнатуры зеркала + проброс в обоих хендлерах. Тест (пропущенный зелёным гейтом пробел — не было теста на in-app passthrough): in-app drawioCreate/drawioUpdate с layout:'elk' → фейк-клиент получает layout 5-м позиционным аргументом; omit-кейс → undefined. Мутационно: убрать проброс в drawioCreate → layout-create-тест краснеет. Гейт: mcp build чисто; tsc -p apps/server без новых ошибок; jest ai-chat-tools.service (35) + shared-tool-specs.contract + tool-tiers + comment-signal-inapp → 273 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c917dcc3c1 |
fix(mcp): ограничить ELK-лейаут (кап узлов/рёбер + таймаут) — untrusted-граф DoS (ревью #440)
applyElkLayout крутит elkjs СИНХРОННО в процессе на mxGraph-XML от LLM (layout:'elk' в drawio_create/update) без лимита размера и без таймаута — большой граф (тысячи узлов, ~1МБ XML проходит stage-1 cap 16МБ) блокирует event-loop MCP-сервера на секунды-минуты. try/catch ловил только брошенную ошибку, но не зависание. - кап ДО построения графа: >500 узлов или >1000 рёбер → вернуть исходный XML (best-effort, как существующий catch); синхронный elkjs → кап и есть реальная защита; - Promise.race с 5s-таймаутом (defense-in-depth на случай async-elkjs); таймер гасится в finally → нет утечки хендла и unhandled-rejection (проигравший timeout остаётся pending с погашенным таймером); - тест: 600-узловой граф возвращается без изменений и быстро (<2s) — кап-путь. 79/79 drawio-тестов зелёные. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
eddc3b5c33 |
feat(ai-chat): пробросить drawio_shapes/drawio_guide in-app — восстановить SHARED_TOOL_SPECS-паритет (#424)
Стадия-1 (#434) уже была довяжена in-app в develop (
|
||
|
|
d219eb7525 |
Merge pull request 'fix(ai-chat): защита от петель агента — lockdown под тоггл + детектор деградации + бюджет шагов (#444)' (#454) from fix/444-agent-loop-guards into develop
Reviewed-on: #454 |
||
|
|
d0f99052cf |
refactor(tools): генерировать инвентарь SERVER_INSTRUCTIONS из реестра + guard имён тулов в промпте (#448)
Финальный линк Фазы 1б. Инвентарь тулов жил в 4 рукописных прозаических копиях (SERVER_INSTRUCTIONS под regex-тестом; <tool_catalog>; имена в ai-chat.prompt.ts без гарда; README) — роадмап #416 планировал 4 последовательных ручных правки этого текста (#411/#412/#413/#415). - SERVER_INSTRUCTIONS разбит (новый модуль server-instructions.ts): ROUTING_ PROSE (рукописные intent-подсказки «когда что» — осмысленно ручные, перенесены ДОСЛОВНО со всеми предостережениями: <=250 у create_comment, soft-delete у delete_page, baseHash у drawio_update, PUBLIC у share_page) + buildToolInventory() — генерирует <tool_inventory> из реестра (mcpName + purpose из catalogLine, группировка по TOOL_FAMILY, бакет OTHER ловит незамаппленное → тул нельзя тихо потерять) + 5 inline MCP-only (INLINE_MCP_INVENTORY). Детерминирован (семейства FAMILY_ORDER, имена localeCompare). regex-тест server-instructions удалён; структурные гарантии — в новом tool-inventory.test.mjs (точное членство множества сильнее старого \b-скрейпа). - Имена тулов в ai-chat.prompt.ts → через экспорт PROMPT_TOOL_NAMES; новый гард ai-chat.prompt.tool-names.spec.ts: каждое имя — реальный тул реестра, скан guidance-нот на camelCase-токены падает на несуществующем (escape- нейтрализация против ложных nThe-токенов). - INLINE_TOOL_TIERS уже содержал ровно 8 genuinely-inline тулов (после #445) — сжатие не потребовалось. Критерий: добавление/переименование спека меняет инвентарь БЕЗ правки прозы. Внутреннее ревью: APPROVE — фактическим прогоном подтверждено, что НИ ОДИН тул из старого SERVER_INSTRUCTIONS не выпал (диф старый-vs-новый пуст; добавился get_workspace, раньше прятавшийся в EXCEPTIONS); проза дословна; инвентарь полон/детерминирован/без фантомов; гард краснеет на обеих ветках провала. 613 node + 289 jest зелёные. Стоит на #445 — мержить последним в стопке 1б. README-каталоги вне обязательного скоупа (docs-скрипт) — в чек-лист #412. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8c74659d91 |
refactor(tools): execute-маппинг в SHARED_TOOL_SPECS + автопроводка обоих хостов (#445)
Ядро Фазы 1б. Реестр (#294) шарил только метаданные (имя/схема/описание/tier), но НЕ execute-логику — у каждого shared-тула было ДВА рукописных execute-тела с копией маппинга аргументов (MCP registerShared в index.ts; in-app sharedTool в ai-chat-tools.service, зеркалящий MCP-транспорт вручную). Корень повторяющихся parity-багов ( |
||
|
|
fe5b6ecd8c |
refactor(mcp): вывести DocmostClientLike/SharedToolSpec из реального типа клиента — убить ручные зеркала (#446)
Восстановленный отложенный долг #294: @docmost/mcp не отдавал .d.ts, поэтому в сервере жили ТРИ дрейфующие ручные копии одних и тех же имён/сигнатур (DocmostClientLike ~230 строк, копия SharedToolSpec, name-only HOST_CONTRACT_ METHODS-тест). In-app execute-тела зовут клиент ПОЗИЦИОННО, так что перестановка параметра в client.ts доезжала до прода рантайм-ошибкой без сигнала на компиляции. - declaration:true (+declarationMap) в packages/mcp/tsconfig.json; types-экспорт в package.json (exports → conditional {types, default} для . и ./http; require.resolve/dynamic-import резолвят default → build/index.js, рантайм не тронут). build/index.d.ts эмитится, реэкспортит DocmostClient + SharedToolSpec. Правок исходников пакета для эмита НЕ потребовалось. - DocmostClientLike → Pick<DocmostClient, 48 методов> из type-only import (стёрт на компиляции, ESM/CJS-границу не задевает); ручное зеркало удалено. - SharedToolSpec → type-only реэкспорт из пакета; ручная копия удалена. - client-host-contract.test.mjs удалён целиком — имена И сигнатуры теперь проверяет tsc. - Позиционная безопасность: never-called __assertClientCallContract(client: DocmostClientLike) воспроизводит каждый позиционный вызов с типизированными плейсхолдерами (AI-SDK стирает вход execute-замыканий в any, иначе позиционные вызовы не проверялись). Перестановка параметров client.ts → ошибка компиляции сервера ровно тут. Loose as-касты в ai-chat-tools.service не потребовали правок; as any не добавлялся. Внутреннее ревью: APPROVE. Runtime resolution через conditional exports не сломан (разобрано для прод-инсталляции, не только symlink); покрытие __assertClientCallContract полное (48 call-sites == union == assert, сверено программно); Pick полон; демонстрация reorder → TS2345 в assert. Единственная находка (Promise<any> в части возвратов) предсуществующая в client.ts, вне цели PR. Стоит на #447 (закрытие skew build/vs/src) — мержить после него. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2e6f1c3de5 |
fix(ci,mcp): REGISTRY_STAMP в билд-артефакте + кросс-пакетный CI — закрыть skew build/ vs src/ (#447)
Два структурных слепых пятна: (1) сервер грузит СКОМПИЛИРОВАННЫЙ
packages/mcp/build/index.js, а серверные guard-тесты читают src/tool-specs.ts —
правка src без пересборки оставляет тесты зелёными, но рантайм расходится со
спеками; (2) спеки добавляют в packages/mcp, а parity-тесты живут в jest-сьюте
apps/server — PR, трогающий только пакет, проходит зелёным, сломанная in-app-
проводка всплывает уже на develop (кейс
|
||
|
|
e24ddf6b3e |
test(ai-chat): покрыть safety-путь детектора деградации + границы (ревью #454)
Ревью: дизайн LGTM, но safety-фича недотестирована. Добавлено (только тесты,
прод-код не тронут):
- e2e-реакция детектора: streamText эмитит degenerate-чанки → union abortSignal
срабатывает с 'Output degeneration detected' (отличимо от Stop) → onAbort
пишет status:error + OUTPUT_DEGENERATION_ERROR + усечённый content, лиза MCP
закрыта. Именно ДЕЙСТВИЕ на детект (детектит-но-не-действует = защиты нет);
- граница monochar-порога: hasPeriodicTail('x'×59)=false, ('x'×60)=true
(мутация >=→> раньше выживала);
- empty-turn маркер (шаги исчерпаны + без текста → STEP_LIMIT_NO_ANSWER_MARKER;
негативы на нормальный текст-ход и на исчерпание-с-текстом — гардят AND);
- различение degeneration-onAbort vs user-Stop (Stop → status:aborted, без
error/усечения).
Мутационно: (a) >=→> роняет 60-границу; (b) нейтрализация onAbort-ветки роняет
reaction-тест; (c) нейтрализация маркера роняет empty-turn-тест. +7 тестов,
137 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
2c03fefa9d |
fix(ai-chat): защита от петель агента — lockdown под тоггл, детектор деградации, бюджет шагов (#444)
Третий класс петли (инцидент 2026-07-10): ран упёрся в 20-шаговый кап, спалив все шаги на чтение; на 20-м шаге final-step lockdown отнял инструменты (toolChoice:'none') посреди незаконченной работы → модель выродилась в текст-повтор («loadTools.» ×20416, 255КБ). Пакет защит по дизайну владельца: - MAX_AGENT_STEPS 20→50; спеки выводятся из константы (нет захардкоженных 19/20). - Final-step lockdown под env-тогглом AI_CHAT_FINAL_STEP_LOCKDOWN (дефолт OFF, по образцу AI_CHAT_DEFERRED_TOOLS): OFF — инструменты доступны на всех шагах + мягкий финальный нудж; ON — легаси toolChoice:'none'+FINAL_STEP_INSTRUCTION. Спеки параметризованы по тогглу. .env.example задокументирован. - Пустой ход (все шаги без текста, шаги исчерпаны) получает синтетический маркер-текст — виден в UI и реплее. - Детектор токен-деградации в onChunk (единственная защита от болтовни, БЕЗ maxOutputTokens — tool-аргументы это выходные токены): чистые правила (≥25 одинаковых строк ИЛИ периодический хвост), при срабатывании abort через внутренний AbortController ∪ effectiveSignal (AbortSignal.any), финализация в onAbort: усечение хвоста, ai_chat_runs.error=Output degeneration detected, лизы MCP/снапшоты освобождаются (существующий lifecycle). - Предупреждение о бюджете шагов на MAX-6…MAX-2 с убывающим N. - loadTools-описание и преамбула каталога явно говорят, что CORE-тулы всегда активны (список из CORE_TOOL_KEYS динамически). Внутренний цикл: 2 прохода. Ревью нашло data-loss-риск: правило периодичности детектора ложно срабатывало на markdown-разделителях/setext-подчёркиваниях/ хвостовых пробелах (монохар-хвост p-периодичен при ЛЮБОМ p → ложный abort с пометкой error и усечением). Починка: отдельная монохар-проверка (порог 60, выше любого реального разделителя) + требование ≥2 различных символов в периодическом блоке при p≥2. Реальный loadTools-цикл (период ~10) ловится. Мутационно: 59 одинаковых — не флаг, 60 — флаг; loadTools×20416 — флаг. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e3dc73e40f |
Merge pull request 'feat(tools): пассивный сигнал «new comments: N» в результатах tool-вызовов' (#428) from feat/417-new-comments-signal into develop
Reviewed-on: #428 |
||
|
|
144ffb07f5 |
Merge pull request 'refactor(mcp): дедупликация конвертер-смежных хелперов (node-ops форк, footnote-*, parse-node-arg)' (#429) from refactor/414-dedup-node-ops into develop
Reviewed-on: #429 |
||
|
|
f46d89eafb |
fix(ai-chat): wire drawio CRUD tools in-app to restore SHARED_TOOL_SPECS parity
PR #434 (drawio stage 1) added drawioGet/drawioCreate/drawioUpdate to the shared tool-spec registry with in-app metadata (inAppKey, deferred tier, catalogLine) but wired them only in the standalone MCP server, breaking the contract-parity and phantom-catalog unit tests on develop CI. - expose drawioGet/drawioCreate/drawioUpdate in forUser() via sharedTool(), mirroring the MCP transport's argument mapping (format ?? 'xml' default, flat schema regrouped into the client's `where` object, positional baseHash pass-through) - extend the DocmostClientLike hand-mirror with the three client methods - append the three names to the HOST_CONTRACT_METHODS drift-guard whitelist Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
86830b860d |
Merge pull request 'feat(ai-chat): авто-реконнект к detached-рану после живого обрыва SSE' (#432) from feat/430-live-reconnect into develop
Reviewed-on: #432 |
||
|
|
22f687c39e |
feat(ai-chat): авто-реконнект к detached-рану после живого обрыва SSE
Автономный ран продолжается на сервере при обрыве SSE (Safari роняет длинный стрим), но клиент показывал баннер 'Lost connection' и мёртвую вкладку до ручной перезагрузки: resumeStream() звался только на mount. Добавлен недостающий триггер. - chat-thread.tsx: в onFinish на живом isDisconnect (гард !wasResumed && autonomousRunsEnabled && mounted && assistant) -> beginReconnect с экспон. backoff (1/2/4/8/16с, лимит 5). Стоп: status->streaming / 2xx re-attach / терминальный хвост reconcile / stop / unmount. Исчерпание -> Retry. - Дедуп (главный риск): зеркалит mount strip/anchor — пиннит текущий streaming-ряд как anchor (id ассистент-строки), стрипает его из стора ДО replay, сервер ?expect=live&anchor=<id> пересобирает без дублей; на отказе/204 строка восстанавливается через onNoActiveStream (контент не теряется). - 204/overflow -> degraded poll через существующий onNoActiveStream. - RUN_STREAM_MAX_BUFFER_BYTES 4->32МБ (марафонские раны 11-25мин переполняли 4МБ); 204->poll остаётся backstop. Degraded-poll: фиксированный 10-мин-от-старта кап заменён на inactivity-кап (продлевается пока приходят новые ряды). - UI: баннер 'reconnecting… (N/5)' + ручной Retry на исчерпании. closes #430 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4b2af3d34a |
refactor(mcp): дедупликация конвертер-смежных хелперов (node-ops форк, footnote-*, parse-node-arg)
Аудит при подготовке #413 нашёл дрейфующие дубли между packages/mcp и packages/prosemirror-markdown. Четыре дедупа (поведение тулов не меняется): 1. node-ops: форк ~960 строк сведён в ОДНУ копию в prosemirror-markdown (живая mcp-версия — строгое надмножество замороженного #293-seed'а пакета; сверено по git-истории, новая пакет-копия байт-в-байт == прежней mcp-копии). Barrel-экспорт полной поверхности; mcp/client.ts/page-search.ts/transforms.ts/collaboration.ts импортируют из пакета; тесты переехали. node-ops тянет stripInlineMarkdown -> пакет-локальная text-normalize.ts несёт только этот примитив (mcp-версия — домен #408; заголовок документирует дубликацию + источник истины). 2. footnote-lex/footnote-analyze (vestigial legacy [^id]: диагностика): сведены к одному fence-aware предупреждению 'reference-style footnotes -> use ^[...]' (полезно для класса #410); footnote-lex удалён. 3. footnote-authoring -> примитивы (footnoteContentKey/makeFootnoteDefinition/ generateFootnoteId) перенесены в пакетный footnote.ts, одна реализация конвенции. 4. parse-node-arg -> перенесён в prosemirror-markdown (не mcp: сервер CommonJS не импортирует ESM-only @docmost/mcp, но нативно импортирует пакет), обе копии удалены, консьюмеры перенаправлены. canonicalizeFootnotes/ENFORCEMENT RULE #228 и comment-anchor/json-edit/text-normalize (mcp) не тронуты. API-поверхность node-ops оставлена чистой для #409/#413. closes #414 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ab40e82123 |
fix(tools): комментарийная обёртка КОМПОНУЕТ собственный toModelOutput тула, а не затирает (ревью #428)
wrapToolsWithCommentSignal всегда ставил свой toModelOutput, молча выбрасывая собственный toModelOutput инструмента (латентная ловушка — будущий тул со своим toModelOutput тихо сломался бы). Теперь база = origToModelOutput(info) при наличии, иначе воспроизведённый дефолт SDK; no-signal путь возвращает базу дословно, signal- путь = части базы (modelOutputToParts: text/json/content) + элемент сигнала последним. execute по-прежнему возвращает СЫРОЙ результат -> part.output/цитаты байт-идентичны. Дефолтный путь (единственный исполняемый сегодня) байт-идентичен и SDK-дефолту, и до-фиксовому signal-пути (проверено повторным ревью). json-ветку загардил ?? null для симметрии с fallback. +2 теста: тул со своим text/content toModelOutput — база честно сохраняется и в no-signal, и в signal (сигнал добавлен последним). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
dca9f2aaf0 |
feat(tools): пассивный сигнал «new comments: N» в результатах tool-вызовов (обе поверхности)
Комментарии человека находят агента сами: короткая эфемерная строка 'new comments: N on page … — call listComments(pageId)' в результате ЛЮБОГО tool-вызова, mid-turn. Общий хелпер packages/mcp/src/comment-signal.ts (createCommentSignalTracker: watermark + per-page debounce + working-set; buildCommentSignalLine; defangCommentSignalTitle). - Standalone MCP (index.ts): второй wrapper в choke point registerTool (паттерн метрик #402) — отдельный {type:'text'} content-элемент, форма результата не меняется. Источник: rate-limited listComments по working-set, title через getPageRaw только на hit. State per-session. - In-app (ai-chat-tools.service.ts): execute ВСЕГДА возвращает сырой результат (part.output/цитаты не трогаются), сигнал доставляется модели через отдельный toModelOutput ({type:'content', value:[raw, signal]}) — зеркало MCP; no-signal ветка точно воспроизводит дефолт SDK. Источник: REST-probe (осознанный форк от DB-count из ТЗ — чтобы не менять конструктор сервиса и не ломать спеки). - Инъекционная защита: в сигнал идут только count+pageId+defanged-title, НИКОГДА текст комментария (untrusted). Per-page watermark (не глобальный) — комментарии на второй странице не теряются. closes #417 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
654ba9f249 |
feat(ai-chat): персист tool-error частей — упавшие тулы видны в истории и сохраняют текст ошибки при реплее
В ai@6 упавший тул — это tool-error часть в step.content ({type,toolCallId,
toolName,input,error}), а не элемент toolResults. Раньше serializeSteps писал
только toolCalls+toolResults (ошибка терялась, orphan tool-call без результата),
а assistantParts эмитил заглушку 'Tool call did not complete.' (реальный текст
ошибки терялся для мультиходового реплея — модель не знала, почему упало, и
повторяла ошибку).
- StepLike расширен полем content; новый хелпер normalizeToolError (Error/string/
object -> строка, обрезка через существующий compactValue/лимиты).
- serializeSteps: на каждый tool-error пушит парный {toolName, error} тем же
паттерном, что успешный {toolName, output} -> колонка tool_calls фиксирует сбой.
- assistantParts: при наличии tool-error эмитит output-error с РЕАЛЬНЫМ текстом;
заглушка остаётся только для по-настоящему непарных вызовов (прерванных).
- docs/reading-ai-logs.md обновлён под новую форму + cutover-оговорка.
Обратно совместимо: старые строки читаются как раньше, error-элемент аддитивен.
closes #407
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
d90c3b8b9e |
fix(ai-chat): stop trimming tool outputs in replayed history
Read-tool outputs were compacted at a 4000-byte gate before being stored in metadata.parts, which is re-sent to the model every later turn. Whole-page reads (tens of KB) got shrunk to a 500-char preview plus a "[truncated N chars]" marker; on the next turn the model read that marker as a source truncation and re-read the page, wasting tokens and producing wrong behavior. - Raise MAX_TOOL_OUTPUT_BYTES 4000 -> 200_000 so normal reads are stored and replayed verbatim; only a single >200 KB output is compacted as a backstop. - Reword the inline string marker so it reads as a replay-history elision, not a source truncation, and tells the model it can re-call the tool. - Update doc comments and enlarge the compactToolOutput unit-test inputs above the new 200 KB gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
327737b701 |
feat(ai-chat): give the in-app agent insertFootnote/insertImage/replaceImage (#410)
The Researcher role wrote 40 literal `^[...]` and zero real footnotes: its
incremental write path (insertNode/editPageText) doesn't parse markdown, and
the footnote-capable tool was MCP-only. Promote three tools from inline
MCP-only to the shared registry so the in-app agent gets them too.
- tool-specs.ts: insertFootnote/insertImage/replaceImage added to
SHARED_TOOL_SPECS (mcpName/schema/description moved VERBATIM from the inline
registrations — MCP names + behaviour unchanged for external clients).
- index.ts: the 3 inline registerTool calls become registerShared; drop the
"MCP-only by design" comments.
- ai-chat-tools.service.ts: register the 3 in-app via sharedTool ->
client.insertFootnote/insertImage/replaceImage (imageUrl->url,
attachmentId->oldAttachmentId mapping).
- tool-tiers.ts: insertFootnote -> core (else the original asymmetry recurs —
footnote tool hidden while editPageText is core); images -> deferred.
- research/{en,ru}.yaml FOOTNOTES: `^[...]` parses ONLY on a whole-markdown
write (create/update/import); for a pinpoint citation to existing text use
insertFootnote; via editPageText/insertNode it stays literal.
- json-edit.ts guardrail: an edit_page_text `replace` containing a `^[...]`
token is refused into failed[] with an insert_footnote hint, mirroring the
existing formatting-marker refusal. (Slightly broader net than that mirror —
a literal `^[a-z]` regex class in a replace is also refused; accepted
defense-in-depth, has a no-false-positive test.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
f55191e2a0 |
Merge pull request 'test(ai-chat): упрочнение фикса MCP-зависания — покрытие defense-in-depth + параллельная сборка (#397 follow-up)' (#405) from fix/397-mcp-hang-hardening into develop
Reviewed-on: #405 |
||
|
|
7538f98a3d |
feat(agent-roles): increase instruction max length to 100000
Raise validation limit from 20000 to 100000 characters to allow more detailed instructions. |
||
|
|
41480bc44f |
test(ai-chat): harden the MCP-hang fix — cover defense-in-depth paths + parallelize build (#397)
Follow-up to the merged MCP-hang fix (
|
||
|
|
4e9f47b4a5 |
fix(ai-chat): strip NUL chars before persisting assistant rows
A single NUL (U+0000) in model/tool output (e.g. a truncated multibyte
read of a web page) is rejected by Postgres in BOTH the content (text) and
toolCalls/metadata (jsonb) columns, so it failed EVERY write of the
streaming assistant row ("invalid input syntax for type json") and silently
dropped the turn's content from the DB while the live stream still showed it.
- add stripNulChars: deep-strips NUL from all strings, returns the same
reference when there is nothing to strip (no needless clone)
- apply it at the flushAssistant choke point (covers content + toolCalls +
metadata for the seed, per-step and terminal writes)
- tests: deep-strip, same-reference, end-to-end via flushAssistant
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
888c87f984 |
feat(config): lower MCP timeouts and raise JSON body limit
Reduce the default external-MCP silence timeout from 5 min to 1 min and the overall call timeout from 15 min to 2 min. Update related tests and comments. Increase Fastify's JSON body limit to 25 MiB (configurable via HTTP_JSON_BODY_LIMIT) to accommodate large AI‑chat payloads. BREAKING CHANGE: shorter MCP timeouts may abort long‑running tool calls that previously succeeded with the older defaults. |
||
|
|
16b476a205 |
fix(ai-chat): bound MCP connect + guard turn setup so a hung handshake can't wedge every run
A transient network blip during an external-MCP handshake left createMCPClient pending forever (@ai-sdk/mcp does not settle on abort). getOrBuildEntry caches the per-workspace build PROMISE, so the never-settling connect poisoned the cache and every later turn hung at step_count=0 before streamText — the run never finalized, the row stayed 'running', and the chat was permanently blocked with A_RUN_ALREADY_ACTIVE (an explicit Stop could not interrupt the un-signalled setup). - mcp-clients: wrap connect() in a settling timeout (connectWithTimeout) that closes a late-arriving client, so a hung handshake rejects instead of poisoning the build cache; the bad server is skipped and the build completes. - mcp-clients: close a connected-but-unregistered client when tools() fails, fixing a pre-existing transport leak in buildEntry. - ai-chat.service: bound the toolsFor build with the run's abort signal AND a deadline (raceAgainstAbortAndTimeout); re-throw an explicit Stop only when a run exists (runId) so the run finalizes as 'aborted', and keep the legacy socket-bound path unchanged; settle the run 'aborted' vs 'error' accordingly. - tests: cache-not-poisoned + orphan-client-close-once + Stop-during-setup finalizes the run once. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
15859e3b9f |
feat(ai-chat): getCurrentPage returns the user's editor selection (#388)
Snapshot the editor selection at send time (same live-ref pattern as openPageRef in prepareSendMessagesRequest), carry it nested inside openPage so it dies with the page on a fail-closed resolve, and surface it to the model only through the existing core getCurrentPage tool. The selection TEXT is returned exclusively in the tool result (untrusted collaborative-page content, treated as data by SAFETY_FRAMEWORK); the system prompt gets only a fixed one-line flag, never the text/before/ after. sanitizeSelection caps text (4000), before/after (200), blockIds (<=64 chars, <=20). Selection is a hint, not ground truth — the tool description tells the agent to localize the fragment before editing. closes #388 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
10d5220f5e |
fix(ai-chat): #381 PR1 review round 1 — open()-gate test + paused-pending byte-cap
Do 1 [test-coverage]: контроллерный тест на флаг-гейт begin-hook open() — при OFF beginRun зовётся (durable-ран независим), а streamRegistry.open НЕ зовётся (закрывает регресс: пустая entry → non-null paused attach → зависший SSE вместо 204); при ON — open зовётся с (chatId, runId). Do 2 [stability]: байт-кап очереди pending paused-подписчика. pendingBytes + overflowed на Subscriber; в paused-ветке ingestFrame при превышении SUBSCRIBER_MAX_BUFFERED_BYTES (8MB) подписчик помечается overflowed, pending чистится, он выбрасывается из entry.subscribers (как overflowed-entry). start() на overflowed → onEnd (чистый 204-эквивалент, без частичного реплея). Контракт «start() в том же тике, что attach()» задокументирован в коде — кап это структурный бэкстоп для phase-2 Redis-await шва. Юнит-тест: paused A + live B, 9×1MB > cap → A выброшен (0 доставок), B получает все 9 живьём, поздний start(A) → один onEnd без реплея. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
52ee3c1f3e |
feat(ai-chat): resumable SSE run-stream registry — server, dormant (#381 PR 1)
PR 1 of 2 (#381, фаза 1.5 #184): серверный реестр SSE-стримов агентских ранов, чтобы любая вкладка могла подключиться к живому рану с реплеем кадров + живым хвостом. «Спящий» — весь провод за флагом AI_CHAT_RESUMABLE_STREAM (off по умолчанию); клиент (PR 2) ещё не написан. - ai-chat-stream-registry.service.ts: in-memory реестр (open/bind/abortEntry/ attach). attach — снапшот+подписка в ОДНОМ синхронном блоке (инвариант 4: нет await между `subscribers.add` и `frames.slice()`), paused-подписчик, overflow, retention с identity-guard (инвариант 2), open поверх live entry даёт ровно один onEnd (инвариант 3), anchor против кросс-ранового реплея (инвариант 6). - ai-chat.controller.ts: begin-хук open(chatId, runId) + GET-attach эндпоинт (403 чужой чат; 204 нет-entry/finished/anchor-мисматч; cleanup до первой записи + recheck req.raw.destroyed; cap→destroy). - ai-chat.service.ts: tee SSE-кадров в реестр (consumeSseStream + generateMessageId, гейт на runId && flag) + abortEntry из внешнего catch. - environment.service.ts: флаг isAiChatResumableStreamEnabled(). Флаг OFF ⇒ байт-в-байт legacy И #184-фаза-1 (нет start.messageId, нет tee). Инжектируемые провайдеры НЕ @Optional() → поломка вайринга роняет старт, а не тихо выключает фичу. Тесты: registry unit (16), controller.attach (9), service pipe-options (4, вкл. flag-off-with-runId негатив), int-spec ai-chat-attach (6, реальный MockLanguageModelV3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
a72ddbbe86 |
Merge pull request 'refactor(ai-chat): единый реестр спеков инструментов — унификация tables/pages/misc/comments (#294)' (#367) from refactor/294-spec-registry-cont into develop
Reviewed-on: #367 |
||
|
|
a325ddbabd |
feat(#371): roles catalog modal redesign — bundle cards + per-role import results
Integrates the designer-handoff Roles Catalog modal, wired to the real API; the
parent ai-agent-roles.tsx and the { opened, onClose, roles } contract are
unchanged.
- Server importFromCatalog now returns per-role lists (createdRoles /
skippedRoles with a reason) alongside the existing counters (compat-preserving),
so the UI can name the conflicting/installed roles.
- New pure view-model (catalog-bundle-model.ts): bundlePhase (empty | allNew |
allInstalled | updates | mixed, ignoring the transient 'skipped'),
installedLangForRole (same-slug-different-language hint), mapCatalogRoleToView —
all unit-tested without mounting.
- Bundle cards with a summary status in the collapsed header (eager useQueries
fan-out over all bundles, sharing the existing per-bundle cache keys), a single
primary action per bundle, checkboxes + select/deselect-all, an inline result
plaque that keeps the modal open, per-bundle and global 'Update all' request
series with progress, and the other-language hint.
- The partial-result plaque distinguishes the skip reason: only a name-conflict
offers 'Rename & install'; an already-installed race is informational (a rename
re-import would just skip again and self-heal into a false success).
- All strings i18n'd (en/ru); mock handoff code (SEED/mockImport/delay) removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
36b940fdb8 |
fix(#294 review F1-F2): test the changed execute wirings + transport-neutral descriptions
- F1: added in-app execute tests for the two wirings that ACTUALLY changed in the migration (the contract-parity test only checks advertised schema keys, not execute bodies): movePage forwards the newly-added optional `position` to client.movePage (and passes undefined position + null parent when omitted); the table trio (insert/delete/updateCell) forwards the unified `table` param positionally. A field destructured under the wrong name would have silently passed undefined to the client (execute is any-cast, tsc won't catch it). - F2: rewrote the three migrated descriptions that hardcoded snake_case sibling tool names (which the in-app camelCase layer exposes under different ids, violating the registry's own transport-neutral-prose convention) into neutral prose: getPage "use get_page_json" -> "use the lossless page-JSON read tool"; updatePageJson "get_page_json -> ... -> update_page_json" -> "read the page-JSON view -> modify -> write it back", "prefer rename_page" -> "prefer the rename-page tool"; exportPageMarkdown "import_page_markdown round-trip" -> "page-Markdown import round-trip" (the last was a direct regress — the in-app base said the camelCase importPageMarkdown). (stashPage's pre-existing get_page_json mention is out of scope, per the reviewer.) Gate: mcp build 0; ai-chat-tools.service + tool-tiers (catalog-partition) pass, incl. the 5 new execute-wiring tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ce70fab1df |
refactor(ai-chat): unify share_page into SHARED_TOOL_SPECS (#294, misc family)
Migrates share_page / sharePage into the transport-agnostic spec registry (schema + description declared once; each transport keeps only its execute/auth): - sharePage (deferred) -> SHARED_TOOL_SPECS; index.ts uses registerShared(), ai-chat uses sharedTool(); removed from INLINE_TOOL_TIERS. Drift reconciled (documented inline): both inline copies already carried the "only share when the user explicitly asked" security framing, so the old "per-transport divergence" note in BOTH layers was STALE — there was no real behavioral divergence, only wording drift. The canonical description merges the MCP copy's URL-format + idempotency detail with the in-app copy's reversibility note and keeps the shared security framing. pageId keeps the MCP copy's stricter .min(1). The MCP execute keeps its own `searchIndexing ?? true` default (per-layer, not part of the shared schema). Intentionally NOT migrated (kept inline — genuinely divergent, as their existing notes state): - search / searchPages: the in-app tool is a semantic+keyword hybrid (RRF) with in-process access control and a tuned schema (limit 1-20); the MCP `search` is a plain REST full-text search (limit up to 100). Different behavior AND schema. - docmost_transform / transformPage: the in-app tool deliberately omits the `deleteComments` schema field (a comment-deletion guardrail) and carries a shorter description. Different schema. Gate: mcp build 0 + node --test 458/458 (page-search excluded — hangs only under the local re2->RegExp type-shim, its source untouched), server jest 775 incl. tool-tiers catalog-partition + shared-spec contract parity, server tsc 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
39735afd73 |
refactor(ai-chat): unify page tools into SHARED_TOOL_SPECS (#294, pages family)
Migrates the three-layer page tools into the transport-agnostic spec registry
(schema + description declared once; each transport keeps only its execute/auth):
- getPage, listPages (core), createPage, movePage, renamePage, deletePage,
updatePageJson, exportPageMarkdown (deferred) -> SHARED_TOOL_SPECS; index.ts
uses registerShared(), ai-chat uses sharedTool(); removed from
INLINE_TOOL_TIERS. Tiers preserved from CORE_TOOL_KEYS (getPage/listPages =
core, the rest deferred).
delete_page is genuinely three-layer (in-app deletePage exists), so it IS
migrated — not MCP-only. Its H4 guardrail is preserved: the shared schema
exposes ONLY pageId, so no permanentlyDelete/forceDelete flag can reach the
client (still asserted by ai-chat-tools.service.spec.ts).
Descriptions merged (documented inline): each canonical text takes the MCP
copy's richer structural notes plus the in-app copy's reversibility framing.
Schema DRIFT reconciled (documented inline):
- createPage.content: MCP pinned .min(1) but the in-app copy left it unbounded
and DOCUMENTS an empty body as valid ("may be empty" — creating an empty page
to fill later is a real use). Kept the looser no-min form: create_page now also
accepts an empty body (harmless) and no previously-valid in-app input is
rejected. title/spaceId keep the MCP .min(1) (empty is never valid).
- movePage: MCP exposed an optional `position` (fractional-index) field the
in-app copy lacked. Unified by KEEPING position — the in-app client already
accepts an optional position arg, so the in-app execute now forwards it;
optional, so no previously-valid call is rejected. `parentPageId` is nullable
on both (real JSON null -> root); the MCP execute keeps its 'null'/'' string
coercion as a per-layer robustness fallback.
- getPage/renamePage/updatePageJson/exportPageMarkdown/listPages: kept the MCP
copy's stricter .min(1) on ids where the in-app copy was unbounded.
Per-transport execute logic preserved: getPage's {title,markdown} projection,
updatePageJson's JSON-string normalization, list_pages' default limit/tree, and
move_page's cycle guard + positive-confirmation check all stay in their execute
bodies.
Intentionally NOT touched: updatePageContent (Markdown-based body update; no MCP
equivalent) and getTable (name-convention divergence, see tables family) stay
inline.
Gate: mcp build 0 + node --test 458/458 (page-search excluded — hangs only under
the local re2->RegExp type-shim, its source untouched), server jest 770 incl.
tool-tiers catalog-partition + shared-spec contract parity + deletePage H4
guardrail, server tsc 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
eebbe6717c |
refactor(ai-chat): unify table row/cell tools into SHARED_TOOL_SPECS (#294, tables family)
Migrates the three-layer table WRITE tools into the transport-agnostic spec registry (schema + description declared once; each transport keeps only its execute/auth): - tableInsertRow, tableDeleteRow, tableUpdateCell -> SHARED_TOOL_SPECS; index.ts uses registerShared(), ai-chat uses sharedTool(); removed from INLINE_TOOL_TIERS (all three are deferred; not in CORE_TOOL_KEYS). Drift reconciled (documented inline): the four table tools previously carried a "NOT shared" note in both layers over a single parameter-NAME drift — the MCP layer named the table reference `table`, the in-app layer `tableRef`. Unified on the MCP name `table` (renaming the public MCP parameter would break external MCP clients; the in-app parameter is model-facing/prompt-only and safe to rename). The in-app execute bodies now destructure `table`. Descriptions took the MCP copy's richer wording (documents `#<index>`, padding, header-row behavior) plus the in-app copy's "Reversible via page history" note; both fields keep the MCP copy's stricter .min(1) (in-app left them unbounded); sibling tool references phrased transport-neutrally. Intentionally NOT migrated (kept inline): table_get / getTable. Its MCP tool name is noun-first (`table_get`) while the in-app key is verb-first (`getTable`), which breaks the snake_case(inAppKey) naming convention the registry enforces (shared-tool-specs.contract.spec.ts). Renaming the public MCP tool would break external clients, so it stays per-transport — but its in-app reference param was still aligned to `table` (was `tableRef`) for consistency with the migrated trio. Gate: mcp tsc 0 + node --test 458/458 (page-search excluded — hangs only under the local re2->RegExp type-shim, its source is untouched), server jest 730 incl. tool-tiers catalog-partition + shared-spec contract parity, server tsc 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e348433a39 |
refactor(ai-chat): unify comment tools into SHARED_TOOL_SPECS (#294, comments family)
Migrates the three-layer comment tools into the single transport-agnostic spec registry (schema + model-facing description declared once; each transport keeps only its execute/auth): - createComment, listComments, resolveComment, checkNewComments — moved to SHARED_TOOL_SPECS; index.ts uses registerShared(), ai-chat uses sharedTool(); removed from INLINE_TOOL_TIERS (tier/catalogLine now on the spec). Tiers preserved from CORE_TOOL_KEYS (create/list/resolve = core, check = deferred). Intentionally NOT migrated (kept MCP-inline): update_comment / delete_comment — they are MCP-only by design; the in-app AI-chat layer deliberately has no updateComment/deleteComment (comment edits are irreversible / not version-tracked), asserted by ai-chat-tools.service.spec.ts. A registry spec's tier/catalogLine are in-app metadata and the catalog-partition test forbids a deferred spec without a live in-app tool, so these stay per-transport. Drift reconciled (documented inline): createComment/listComments/checkNewComments took the more-maintained/superset description + stricter .min(1) guards. resolveComment: `resolved` drifted (MCP optional+default(true) vs in-app required) — kept the MCP superset, so in-app resolveComment now accepts an omitted `resolved` (defaults to resolve) — a deliberate, backward-compatible unification (never rejects a previously-valid input). Gate: mcp build 0 + node --test 480/480, ai-chat 654, tool-tiers (incl. F3 catalog-partition) 16/16, server tsc 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
68899a2c2e |
feat(ai-chat): durable detached agent runs — phase 1 (#184/#234)
Squashed for a clean rebase onto develop (was 19 commits; the reviewer approved
the net diff at
|
||
|
|
68caf8157a |
test(ai-chat): document AI_CHAT_DEFERRED_TOOLS + pin ON-path & catalog completeness (#341 review F1-F3)
- F1: document AI_CHAT_DEFERRED_TOOLS in .env.example (AI_* section) — default ON = deferred loading (compact catalog + loadTools), =false restores the old "all tools always active" behavior. - F2: integration test of the ON path in ai-chat-stream.int-spec.ts — a deferred tool activated via loadTools is active on the SAME turn's next step but a fresh turn starts cold (CORE + loadTools only), proving the per-turn activatedTools Set does not leak across turns/chats. Drives the real streamText loop with a MockLanguageModelV3 and inspects recorded per-step activeTools-filtered tools. - F3: replace the magic toHaveLength(28) in tool-tiers.spec.ts with a two-way partition against the LIVE in-app toolset (AiChatToolsService.forUser keys): every non-core tool must appear in buildInAppDeferredCatalog and every catalog entry must map to a real non-core tool — so a future tool forgotten in INLINE_TOOL_TIERS fails the suite instead of silently vanishing from the agent. No production logic change (mechanism was already reviewed correct). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e431b33bb1 |
feat(ai-chat): deferred tool loading (tiers + loadTools meta-tool) (#332)
The in-app AI agent shipped all ~41 tool schemas on every model step. This adds a two-tier catalog: core tools (frequent or one-line) stay always-active; the rest are advertised as a compact catalog and their full schema is fetched on demand via the loadTools meta-tool, wired through ai@6 prepareStep's per-step activeTools. - tools/tool-tiers.ts: CORE_TOOL_KEYS, INLINE_TOOL_TIERS, applyLoadTools, catalog builders (+ tool-tiers.spec.ts, 13 cases). - ai-chat.service.ts prepareAgentStep: returns activeTools = [...CORE_TOOL_KEYS, loadTools, ...activatedTools]; per-turn activated Set. - ai-chat.prompt.ts: buildToolCatalogBlock renders the deferred catalog. - mcp/tool-specs.ts: tier + catalogLine metadata (external snake_case /mcp transport unchanged). - EnvironmentService.isAiChatDeferredToolsEnabled(): AI_CHAT_DEFERRED_TOOLS, default ON per issue intent (kill-switch =false restores old behavior). Gate: server ai-chat 631/631, tool-tiers 13/13, mcp 472/472, tsc clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
eacc1c4811 |
Merge branch 'develop' of https://gitea.vvzvlad.xyz/vvzvlad/gitmost into feat/293-B-prosemirror-markdown-pkg
# Conflicts: # packages/mcp/build/client.js # packages/mcp/build/index.js # packages/mcp/build/tool-specs.js |
||
|
|
40d42d61e6 |
feat(mcp): search_in_page tool — in-page substring/regex search for the agent (#330)
Editorial roles (Corrector/Factchecker) brute-forced `get_node` block-by-block to
find occurrences (unquoted «ё», straight quotes, «т.е.»), burning tokens. New
`search_in_page(pageId, query, {regex?, caseSensitive?, limit?})` reads the page's
ProseMirror JSON via the existing getPageRaw and searches it IN MEMORY — no server
endpoint, no DB/schema change, no touch to the packages/mcp/src/lib schema mirror.
New pure `searchInDoc(doc, query, opts)` (packages/mcp/src/lib/page-search.ts):
recursive descent to each TEXT CONTAINER (paragraph/heading/table-cell paragraph),
glues its inline text via `blockPlainText` (a match survives inline-mark
boundaries — e.g. «т.е.» split across bold/italic), searches literal (indexOf) or
regex, and returns `{ total, truncated, matches:[{ nodeId, blockIndex, type,
before, match, after }] }`. `nodeId` is the container's attrs.id or the
`#<topLevelIndex>` of the enclosing top-level block — the SAME ref format
get_node/patch_node/comment-anchoring accept (verified identical to getNodeByRef),
so the agent goes straight from a hit to a targeted comment; `before`/`after` are
~40-char windows for a unique selection. `total`/`truncated` always reported (never
silent truncation). Lives in the SHARED_TOOL_SPECS registry → exposed in BOTH
transports (external /mcp + in-app AI-chat), with a SERVER_INSTRUCTIONS line and a
DocmostClientLike signature + contract-test entry. Corrector/Factchecker prompts
get a one-line "use search_in_page first" hint (versions bumped, catalog hash lock
refreshed).
Guards: empty/whitespace query → clear error; invalid regex → clear error (not a
generic 500); zero-length regex matches (`\b`, `a*`) skipped with lastIndex
advanced (no loop/flood); MAX_PATTERN_LENGTH=1000, MAX_CONTAINER_TEXT=100k bound
each exec; limit clamped [1,200] (default 50).
Tests: new page-search.test.mjs (17) — literal+regex, case-sensitivity,
mark-boundary glue, nodeId for paragraph/heading (attrs.id) and table-cell
(#<index> fallback), context bounds, limit/total/truncated + clamp, invalid
regex/empty/over-long errors, zero-length skip, empty-doc null-safety.
mcp: tsc clean; node --test 467 passed (+17). apps/server: tsc --noEmit clean
(DocmostClientLike + wiring). catalog check.mjs OK.
Known limitations (from internal review, non-blocking):
- Residual ReDoS: a crafted catastrophic-backtracking pattern (e.g. `(a+)+$`)
against a large single container can hang the event loop — JS regex is not
interruptible, so the length caps bound the base but not the backtracking.
Realistic exposure is low (containers are small; the pattern is supplied by the
authenticated model). Candidate for a follow-up hardening (safe-regex validation
or a worker+timeout) if it matters.
- Case-insensitive LITERAL search folds via toLowerCase; a char whose lowercase
differs in length (e.g. Turkish İ) BEFORE a match could shift the context
window — negligible for the RU/EN editorial scenario.
- On a `#<index>` table-cell fallback, `type` is the inline container ("paragraph")
while nodeId addresses the top-level block — addressing is correct; the field is
documented as the container's type.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
bcd194ee5d |
feat(mcp): hide resolved-comment anchors + feed from the agent (#328)
The AI agent (MCP + in-app chat) saw ALL comments incl. resolved via two channels, cluttering its context and breaking fragment search. Default now: the agent sees only ACTIVE discussions; resolved is opt-in. Active anchors and threads are always kept. Channel 1 — resolved comment anchors on agent reads (converter option): `convertProseMirrorToMarkdown(content, options?)` gains `options.dropResolvedCommentAnchors` (default false — zero change for every existing caller incl. git-sync). Both `case "comment"` emitters (top-level and the raw-HTML inlineToHtml path) emit BARE text (no `<span data-comment-id>`) when `resolved && the flag`; active anchors keep their wrapper. mcp `getPage` passes the flag; `export_page_markdown` does NOT (lossless export must preserve resolved anchors — that is why it is an opt-in option, not unconditional); `get_page_json` is untouched (lossless PM JSON). Built on the #293 package converter. Channel 2 — `list_comments` default active-only: `listComments(pageId, includeResolved=false)` now returns `{ items, resolvedThreadsHidden }` (was a bare array). By default a RESOLVED top-level thread is hidden wholesale — the root AND every reply anchored to it (a thread is gated only by its root's resolvedAt; a resolved reply under an ACTIVE root stays). `resolvedThreadsHidden` counts hidden threads so the agent knows to re-query. `includeResolved:true` returns everything. The `includeResolved` param is added to both tool registrations (MCP index.ts + in-app ai-chat-tools.service.ts); `DocmostClientLike` signature updated. Server `findPageComments` is NOT touched — the web UI's tabs depend on the full feed; filtering is only at the mcp-client level. All internal call sites (export_page_markdown / checkNewComments / transformPage) updated to `.items` with `includeResolved:true` to keep their full-feed behavior. The comment model is assumed FLAT (a reply's parentCommentId points at the thread root) — documented in the filter; a future reply-of-reply model would need a root-walk there. Tests: resolved-comment-anchors.test.ts (6 — anchor dropped with flag / kept without, for BOTH emitters; active always kept); list-comments-resolved.test.mjs (4 — resolved thread+reply hidden + counter; includeResolved:true returns all; an ACTIVE thread with a RESOLVED reply is NOT hidden). package vitest: 664 passed; tsc clean. mcp: node --test 458 passed; tsc clean. apps/server + git-sync: tsc clean (converter option default-off). NOTE: based on feat/293-B (#293/#326 STEP 5) — the converter lives in the package; this PR is stacked on #333 and its base retargets to develop once #333 merges. mcp/build is gitignored (not committed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |