From a07ddefb232d5c9dafb9faaa52aff58531b2adec Mon Sep 17 00:00:00 2001 From: GRayHook Date: Thu, 18 Jun 2026 22:55:31 +0700 Subject: [PATCH] Phase 15: SPEC contract for mid-turn compaction reinject (B-003) Document source fallback on session_compact, defer steer delivery when !isIdle, kept-window skill-reinject:inject entries, and double-compact acceptance criteria before implementation. Co-authored-by: Cursor --- SPEC.md | 73 +++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 20 deletions(-) diff --git a/SPEC.md b/SPEC.md index f99387a..20f1f37 100644 --- a/SPEC.md +++ b/SPEC.md @@ -118,7 +118,7 @@ References are relative to {baseDir}. Для N skills при доставке через `sendUserMessage`: отдельное сообщение на skill, порядок = порядок первого вызова в сессии. -При доставке через `before_agent_start` (§6.5, режим `defer`, в т.ч. с [pi-auto-compact](https://github.com/capyup/pi-auto-compact)): **одно** injected message со всеми skill-блоками подряд (меньше turn'ов, нет гонки с follow-up). +При доставке через `before_agent_start` или mid-turn `sendMessage`/`steer` (§6.5, режим `defer`, в т.ч. с [pi-auto-compact](https://github.com/capyup/pi-auto-compact)): **одно** injected message с `customType: "skill-reinject:inject"` и всеми skill-блоками подряд (меньше turn'ов, нет гонки с follow-up). --- @@ -141,7 +141,7 @@ interface ExtensionState { sessionOverride: boolean | null; skills: TrackedSkill[]; // dedupe by name, preserve insertion order lastCompactionSource: "auto" | "manual" | null; - /** Skills, ожидающие re-inject на следующем before_agent_start (§6.5) */ + /** Skills, ожидающие re-inject (§6.5): idle → before_agent_start; mid-turn → steer на session_compact */ pendingReinject: string[]; // skill names } @@ -182,7 +182,12 @@ In-memory кэш + персистенция через `pi.appendEntry` при Перед re-inject: 1. Взять entries от `compactionEntry.firstKeptEntryId` до хвоста ветки. -2. Для каждого tracked skill: если в kept user messages есть ` 3) | | `sendUserMessage` запускает нежеланный turn | `defer` по умолчанию при pi-auto-compact; иначе `triggerTurn: false` | -| Гонка `sendUserMessage` с pi-auto-compact follow-up | §16: не слать в `session_compact`; `before_agent_start` | +| Гонка `sendUserMessage` с pi-auto-compact follow-up | §16: не слать `sendUserMessage` в `session_compact`; idle → `before_agent_start`; mid-turn → `sendMessage`/`steer` (§6.5.1) | | Нельзя отличить auto/manual без эвристики | input hook на `/compact`; pi-auto-compact manual `/compact` не в его `onComplete` | | Двойной compaction (Pi default + pi-auto-compact) | документировать; опционально warn в status | | Pi изменит формат skill block | version в state; тест на `parseSkillBlock` regex | @@ -473,7 +501,7 @@ cp -r src/index.ts ~/.pi/agent/extensions/skill-reinject.ts 1. Агент перестаёт быть idle → pi-auto-compact **не** шлёт follow-up → **сессия замирает** после compaction. 2. Или оба шлют сообщения в одном tick → `"Agent is already processing"` ([комментарий в auto-compact.ts](https://github.com/capyup/pi-auto-compact/blob/main/extensions/auto-compact.ts)). -**Вывод:** при обнаружении pi-auto-compact re-inject **обязан** идти через `defer` + `before_agent_start`, не через конкурирующий `sendUserMessage`. +**Вывод:** при обнаружении pi-auto-compact re-inject **обязан** идти через режим `defer`, не через конкурирующий `sendUserMessage` в `session_compact`. Idle: `before_agent_start`; mid-turn (`!isIdle`): `sendMessage` с `deliverAs: "steer"` (§6.5.1) — тот же инвариант §16.5, без user message. ### 16.3. Целевой совместный flow @@ -483,15 +511,18 @@ turn_start / turn_end / emergency ▼ pi-auto-compact: ctx.compact() │ - ├── session_before_compact (наш hook: только mark source=auto) + ├── session_before_compact (наш hook: mark source=auto) ├── … Pi summarization … ├── session_compact (наш hook: pendingReinject := skills ∖ kept) - └── onComplete → setImmediate → sendUserMessage("Auto-compact ran…") + │ │ + │ ├── isIdle → pending для before_agent_start + │ └── !isIdle → sendMessage steer (§6.5.1), без follow-up + └── onComplete → setImmediate → if isIdle: sendUserMessage("Auto-compact ran…") │ ▼ - prompt() → before_agent_start (наш hook) + prompt() → before_agent_start (наш hook, idle-path) │ - ├── inject: × N ← re-inject + ├── inject: × N ← re-inject (если не steer'нули) └── user prompt: "Auto-compact ran…" ← kickoff pi-auto-compact │ ▼ @@ -519,7 +550,7 @@ function detectPiAutoCompact(pi: ExtensionAPI): boolean { | Значение | Поведение | |----------|-----------| | `"auto"` (default) | `defer` если `detectPiAutoCompact()`, иначе по `triggerTurn` | -| `"defer"` | всегда `before_agent_start`, даже без pi-auto-compact | +| `"defer"` | idle: `before_agent_start`; mid-turn (`!isIdle`): steer на `session_compact` (§6.5.1); никогда `sendUserMessage` в `session_compact` | | `"immediate"` | всегда `sendUserMessage` (для отладки; с pi-auto-compact — риск §16.2) | | `"off"` | игнорировать detect; только `triggerTurn` | @@ -538,10 +569,12 @@ function detectPiAutoCompact(pi: ExtensionAPI): boolean { | Сценарий | Результат | |----------|-----------| -| Auto-compact + follow-up | Следующий `before_agent_start` — наш inject; skills в контексте | -| User печатает во время compaction | pi-auto-compact молчит; user prompt → наш inject на его turn | +| Auto-compact + follow-up (idle) | Следующий `before_agent_start` — наш inject; skills в контексте | +| Auto-compact **mid-turn** (`!isIdle`, нет follow-up) | Steer на `session_compact` (§6.5.1); skills до следующего LLM-вызова в том же turn | +| User печатает во время compaction | pi-auto-compact молчит; user prompt → наш inject на его turn (`before_agent_start`) | | pi-auto-compact `keep-bookends` / `summarize-all` | Kept window определяем по `compactionEntry.firstKeptEntryId` в session branch (§6.4), не по их процентам | -| Два compaction подряд | Каждый `session_compact` пересчитывает `pendingReinject`; дедуп по kept window | +| Два compaction подряд | Каждый `session_compact` пересчитывает `pendingReinject`; source fallback §8; дедуп по kept window включая `skill-reinject:inject` (§6.4) | +| Turn-boundary compact после steer | `before_agent_start` не дублирует inject, если steer уже доставил для `compactionEntry.id` этого compaction | ### 16.7. Coexistence с Pi default auto-compaction