3e945305c8
getPage — доминирующая операция агентского цикла (812 вызовов/2ч, p95 840мс): полный обход ProseMirror-дерева convertProseMirrorToMarkdown на КАЖДЫЙ вызов, кэша нет. При 812 read против 28 update большинство — повторная конверсия того же неизменившегося контента в тот же Markdown на общем event loop. closes #479 - getpage-cache.ts: LRU-кэш (класс, не синглтон) результата конверсии. Ключ (canonical pageId UUID, updatedAt, optionsHash). updatedAt из ТОГО ЖЕ ответа /pages/info, что и content → инвалидация бесплатная и точная (страница изменилась → новый ключ). optionsHash — стабильная сериализация опций (dropResolvedCommentAnchors #328), getPage и export не коллизят. Границы: LRU по количеству (50) И по байтам (10МБ, Buffer.byteLength), вытеснение по любому; oversized-запись хранится, не заклинивает. - Кэш — protected инстанс-поле в context.ts (один DocmostClient на сессию/ идентичность) → изоляция как у клиента, межпользовательской утечки контента нет. Байт-идентичный вывод: кэшируется строка ДО подстановки {{SUBPAGES}}, подстановка на живых subpages выполняется на hit и miss одинаково. - Счётчики через существующий onMetricFn-синк: mcp_getpage_cache_hits_total / misses_total (honest hit-rate: miss на реальной конверсии, вкл. non-cacheable). - Subpages НЕ параллелизуемы: listSidebarPages требует spaceId из ответа page-fetch (resolvePageId даёт UUID, но не spaceId) → последовательность сохранена (задокументировано); кэш — основной выигрыш. Тесты: mcp node --test 813/813 (9 unit: hit/miss по updatedAt+options, вытеснение по count И byte, recency, oversized, hash order-insensitive; 4 mock: MISS→HIT байт-идентично + конверсия один раз, смена updatedAt→свежий MISS, slugId+UUID одна запись, дифф-тест живой подстановки subpages на hit). tsc чисто. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>