Forge
markdowndeeb25a2
1# ADR 0134: Intercom — единый prepare-pipeline сообщений (wire, presentation, commit)
2
3**Статус:** Accepted · Implemented
4**Дата:** 2026-05-20
5
6## Связанные ADR
7
8| ADR | Роль |
9|-----|------|
10| [0128](0128-intercom-attachment-anchors-and-code-references.md) | `AttachmentAnchor`, маркеры `⟦a:id⟧`, reveal, degraded outcomes |
11| [0129](0129-intercom-message-body-markdown-and-fenced-code.md) | Fenced code в `content` — отдельно от attach |
12| [0119](0119-chat-slash-commands-intercom-surface.md) | `IntercomOutboundSendOrchestrator`, фазы send |
13| [0123](0123-intercom-full-skia-surface-evolution.md) | Flat feed, Skia-лента |
14| [0045](0045-agent-chat-persistence-event-log-and-projections.md) | Event log: `content` + `attachments[]` |
15| [0135](0135-intercom-attach-symbol-cache-and-hci-sidecar.md) | L1/L2 кэш member→lines; HCI sidecar после reindex |
16
17## Резюме
18
19Зафиксировать **один пайплайн подготовки** исходящего/входящего в ленту сообщения Intercom:
20
211. **Prepare** (фон, Roslyn) — parse → resolve по attach → **wire** `content` с `⟦a:id⟧` → **presentation** для Skia.
222. **Commit policy** — единые правила для composer send и `send_chat` (MCP): не коммитить «сырой» текст без маркеров при failed prepare.
233. **Degraded resolve** — один неудачный member не роняет всё сообщение: anchor + marker + `resolveOutcome` (ADR 0128 §9.1).
244. **Тупой рендер** — Skia читает `IntercomFeedPresentation`, hit только на `AttachLink`-сегментах; выделение сообщения не перекрывает ссылки.
25
26**Не цель:** отдельный WPF `BackgroundWorker`; уже достаточно `Task.Run` / `IntercomAttachmentResolveAtSendWorker`.
27
28---
29
30## Проблема (as-is)
31
32| Симптом | Причина |
33|---------|---------|
34| MCP-сообщение с `[F:… M:…]` без клика | `AppendMessageFromMcpAsync` при `!TryBuild` всё равно пишет **сырой** `content`, `attachments = []` |
35| Клик выделяет сообщение | Нет attach-hit: нет маркеров → нет attach-сегмента → только `CreateHit` на всё сообщение |
36| Нет подчёркивания | Attach-сегмент не строится; prose идёт через RTK/markdown без `BodyTone.Link` |
37| Зависание UI (исправлено ранее) | Синхронный build на UI; вынесено в worker |
38
39Composer send при ошибке build **не** коммитит (orchestrator). MCP — **другая политика**. Skia **повторно** парсит `content`+`attachments` с правилами, отличными от builder.
40
41---
42
43## Решение
44
45### 1. Слои
46
47```text
48RawInput
49 → Prepare (background)
50 Parsed body segments (prose | code)
51 Per-attach resolve (strict → degraded)
52 Wire: Content + Attachments[]
53 Presentation: IntercomFeedPresentation
54 → Commit (UI, policy)
55 → Project / Render (UI thread, dumb)
56 → Reveal @ click (re-resolve получателя, ADR 0128 §8)
57```
58
59| Слой | Владелец | Хранится в event log |
60|------|----------|----------------------|
61| Wire | `ChatMessageViewModel.Content` | да |
62| Attachments | `ChatMessageViewModel.Attachments` | да |
63| Presentation | вычисляется `IntercomFeedProjector` @ commit/load | нет (детерминирован от wire) |
64| Agent envelope | `IntercomAttachmentPromptFormatter` | только prompt, не лента |
65
66### 2. Типы
67
68- `IntercomMessagePrepareStatus`: `Success`, `PartialSuccess`, `Failed`.
69- `IntercomMessageCommitPolicy`: `Strict` (composer), `Strict` (MCP inject) — v1 одинаково.
70- `PreparedIntercomMessage`: status, warnings, error, `Outbound`, `FeedPresentation`.
71- `IntercomFeedSegmentKind`: `Prose`, `Code`, `AttachLink` (+ `AnchorId`, `Stale`).
72
73### 3. Prepare API
74
75```csharp
76IntercomOutboundMessagePreparer.PrepareAsync(
77 raw, pending, editor, workspace, solution, ct)
78 → PreparedIntercomMessage
79```
80
81Вызывают:
82
83- `IntercomOutboundSendOrchestrator` (user send),
84- `AppendMessageFromMcpAsync` (`send_chat` assistant),
85- `IntercomAttachmentResolveAtSendWorker` (тонкая обёртка над preparer).
86
87### 4. Degraded resolve @ send
88
89При неудаче Roslyn для `member` / `syntaxScope`:
90
91- всё равно вставить `⟦a:id⟧` и `AttachmentAnchor` с `file`, `memberKey`, `displayLabel`, `excerpt` (если есть);
92- `resolveOutcome` = `member_not_found` (или аналог);
93- `lineStart`/`lineEnd` опционально пусты;
94- статус prepare = `PartialSuccess`, предупреждение в UI.
95
96Reveal у получателя делает re-resolve (уже `IntercomAttachmentRevealPlanner`).
97
98### 5. Commit policy
99
100| Prepare status | Composer send | MCP `send_chat` assistant |
101|----------------|---------------|---------------------------|
102| `Failed` | не коммитить, статус ошибки | вернуть ошибку, **не** добавлять в ленту |
103| `PartialSuccess` | коммит + предупреждение | коммит + предупреждение |
104| `Success` | коммит | коммит |
105
106### 6. Skia / hit-test
107
108- `IntercomFeedProjector.Project(content, attachments)` — единая логика сегментов.
109- **`SkiaChatHitRegistry`** — единственный реестр pointer hit на кадр (`_chatHits.Clear()` в начале `DrawSkiaScene`).
110- **Приоритет `FindIndex`:** `RevealAttachment` (attach chip, markdown link) → `SkiaChatPointerAction` (chrome: overview, composer, slash) → z-order.
111- **`SkiaChatDrawContext.RegisterHit`** → `RegisterContentRect` (content + `scrollOffset`); chrome — `RegisterControlRect` (`SkiaChatHitGeometry`).
112- **Диспетчер:** `SkiaChatSurfaceControl.PointerDispatch.cs` — `TryDispatchPointerPress` / `TryDispatchPointerWheel` (не разрозненные `Contains` по полям).
113- **Лента:** `SkiaChatMessageFeedEntity.CreateHit` → **null**; узкие hits только в `Draw` (attach chip, markdown link, row select).
114- **Markdown link:** `RegisterFeedMarkdownLinkHits` (markdown-lines) или `RegisterFeedMarkdownLinkHitsFromText` (RTK/plain prose).
115- **Composer / overview:** `RegisterComposerPointerHits`, `registerChromePointerHits` — `SkiaChatPointerAction`.
116- **`CreateHit` на entity** — только цельные плитки (topic card, slash block), не feed-сообщения.
117- `ParseInline`: при `[` без code-ref — продвинуть `i`, не зацикливаться.
118
119### 7. Reveal
120
121Клик из ленты → `IntercomAttachmentNavigator.Apply` (без MCP roundtrip с UI-потока) — ADR 0130 / уже в коде.
122
123---
124
125## Фазы внедрения (этот ADR)
126
127| Шаг | Содержание |
128|-----|------------|
129| 1 | ADR 0134 + типы + `IntercomFeedProjector` |
130| 2 | `TryPrepare` + degraded resolve |
131| 3 | `IntercomOutboundMessagePreparer`, единый commit |
132| 4 | Orchestrator + MCP append |
133| 5 | Skia presentation + hit (chip: рамка/статус, без underline) |
134
135---
136
137## Последствия
138
139- Старые сообщения в логе без маркеров: projector fallback только если есть `attachments[]`; иначе prose как текст (без клика).
140- Persistence schema не меняется (presentation не в event log).
141- Тесты: builder partial/degraded, projector attach segment, MCP append fail-closed.
142
143## Критерии приёмки
144
1451. `send_chat` с `[F:path M:Member]` при открытом solution → в ленте `[Member]` с подчёркиванием, клик → reveal.
1462. При полном fail prepare MCP возвращает ошибку, в ленте нет сырой bracket-строки без маркера.
1473. Composer send при fail prepare не добавляет сообщение (как раньше).
1484. Shift+клик по attach → select в редакторе.
149
View only · write via MCP/CIDE