TODO: plan Phase 15 for mid-turn compaction reinject gap (B-003)

Document root cause and fix strategy from lost-reinject.jsonl analysis.
Add B-003 to BACKLOG; ignore *.ndjson and *.jsonl session dumps.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-18 22:41:59 +07:00
parent 7665096601
commit ca97660bac
3 changed files with 54 additions and 2 deletions
+2
View File
@@ -2,3 +2,5 @@ node_modules/
dist/ dist/
*.tsbuildinfo *.tsbuildinfo
.DS_Store .DS_Store
*.ndjson
*.jsonl
+10 -1
View File
@@ -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`
--- ---
+42 -1
View File
@@ -106,8 +106,9 @@
| 12 | Edge cases и полировка | 711 | §11 | | 12 | Edge cases и полировка | 711 | §11 |
| 13 | Приёмка и документация | 0–12 | §1213 | | 13 | Приёмка и документация | 0–12 | §1213 |
| 14 | Re-inject для `--skill` / не-discovery skills (B-002) | 7, 9, 10, 13 | §5.2, §6.2, §11; BACKLOG B-002 | | 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.46.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` с `<skill name="…"` в `content`; зачем: §6.4, не планировать лишний reinject после успешного inject
- [ ] **test/kept-window.test.ts reinject-custom** — inject custom в kept slice → skill считается present; вне slice → absent; зачем: double-compact dedup
- [ ] **reinject.ts mid-turn steer**`deliverDeferredReinjectSteer(pi, planned, …)` через `pi.sendMessage` + `DEFERRED_REINJECT_CUSTOM_TYPE`, `deliverAs: "steer"`; clear `pendingReinject`; флаг `compactionRuntime.deferredDeliveredForCompactionId` (или аналог) чтобы `before_agent_start` не дублировал; зачем: B-003 факт #2, §16.2-safe
- [ ] **index.ts mid-turn wire** — в defer-ветке `handleSessionCompact`: после enqueue, если `shouldReinject && planned.length > 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) ## После v1 (не блокирует фазы 0–13)
Зафиксировано в SPEC §14 — не включать в чеклист v1, только при отдельном запросе: Зафиксировано в SPEC §14 — не включать в чеклист v1, только при отдельном запросе: