feat(secretary): устойчивость к обрывам — источник=транскрипт, склейка продолжений, догон сессий
Секретарь перестал терять промпт владельца при обрыве (сбой API / ручной стоп / жёсткий крах). Источник правды — транскрипт на диске: сырьё (Слой 1) пересобирается из всего транскрипта на каждом завершении, а не дописывается по последнему обмену. - classifyEntry/assembleExchanges: распознавание машинных меток (isApiErrorMessage, [Request interrupted by user] обе формы, isCompactSummary, isMeta) — метка не считается настоящим промптом; промпт после обрыва помечается продолжением (cont=1), хвост — tail=1. - realBoundariesFromRaw: продолжение не открывает новый спан (одна работа не дробится). - честные пометки спана: «(связь прерывалась — продолжено)» / «(прервана, не завершена)». - stop-хук: пересборка сырья из транскрипта + догон недоразобранного хвоста прошлых (умерших) сессий дела при «включи секретаря <дело>» (_sessions.json, secretary-sessions). - parseLastExchange → тонкая обёртка над assembleExchanges (без дубля логики). Свод секретаря зелёный: 172 теста / 12 файлов. Спека: docs/superpowers/specs/2026-06-23-secretary-interruption-resilience-spec.md План: docs/superpowers/plans/2026-06-23-secretary-interruption-resilience.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,173 @@
|
||||
# Финальная спека: секретарь — устойчивость к обрывам (источник = транскрипт)
|
||||
|
||||
> **Финал брейншторма** поверх хендофф-направления `2026-06-23-secretary-interruption-resilience-design.md`.
|
||||
> Все открытые вопросы §6 хендоффа закрыты с владельцем (см. §A). Все машинные метки проверены Grep'ом
|
||||
> по реальным транскриптам — строки указаны. Дисциплина дальше: `writing-plans` → TDD.
|
||||
> Режим: штатный (стен нет; пол + проверка-перед-пушем). Ветка `main`, удалёнка `gitea`. Тетради дел НЕ коммитить.
|
||||
|
||||
---
|
||||
|
||||
## A. Закрытые решения владельца (источник правды по спорным точкам)
|
||||
|
||||
| Вопрос (§6 хендоффа) | Решение владельца |
|
||||
|---|---|
|
||||
| 6.1 источник правды | **Транскрипт — главный.** На каждом завершении ответа сырьё (Слой 1) пересобирается ИЗ ВСЕГО транскрипта, а не дописывается по последнему обмену. |
|
||||
| 6.2 продолжение/отмена/уточнение | **Один склеенный кусок + честная пометка.** Модель смысл обрыва НЕ решает — всё механически, по меткам. |
|
||||
| 6.3 крах между сессиями | **Догон при «включи секретаря `<дело>`».** Не на каждом старте; запуск — действием владельца, привязка по имени дела. |
|
||||
| 6.4 связь со сделанным | Нарезка по спанам, `distillSpan`, `realBoundariesFromRaw` (meta=1) **остаются**; над ними встаёт транскрипт-сборщик. |
|
||||
| 6.5 перестройка сырья | **Да** — сырьё всегда полно перестраивается из транскрипта, прерванные ходы не теряются. |
|
||||
| 6.6 объём меток | Список ниже (§C), **проверен на реальных транскриптах**. |
|
||||
| Правило после РУЧНОГО стопа | **Считать продолжением (склеивать).** Следующая реплика после любого обрыва (сбой API ИЛИ ручной стоп) по умолчанию = продолжение прежней работы. |
|
||||
| §8 связанные дела | **Вне этой задачи** (контекст другой сессии). Записаны в §H, не реализуются. |
|
||||
|
||||
---
|
||||
|
||||
## B. Проблема и корень (доказано)
|
||||
|
||||
Секретарь захватывает работу на **Stop** (конец ответа) через `parseLastExchange`
|
||||
([secretary-transcript.mjs:40](../../../tools/secretary-transcript.mjs)). При обрыве Stop хрупок:
|
||||
|
||||
- **Корень потери промпта.** Ручной стоп лежит в транскрипте как сообщение с ролью `user` и текстовым блоком:
|
||||
```json
|
||||
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"[Request interrupted by user]"}]}}
|
||||
```
|
||||
(`6cead3ab` строки 334, 468, 1465). Текущий `isRealUserPrompt` ([secretary-transcript.mjs:18](../../../tools/secretary-transcript.mjs#L18))
|
||||
принимает **любой** user-текст за настоящий промпт → метка-обрыв (и «продолжи» после сбоя) крадёт границу
|
||||
настоящей просьбы владельца. Одна работа дробится на «я: продолжи».
|
||||
- **Единственное, что переживает все обрывы, — файл транскрипта на диске** (`~/.claude/projects/<proj>/<session>.jsonl`),
|
||||
Claude Code пишет его по ходу. Значит источник правды — транскрипт, а не Stop/сырьё.
|
||||
|
||||
---
|
||||
|
||||
## C. Машинные метки (проверено Grep'ом — НЕ по памяти)
|
||||
|
||||
Сборщик обязан распознавать и НЕ принимать за настоящий промпт владельца:
|
||||
|
||||
| Метка | Где на записи | Смысл | Доказательство (файл:строка) |
|
||||
|---|---|---|---|
|
||||
| `entry.isMeta === true` | верхний уровень записи | служебный ход (гейт/навык/контекст) | `ff1d4618` — 5 шт.; уже используется (`userIsMeta`) |
|
||||
| `entry.isApiErrorMessage === true` | запись с `role:"assistant"`, текст `"API Error: …"` | сбой API, ответ лопнул | `ff1d4618:534` (без статуса), `ff1d4618:577` (`apiErrorStatus:529`) |
|
||||
| текст user-блока `[Request interrupted by user]` **или** `[Request interrupted by user for tool use]` | `role:"user"`, `content[].type:"text"` | ручной стоп (ДВЕ формы!) | `6cead3ab:334`; `240e3972:530` и `42e79641:225` (форма «for tool use») |
|
||||
| `entry.isCompactSummary === true` (+ `isVisibleInTranscriptOnly:true`) | служебная запись | выжимка при сжатии контекста | `69992620:4929`, `aca79163` |
|
||||
|
||||
**Важно:** `apiErrorStatus` присутствует НЕ всегда (на `ff1d4618:534` его нет) → опираться на `isApiErrorMessage`,
|
||||
статус — только доп-инфо. Детект ручного стопа матчить по **префиксу** `[Request interrupted by user`
|
||||
(чтобы покрыть обе формы и будущие вариации хвоста).
|
||||
|
||||
---
|
||||
|
||||
## D. Решение
|
||||
|
||||
### D1. Источник = транскрипт; сырьё пересобирается целиком
|
||||
На каждом завершении ответа (Stop) секретарь:
|
||||
1. читает **весь** транскрипт сессии (через `node:fs` в хуке — Read-инструмент закрыт, fs работает);
|
||||
2. собирает из него последовательность **обменов** (ход = настоящий промпт владельца → ответ → действия),
|
||||
распознавая метки §C: служебные/метки-обрывы/выжимки **не** считаются настоящим промптом;
|
||||
3. перезаписывает сырьё (`docs/secretary/raw/<session>.log`) этой пересборкой (PII вырезается перед записью,
|
||||
как сейчас — `sanitize`).
|
||||
|
||||
Сырьё всегда полное → ни один ход (включая прерванный) не пропадает. Нумерация ходов стабильна
|
||||
(транскрипт append-only). Нарезка по спанам (`computeSpans`/`realBoundariesFromRaw`) и разбор (`distillSpan`)
|
||||
работают поверх честного сырья **без изменения логики**.
|
||||
|
||||
### D2. Сегментация обменов и метки-обрывы
|
||||
При сборке обменов:
|
||||
- **Границей нового обмена** служит только настоящий промпт владельца (не meta, не метка-обрыв, не выжимка).
|
||||
- Запись `isApiErrorMessage` (assistant) и `[Request interrupted by user…]` (user) — **не границы**; они
|
||||
поглощаются текущим обменом как факт обрыва (помечают, что работа прерывалась).
|
||||
- `isMeta` и `isCompactSummary` — пропускаются как служебные (не границы, не работа владельца).
|
||||
|
||||
### D3. Правило продолжения (структурно, БЕЗ слов)
|
||||
**Настоящий промпт владельца, идущий сразу после метки-обрыва** (сбой API ИЛИ ручной стоп), **без
|
||||
завершённого ответа между ними**, — это **продолжение**: он НЕ открывает новый спан, его работа склеивается
|
||||
со спаном предыдущей настоящей просьбы. Распознаётся по позиции относительно метки, **не по тексту реплики**
|
||||
(словарь «продолжалок» отвергнут владельцем).
|
||||
|
||||
- Реализация: ход-продолжение метится в сырье отдельным ярлычком (по образцу `meta=1`, напр. `cont=1`),
|
||||
`realBoundariesFromRaw` исключает его из границ — как и meta. Спан настоящей просьбы поглощает ходы-продолжения.
|
||||
- В протоколе спан получает честную пометку: **«(связь прерывалась — продолжено)»**.
|
||||
- Хвост, прерванный и **не** продолженный (на момент краха), помечается **«(прервана, не завершена)»** —
|
||||
обрывок не выдаётся за готовую работу.
|
||||
|
||||
### D4. Догон после жёсткого краха (между сессиями)
|
||||
Жёсткий крах (свет/закрыл VS Code) машинной метки не оставляет; старая сессия умирает, новая получает другой
|
||||
транскрипт. Догон — при **«включи секретаря `<дело>`»**:
|
||||
1. секретарь по имени дела находит **прошлую сессию(и)** этого дела (указатель сессий хранится в папке дела —
|
||||
см. D5);
|
||||
2. находит её транскрипт `dirname(currentTranscriptPath)/<prevSession>.jsonl`;
|
||||
3. пересобирает её сырьё из транскрипта (теперь уже с хвостом — D1) и доразбирает недоразобранные спаны
|
||||
(по курсору) в тетрадь дела через тот же `distillSpan`.
|
||||
|
||||
Граница честности (§2.5 хендоффа): хвост появляется не мгновенно, а этим догоном на следующем включении дела.
|
||||
|
||||
### D5. Привязка сессии к делу
|
||||
Чтобы догон знал, чей транскрипт перечитывать, в **папке дела** ведётся указатель сессий
|
||||
(напр. `docs/secretary/<work>/_sessions.json`: список `{session, lastSpanCursor}`), обновляемый stop-хуком
|
||||
на каждом ходе, пока дело активно. На re-активации этот указатель → список прошлых сессий для догона.
|
||||
Сам прогон догона (вызовы модели) делает **stop-хук** на первом Stop после активации (prompt-хук лишь ставит
|
||||
пометку «догнать сессии X»), чтобы вся LLM-машинерия осталась в одном месте.
|
||||
|
||||
---
|
||||
|
||||
## E. Контракт (что должно стать правдой)
|
||||
|
||||
- `parseLastExchange`/новый ассемблер распознаёт `isApiErrorMessage`, `isCompactSummary` и текст
|
||||
`[Request interrupted by user…]`; НЕ берёт их за настоящий промпт; находит настоящие промпты как границы обменов.
|
||||
- Сырьё пересобирается из всего транскрипта → прерванные ходы не теряются; настоящий промпт владельца сохранён
|
||||
(он финализируется в момент отправки, до ответа).
|
||||
- Ход-продолжение (после метки-обрыва) не создаёт новый спан; склеивается с предыдущей настоящей просьбой;
|
||||
спан помечен «(связь прерывалась — продолжено)».
|
||||
- Незавершённый прерванный хвост помечен «(прервана, не завершена)».
|
||||
- Догон при re-активации дела дописывает недоразобранный хвост прошлой (умершей) сессии.
|
||||
- Модель не решает смысл обрыва (детерминированно по меткам).
|
||||
|
||||
---
|
||||
|
||||
## F. Карта файлов (что трогать)
|
||||
|
||||
| Файл | Правка |
|
||||
|---|---|
|
||||
| `tools/secretary-transcript.mjs` | новый/расширенный ассемблер ВСЕГО транскрипта по обменам; классификация user-записи (настоящий промпт / meta / метка-обрыв / выжимка); флаг продолжения по позиции относительно метки. |
|
||||
| `tools/secretary-stop-hook.mjs` | «пересобрать сырьё из всего транскрипта» вместо «дописать последний обмен»; обновлять указатель сессий дела (D5); выполнять догон (D4) при пометке от prompt-хука. |
|
||||
| `tools/secretary-layer1.mjs` | `buildRawRecord` — ярлычок `cont=1` для хода-продолжения; `realBoundariesFromRaw` — исключать `cont=1` из границ (как `meta=1`). |
|
||||
| `tools/secretary-prompt-hook.mjs` | на «включи секретаря `<дело>`» — поставить пометку «догнать прошлые сессии дела» по указателю (D5). |
|
||||
| `tools/secretary-span.mjs` | без изменений логики; кормится честным сырьём. Рендер пометок «прерывалось/продолжено» и «не завершена». |
|
||||
| `tools/secretary-distill.mjs` | без изменений логики (общий разбор спана). |
|
||||
| `tools/secretary-protocol.mjs` (рендер) | показ честных пометок в спане. |
|
||||
| `tools/secretary-*.test.mjs` | TDD-фикстуры на каждый вид обрыва (см. §G). |
|
||||
|
||||
---
|
||||
|
||||
## G. Тесты (TDD — фикстуры под каждый вид обрыва)
|
||||
|
||||
1. **Сбой API + продолжение:** транскрипт с `isApiErrorMessage` посередине работы и настоящим промптом-продолжением
|
||||
после → один спан под настоящей первой просьбой, пометка «продолжено»; «продолжи»/реплика-продолжение НЕ
|
||||
стала отдельным спаном.
|
||||
2. **Ручной стоп (обе формы) + продолжение:** `[Request interrupted by user]` и `…for tool use` → то же склеивание.
|
||||
3. **Прерван и не продолжен:** обрыв в конце транскрипта без продолжения → пометка «(прервана, не завершена)».
|
||||
4. **Выжимка контекста:** `isCompactSummary` в транскрипте не становится ни промптом, ни работой.
|
||||
5. **Служебный ход:** `isMeta` (регресс — не сломать существующее) не граница.
|
||||
6. **Догон между сессиями:** сырьё прошлой сессии перестроено из её транскрипта; недоразобранный хвост дописан
|
||||
в тетрадь дела на re-активации.
|
||||
7. **Полнота:** пересборка сырья из транскрипта не теряет ни один ход (сверка число обменов ↔ настоящие промпты).
|
||||
|
||||
---
|
||||
|
||||
## H. Приёмка
|
||||
|
||||
- **Полный свод секретаря зелёный** (11 тест-файлов, команда из хендоффа §1, плюс новые тесты §G):
|
||||
```
|
||||
npx vitest run tools/secretary-reconcile.test.mjs tools/secretary-layer1.test.mjs tools/secretary-protocol.test.mjs tools/secretary-index.test.mjs tools/secretary-audit.test.mjs tools/secretary-hookutil.test.mjs tools/secretary-transcript.test.mjs tools/secretary-flag.test.mjs tools/secretary-prompt-hook.test.mjs tools/secretary-span.test.mjs tools/secretary-distill.test.mjs
|
||||
```
|
||||
- **ЖИВОЙ прогон** с реальным `SECRETARY_LLM_KEY` (aitunnel, `SECRETARY_LLM_MODEL`), где есть обрыв:
|
||||
(а) сбой API + «продолжи», (б) ручной стоп. Убедиться: настоящий промпт владельца **в тетради** (а не «продолжи»);
|
||||
прерванная работа помечена честно; одна работа не раздроблена.
|
||||
- Сверка: ничего из работы/диалога не потеряно (транскрипт ↔ тетрадь).
|
||||
|
||||
---
|
||||
|
||||
## I. Вне области (контекст §8 хендоффа — НЕ реализуется здесь)
|
||||
|
||||
Поднято в другой сессии, отдельные дела со своим брейнштормом/спекой — **не трогаем**:
|
||||
- (A) дословное логирование промптов/ответов наставника/судьи/роутера (подсистема стены, не секретарь);
|
||||
- (Б) качество модели секретаря (deepseek-v4-flash слабо поднимает «Решения/волю» — вопрос смены `SECRETARY_LLM_MODEL`).
|
||||
Reference in New Issue
Block a user