diff --git a/.gitignore b/.gitignore index 62ccde4..a53e979 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules/ dist/ *.tsbuildinfo .DS_Store +*.ndjson +*.jsonl diff --git a/BACKLOG.md b/BACKLOG.md index c54eebe..bbf9f50 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -41,7 +41,16 @@ ## Открыто -_Новые пункты — ниже (следующий id: **B-003**)._ +_Новые пункты — ниже (следующий id: **B-004**)._ + +### B-003 · open · e2e · 2026-06-18 + +- **Сценарий:** Длинная сессия `gitlab-mr-review` + pi-auto-compact; auto compaction в ходе Phase 6 review (issue #480334) +- **Проблема:** Второй подряд auto compaction не re-inject'ит tracked skill; `/skill-reinject` status показывает `last compaction: none` +- **Место:** `session_before_compact` / `session_compact` / defer + `before_agent_start`; артефакт `lost-reinject.jsonl` +- **Факт:** Compaction #1 (07:25:03): `lastCompactionSource: auto`, `skill-reinject:inject` есть, follow-up «Auto-compact ran before this turn». Compaction #2 (+11s, mid-turn после `ls`): `lastCompactionSource: null`, inject нет, агент продолжил без user-prompt. Skill `gitlab-mr-review` вне kept window (оригинальный block до `firstKeptEntryId`; reinject — `custom_message`, не user) +- **Обход:** `/skill-reinject now` (не проверялось в этом прогоне) +- **Предложение:** Phase 15 в `TODO.md` — fallback детекции source на `session_compact` + mid-turn доставка defer вне `before_agent_start` + kept-window учитывает `skill-reinject:inject` --- diff --git a/TODO.md b/TODO.md index 292905b..9ce8ebc 100644 --- a/TODO.md +++ b/TODO.md @@ -106,8 +106,9 @@ | 12 | Edge cases и полировка | 7–11 | §11 | | 13 | Приёмка и документация | 0–12 | §12–13 | | 14 | Re-inject для `--skill` / не-discovery skills (B-002) | 7, 9, 10, 13 | §5.2, §6.2, §11; BACKLOG B-002 | +| 15 | Mid-turn / пропущенный compaction (B-003) | 7, 8, 12, 14 | §5.2, §6.4–6.5, §8, §13, §16; BACKLOG B-003 | -**Порядок:** фазы 0→13; внутри фазы — сверху вниз. Параллельно после фазы 0 можно вести 1 и 2; фазы 3 и 4 — независимы друг от друга. +**Порядок:** фазы 0→13; внутри фазы — сверху вниз. Параллельно после фазы 0 можно вести 1 и 2; фазы 3 и 4 — независимы друг от друга. Фаза 15 — после 14. --- @@ -272,6 +273,46 @@ --- +### Фаза 15 — Mid-turn compaction и пропуск reinject (B-003) + +Закрывает [BACKLOG B-003](./BACKLOG.md#b-003--open--e2e--2026-06-18): второй auto compaction подряд не re-inject'ит skill; status `last compaction: none`. Артефакт: `lost-reinject.jsonl` (два compaction за ~11s; первый OK, второй — нет). + +**Корень (два связанных сбоя):** + +1. **Gate источника compaction** — `lastCompactionSource` выставляется из `pendingCompactionSource` только если до `session_compact` успел отработать `session_before_compact`. На втором compaction в логе `lastCompactionSource: null` → `shouldReinject = false` → очередь сброшена, reinject не планировался. +2. **Дыра defer-доставки** — consume только на `before_agent_start` (новый user prompt). Mid-turn compaction внутри активного agent loop (после toolResult, без «Auto-compact ran…») не даёт `before_agent_start` до следующего сообщения пользователя → skill теряется до ручного `/skill-reinject now`. + +**Дополнительно (двойной compact):** `skillsPresentInKeptWindow` не видит `custom_message` `skill-reinject:inject` → после успешного reinject skill формально «вне kept», и при корректном gate второй compact снова планирует reinject (лишний inject, не причина пропуска в B-003, но усиливает хрупкость). + +**Целевое поведение:** каждый auto `session_compact` либо re-inject'ит отсутствующие в kept skills, либо явно skip с причиной в debug; status не показывает ложное `none` после auto compact; нет гонки с pi-auto-compact на turn-boundary path. + +**Стратегия доставки (две ветки, одна точка решения на `session_compact`):** + +| Условие на `session_compact` | Доставка defer | +|------------------------------|----------------| +| `ctx.isIdle()` | Как сейчас: `pendingReinject` → consume на следующем `before_agent_start` (pi-auto-compact follow-up / user prompt) | +| `!ctx.isIdle()` (mid-turn) | Немедленно: `pi.sendMessage({ customType: "skill-reinject:inject", … }, { deliverAs: "steer" })` — в очередь до следующего LLM-вызова в том же turn; **не** `sendUserMessage` в `session_compact` (§16.2) | + +**Стратегия source (fallback §8):** manual — только явный `/compact` в `input`; всё остальное на `session_compact` с `pendingCompactionSource === null` → считать `auto` (вызов `ensureCompactionSourceMarked` и в `session_before_compact`, и в `session_compact` как safety net). + +- [ ] **SPEC phase 15** — §8: fallback infer `auto` на `session_compact`; §6.5.1: mid-turn defer через `sendMessage`/`steer`; §6.4: kept учитывает `skill-reinject:inject`; §16.6: double compact + mid-turn; §13: критерий «второй compact подряд»; зачем: контракт до кода +- [ ] **compaction.ts ensureSource** — `ensureCompactionSourceMarked(runtime)`: если не `manual` → `auto`; вызывать из `markAutoCompactionBeforeCompact` и экспортировать для `session_compact`; зачем: закрыть `lastCompactionSource: null` (B-003 факт #1) +- [ ] **index.ts compact source fallback** — в `handleSessionCompact` до `consumeCompactionOnSessionCompact`: `ensureCompactionSourceMarked(compactionRuntime)`; зачем: safety net когда `session_before_compact` не пришёл +- [ ] **test/compaction-source-fallback.test.ts** — (a) `session_compact` без prior `before_compact`: source `auto`, `shouldReinject` true; (b) prior `manual` из input: остаётся manual, reinject off по default; зачем: регрессия gate +- [ ] **kept.ts reinject custom** — `skillsPresentInKeptWindow` (или сосед) учитывает entries `type: custom_message`, `customType: skill-reinject:inject` с ` 0 && !ctx.isIdle()` → steer deliver; иначе оставить pending для `before_agent_start`; persist; зачем: единая развилка idle / mid-turn +- [ ] **index.ts before_agent_start dedup** — `tryConsumeDeferredReinject` / wire: skip consume если steer уже доставил для `lastCompactionFirstKeptEntryId` / compaction entry id; зачем: turn-boundary compact + pi-auto-compact не дают двойной inject +- [ ] **test/reinject-mid-turn.test.ts** — mock `pi.sendMessage`: mid-turn (`isIdle: false`) → steer вызван, pending очищен; idle → steer не вызывается; после steer `before_agent_start` не inject'ит повторно; зачем: §12.1 +- [ ] **diag.ts mid-turn** — расширить snapshot: `compactionSource`, `sourceInferred`, `deliveryBranch: "before_agent_start" \| "steer"`, `isIdle`; фазы `session_compact` + опционально `mid_turn_deliver`; зачем: отладка без повторения lost-reinject +- [ ] **scripts/b003-repro.mjs** — RPC/скрипт: симулировать или документировать repro по `lost-reinject.jsonl` (два compact, проверка inject count ≥ 1 на каждый compact с skill вне kept); зачем: регрессионный gate B-003 +- [ ] **manual E2E B-003** — длинная сессия или уменьшенный `keepRecentTokens`; два auto compact подряд (в т.ч. mid-turn); после каждого — skill в контексте или `debug` показывает `skipped-kept`; `/skill-reinject` → `last compaction: auto`; зачем: критерий закрытия B-003 +- [ ] **README mid-turn** — короткий подпункт: defer + mid-turn steer, coexistence с pi-auto-compact, что делать при `last compaction: none` (включить `debug`); зачем: §9.1 +- [ ] **BACKLOG close B-003** — `open` → `done` с датой и ссылкой на коммиты фазы 15; перенести в «Закрыто»; коммит `BACKLOG: …`; зачем: `dev-backlog.mdc` + +--- + ## После v1 (не блокирует фазы 0–13) Зафиксировано в SPEC §14 — не включать в чеклист v1, только при отдельном запросе: