Files
grayhook a07ddefb23 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 <cursoragent@cursor.com>
2026-06-18 22:55:31 +07:00

610 lines
36 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ТЗ: pi-skill-reinject
Extension для [Pi Coding Agent](https://github.com/earendil-works/pi), который отслеживает уже вызванные skills и повторно инжектит их в контекст после auto compaction.
**Статус:** draft
**Репозиторий:** `pi-auto-reinjection`
**Целевая платформа:** upstream Pi (`earendil-works/pi`), не форки (oh-my-pi и т.п.)
---
## 1. Проблема
Pi использует progressive disclosure для skills: в system prompt всегда только name + description, полный `SKILL.md` попадает в контекст через `read` или `/skill:name` ([skills.md](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/skills.md)).
При compaction старые сообщения суммаризуются; полный текст skill-блоков из «сжатой» части истории теряется ([compaction.md](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/compaction.md)). После auto compaction:
- descriptions skills остаются в system prompt;
- модель **не обязана** снова читать `SKILL.md` ([skills.md — «models don't always do this»](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/skills.md));
- встроенного механизма re-inject в upstream Pi нет.
---
## 2. Цель
Универсальный extension, который:
1. **Отслеживает** skills, реально использованные в сессии.
2. **После auto compaction** повторно инжектит их в контекст (как при `/skill:name`).
3. **По умолчанию выключен**; включается командой на уровне сессии или глобально (навсегда).
---
## 3. Не-цели (v1)
- Замена/обёртка стандартного compaction summary.
- Re-inject после **ручного** `/compact` (опционально в v2, см. §8).
- Поддержка oh-my-pi-специфики (`compaction.autoContinue`, snapcompact и т.д.).
- Re-inject skills, которые модель «прочитала» через `read`, но пользователь явно не вызывал — только если включена опция `trackReadPaths` (см. §6.2, по умолчанию `true`).
- Изменения в сторонних extensions (в т.ч. [pi-auto-compact](https://github.com/capyup/pi-auto-compact)) — только опциональная интеграция на нашей стороне с автоопределением (§16).
---
## 4. Ссылки на документацию Pi
| Тема | Ссылка |
|------|--------|
| Extensions (API, события, команды) | [packages/coding-agent/docs/extensions.md](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md) |
| Compaction (когда срабатывает, что теряется) | [packages/coding-agent/docs/compaction.md](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/compaction.md) |
| Skills (вызов, формат, progressive disclosure) | [packages/coding-agent/docs/skills.md](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/skills.md) |
| Settings (глобальные / project overrides) | [packages/coding-agent/docs/settings.md](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/settings.md) |
| Примеры extensions | [packages/coding-agent/examples/extensions/](https://github.com/earendil-works/pi/tree/main/packages/coding-agent/examples/extensions) |
| `sendUserMessage` | [send-user-message.ts](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/send-user-message.ts) |
| Custom compaction hook | [custom-compaction.ts](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/custom-compaction.ts) |
| `parseSkillBlock` (формат skill-блока) | [agent-session.ts](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/agent-session.ts) |
| `_expandSkillCommand` (как Pi разворачивает `/skill:name`) | [agent-session.ts](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/agent-session.ts) |
| Agent Skills standard | [agentskills.io](https://agentskills.io/specification) |
| **pi-auto-compact** (совместимость) | [github.com/capyup/pi-auto-compact](https://github.com/capyup/pi-auto-compact) |
| pi-auto-compact исходник | [extensions/auto-compact.ts](https://github.com/capyup/pi-auto-compact/blob/main/extensions/auto-compact.ts) |
| Inter-extension events | [event-bus.ts example](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/event-bus.ts) |
| `before_agent_start` (inject message) | [extensions.md — before_agent_start](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md) |
---
## 5. Поведение
### 5.1. Режимы включения
Три независимых слоя (приоритет сверху вниз):
| Слой | Хранение | Default | Описание |
|------|----------|---------|----------|
| **Session override** | `pi.appendEntry("skill-reinject:config", …)` | `null` (нет override) | Вкл/выкл только в текущей сессии |
| **Global default** | `~/.pi/agent/settings.json``skillReinject.enabled` | `false` | «Навсегда» для всех новых сессий |
| **Effective** | вычисляется | `false` | `sessionOverride ?? globalDefault` |
Команда `/skill-reinject` (см. §7) меняет session override или global default.
**Важно:** session override не переживает `/resume` в другую сессию — восстанавливается из entries этой сессии. Global default читается при `session_start`.
### 5.2. Когда делать re-inject
Триггер: событие extension **`session_compact`** ([extensions.md — session_before_compact / session_compact](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md)).
Условия (все должны выполниться):
1. `effectiveEnabled === true`
2. Compaction **не** отменён (`session_compact` успешно отработал)
3. Источник compaction = **auto** (`threshold` или `overflow`), не manual `/compact`
→ см. §8 про детекцию источника
4. Есть хотя бы один tracked skill, **отсутствующий** в kept-части контекста (§6.4)
5. Skill всё ещё зарегистрирован в `resourceLoader` (не удалён с диска)
**Не** делать re-inject:
- при `session_before_compact` с `cancel: true`;
- если список skills для re-inject пуст;
- если агент в момент `session_compact` стримит и `willRetry === true` (overflow recovery) — отложить до `agent_end` или использовать `deliverAs: "followUp"` (§6.5).
### 5.3. Что именно инжектить
Повторять поведение Pi `_expandSkillCommand`: user message с блоком
```xml
<skill name="{name}" location="{filePath}">
References are relative to {baseDir}.
{body без YAML frontmatter}
</skill>
```
Опционально добавлять служебный суффикс (конфигурируемо):
```text
[skill-reinject] Re-applied after compaction. Follow this skill's workflow.
```
**Не** использовать цепочку `/skill:a /skill:b` в одном сообщении — Pi разворачивает только один `/skill:` на сообщение.
Для N skills при доставке через `sendUserMessage`: отдельное сообщение на skill, порядок = порядок первого вызова в сессии.
При доставке через `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).
---
## 6. Отслеживание skills
### 6.1. Структура состояния
```typescript
interface TrackedSkill {
name: string; // frontmatter name
filePath: string; // абсолютный путь к SKILL.md
baseDir: string; // директория skill
firstSeenAt: number; // timestamp
lastSeenAt: number;
sources: Array<"slash" | "skill-block" | "read">;
}
interface ExtensionState {
version: 1;
sessionOverride: boolean | null;
skills: TrackedSkill[]; // dedupe by name, preserve insertion order
lastCompactionSource: "auto" | "manual" | null;
/** Skills, ожидающие re-inject (§6.5): idle → before_agent_start; mid-turn → steer на session_compact */
pendingReinject: string[]; // skill names
}
/** Runtime-only, не персистить */
interface RuntimeFlags {
autoCompactDetected: boolean;
autoCompactIntegration: "auto" | "defer" | "immediate" | "off";
}
```
In-memory кэш + персистенция через `pi.appendEntry` при каждом изменении tracked skills / session override.
### 6.2. Источники детекции
| # | Событие | Условие | `source` |
|---|---------|---------|----------|
| 1 | `input` | `event.text` matches `/^\/skill:([a-z0-9-]+)/` | `slash` |
| 2 | `message_end` (user) | текст содержит `<skill name="…"` (regex из `parseSkillBlock`) | `skill-block` |
| 3 | `tool_call` / `tool_result` | `toolName === "read"` и `path` совпадает с `SKILL.md` известного skill | `read` |
Для (3): сопоставлять `path` с `skill.filePath` из `ctx` / `getSkills()` ([formatSkillsForPrompt](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/skills.ts)).
Опция `skillReinject.trackReadPaths` (default `true`): если `false`, игнорировать детекцию через `read`.
### 6.3. Восстановление при `session_start` / `/resume`
На `session_start` (включая `reason: "reload"`):
1. Пройти `ctx.sessionManager.getBranch()`:
- entries `type === "custom"` с `customType === "skill-reinject:state"` → восстановить state;
- user messages → re-scan skill blocks;
2. Если entries state нет — full rescan ветки.
### 6.4. Skills уже в kept-контексте
После compaction часть recent messages сохраняется (`keepRecentTokens`, default 20k — [settings.md](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/settings.md)).
Перед re-inject:
1. Взять entries от `compactionEntry.firstKeptEntryId` до хвоста ветки.
2. Для каждого tracked skill: если в kept window есть `<skill name="{name}"`**пропустить** re-inject для этого skill.
3. Источники текста в kept slice:
- user messages (`type === "message"`, `role === "user"`);
- наши reinject-сообщения: `type === "custom_message"` с `customType === "skill-reinject:inject"` (§6.5.1) — skill-блоки в `content`, не в user role.
Пункт 3 закрывает ложное «skill вне kept» после успешного defer-reinject: `custom_message` не user message, но skill уже в контексте turn'а. Без этого второй compaction подряд снова планирует reinject (лишний inject; см. §16.6).
### 6.5. Доставка после compaction
Два режима доставки (выбор — §6.5.1 / §16):
#### 6.5.1. Режим `defer` (рекомендуемый по умолчанию при наличии pi-auto-compact)
1. На `session_compact`: записать имена skills в `state.pendingReinject` (не вызывать `sendUserMessage`).
2. Доставка — **одна точка решения** на `session_compact` по `ctx.isIdle()`:
| Условие на `session_compact` | Доставка |
|------------------------------|----------|
| `ctx.isIdle()` | Оставить `pendingReinject`; consume на следующем `before_agent_start` (kickoff pi-auto-compact follow-up / user prompt) |
| `!ctx.isIdle()` (mid-turn compaction) | Немедленно: `pi.sendMessage({ customType: "skill-reinject:inject", content, display: true }, { deliverAs: "steer" })`; очистить `pendingReinject`; записать `compactionEntry.id` в runtime, чтобы `before_agent_start` **не** дублировал inject для этого compaction |
3. На `before_agent_start` (idle-path): если `pendingReinject` не пуст и steer для этого compaction ещё не доставлялся — вернуть injected `message` с `customType: "skill-reinject:inject"` и объединёнными skill-блоками, очистить очередь.
**Mid-turn steer:** `deliverAs: "steer"` ставит skill-блоки в очередь до следующего LLM-вызова **в том же turn** (после tool result, без user prompt). Это закрывает сценарий, когда pi-auto-compact компактирует mid-turn и **не** шлёт follow-up (агент не idle) — consume только на `before_agent_start` теряет skill до следующего сообщения пользователя (B-003).
**Не** вызывать `sendUserMessage` в `session_compact` (§16.2). Steer — не user message и не конкурирует с pi-auto-compact follow-up на turn boundary.
На turn-boundary path skills попадают в контекст **в том же turn**, что и kickoff-сообщение (в т.ч. auto-continue от pi-auto-compact), **без гонки** `sendUserMessage`.
#### 6.5.2. Режим `immediate` (`sendUserMessage`)
Использовать `pi.sendUserMessage` ([extensions.md — sendUserMessage](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md)):
| Ситуация | Стратегия |
|----------|-----------|
| Агент idle после compaction | Первый skill → `sendUserMessage(expandedBlock)`; остальные → `followUp` |
| Overflow recovery (`willRetry`) | Все skills → `sendUserMessage(..., { deliverAs: "followUp" })` |
| Агент стримит | `deliverAs: "followUp"` для всех |
**Не использовать `immediate`**, если обнаружен [pi-auto-compact](https://github.com/capyup/pi-auto-compact) и `autoCompactIntegration !== "off"` — см. §16.
#### 6.5.3. Выбор режима
| Условие | Режим доставки |
|---------|----------------|
| `autoCompactIntegration: "defer"` | всегда `defer` |
| `autoCompactIntegration: "immediate"` | всегда `sendUserMessage` |
| `autoCompactIntegration: "off"` | по `triggerTurn` (§7.3) |
| `autoCompactIntegration: "auto"` (default) + pi-auto-compact обнаружен | `defer` |
| `autoCompactIntegration: "auto"` + pi-auto-compact **не** обнаружен + `triggerTurn: false` | `defer` (ждать следующий user prompt) |
| `autoCompactIntegration: "auto"` + pi-auto-compact **не** обнаружен + `triggerTurn: true` | `immediate` |
**Важно:** встроенный Pi auto-compaction ([`_runAutoCompaction`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/agent-session.ts)) при `reason: "threshold"` **не** продолжает turn (`willRetry: false`). С установленным pi-auto-compact продолжение обеспечивает **он**, не Pi — наш `defer` рассчитан на этот сценарий.
---
## 7. Команда `/skill-reinject`
Зарегистрировать через `pi.registerCommand("skill-reinject", …)` ([extensions.md — registerCommand](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md)).
### 7.1. Синтаксис
```text
/skill-reinject # статус
/skill-reinject on # вкл в этой сессии
/skill-reinject off # выкл в этой сессии
/skill-reinject reset # сброс session override → global default
/skill-reinject global on # вкл навсегда (~/.pi/agent/settings.json)
/skill-reinject global off # выкл навсегда
/skill-reinject list # tracked skills
/skill-reinject clear # очистить tracked skills (не трогает toggle)
/skill-reinject now # принудительный re-inject (debug)
/skill-reinject integration auto|defer|immediate|off # режим доставки (§6.5, §16)
```
Алиасы (опционально): `/sr`, `/skills-reinject`.
### 7.2. Вывод `/skill-reinject` (status)
```text
skill-reinject: off (session override) | on (global) | on (session)
delivery: defer (pi-auto-compact detected) | immediate | defer
tracked: 2 skills — redmine-issue-context, fup-blame-commits
pending reinject: 0
last compaction: auto @ 14:32
```
Footer status через `ctx.ui.setStatus("skill-reinject", "on·2")` ([status-line.ts example](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/examples/extensions/status-line.ts)).
### 7.3. Global settings
Писать в `~/.pi/agent/settings.json` (merge, не затирать файл целиком):
```json
{
"skillReinject": {
"enabled": false,
"trackReadPaths": true,
"triggerTurn": false,
"reinjectOnManualCompaction": false,
"autoCompactIntegration": "auto",
"suffix": "[skill-reinject] Re-applied after compaction."
}
}
```
| Поле | Default | Описание |
|------|---------|----------|
| `autoCompactIntegration` | `"auto"` | `"auto"` \| `"defer"` \| `"immediate"` \| `"off"` — см. §6.5.3, §16 |
Project override: `.pi/settings.json` → тот же ключ (merged по правилам Pi, [settings.md — Project Overrides](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/settings.md)).
---
## 8. Детекция auto vs manual compaction
Extension event `session_compact` **не** передаёт `reason` ([SessionCompactEvent](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/src/core/extensions/types.ts)).
**Стратегия v1:**
```typescript
// input handler (до expansion) — единственный путь manual
if (text.startsWith("/compact")) pendingCompactionSource = "manual";
function ensureCompactionSourceMarked(runtime) {
if (runtime.pendingCompactionSource !== "manual") {
runtime.pendingCompactionSource = "auto";
}
}
// session_before_compact
ensureCompactionSourceMarked(runtime);
// session_compact — safety net, если before_compact не пришёл (mid-turn, гонка хуков)
ensureCompactionSourceMarked(runtime);
const shouldReinject =
effectiveEnabled &&
(pendingCompactionSource === "auto" || settings.reinjectOnManualCompaction);
state.lastCompactionSource = pendingCompactionSource;
pendingCompactionSource = null;
```
**Fallback на `session_compact`:** если `session_before_compact` не отработал, `pendingCompactionSource` остаётся `null` → без fallback `lastCompactionSource: null`, `shouldReinject === false`, status `last compaction: none` при реальном auto compact (B-003). Всё, что не помечено явным `/compact` в `input`, на `session_compact` считается **auto**.
Ручной RPC compact (`{type: "compact"}`) тоже помечать как `manual` если extension получает соответствующий сигнал (v2).
---
## 9. Архитектура файлов
```
pi-auto-reinjection/
├── SPEC.md # это ТЗ
├── README.md
├── package.json # pi package manifest (опционально npm)
├── src/
│ ├── index.ts # export default function(pi: ExtensionAPI)
│ ├── state.ts # load/save/merge state, appendEntry
│ ├── detect.ts # skill detection helpers
│ ├── expand.ts # expand skill → block (mirror Pi logic)
│ ├── reinject.ts # post-compaction reinject orchestration
│ ├── auto-compact.ts # detect @capyup/pi-auto-compact, delivery mode (§16)
│ ├── settings.ts # read/write skillReinject.* in settings.json
│ └── commands.ts # /skill-reinject handler
├── test/
│ ├── detect.test.ts
│ ├── kept-window.test.ts
│ └── expand.test.ts
└── .gitignore
```
### 9.1. Установка (целевая)
```bash
# dev
pi -e ~/Documents/repos/pi-auto-reinjection/src/index.ts
# production
cp -r src/index.ts ~/.pi/agent/extensions/skill-reinject.ts
# или через pi package / settings.extensions
```
См. [extensions.md — Extension Locations](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md): `~/.pi/agent/extensions/`, `.pi/extensions/`.
---
## 10. Зависимости
```json
{
"pi": {
"extensions": ["./src/index.ts"]
},
"devDependencies": {
"@earendil-works/pi-coding-agent": "workspace:* или latest",
"typescript": "^5"
}
}
```
Импорты только из публичного API Pi ([extensions.md — Available Imports](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md)).
Для expand skill body: `readFileSync` + strip YAML frontmatter (как Pi), без дублирования приватных internal imports если возможно; иначе — локальная копия логики с комментарием «mirror agent-session._expandSkillCommand».
---
## 11. Edge cases
| Case | Поведение |
|------|-----------|
| Skill удалён с диска | skip + `ui.notify` warning |
| Skill `disable-model-invocation: true` | re-inject всё равно (explicit inject) |
| Два skill с одним name (collision) | использовать первый из resourceLoader; warn |
| Compaction во время стрима | `followUp` delivery |
| Пустой tracked list | no-op |
| `/tree` branch switch | state из entries новой ветки; rescan |
| `session_shutdown` | flush pending appendEntry |
| RPC / print mode (`hasUI === false`) | команды работают; notify → no-op; reinject без UI feedback |
---
## 12. Тестирование
### 12.1. Unit
- `parseSkillBlock` / detect в user text
- kept-window: skill in / not in kept messages
- expand: frontmatter strip, path resolution
- settings merge read/write
### 12.2. Manual E2E (standalone)
1. `pi` + extension, `/skill-reinject on`
2. `/skill:some-skill` → длинная сессия → дождаться auto compaction (или уменьшить `keepRecentTokens` / `reserveTokens` в `.pi/settings.json`)
3. Проверить: после compaction в контексте снова есть `<skill name="some-skill"…>`
4. `/skill-reinject off` → compaction → re-inject не происходит
5. `/skill-reinject global on` → новая сессия → re-inject без `on`
### 12.3. Manual E2E (с pi-auto-compact)
Предусловие: `pi install npm:@capyup/pi-auto-compact` ([README](https://github.com/capyup/pi-auto-compact/blob/main/README.md)).
1. Оба extension загружены; `/skill-reinject on`
2. `/skill-reinject` → status показывает `delivery: defer (pi-auto-compact detected)`
3. `/skill:some-skill` → сессия до срабатывания pi-auto-compact (порог по умолчанию 90% context window)
4. После auto-compaction:
- pi-auto-compact отправляет follow-up (*«Auto-compact ran… Continue…»*);
- агент **продолжает** работу без idle;
- в контексте turn'а есть skill-блок(и) **до** follow-up текста
5. В логах/TUI **нет** ошибки `Agent is already processing` / `Failed to send queued message`
6. Ручной `/compact` → pi-auto-compact **не** шлёт follow-up → re-inject только если `reinjectOnManualCompaction: true`, иначе `pendingReinject` сбрасывается на следующем user prompt
7. Во время compaction пользователь печатает сообщение → pi-auto-compact молчит → re-inject через `before_agent_start` на turn пользователя
---
## 13. Критерии приёмки (v1)
- [ ] Default off; `/skill-reinject on` включает re-inject в сессии
- [ ] `/skill-reinject global on` сохраняет в `~/.pi/agent/settings.json` и переживает restart
- [ ] После **auto** compaction re-injectятся все tracked skills, отсутствующие в kept window
- [ ] После **manual** `/compact` re-inject **не** происходит (при default `reinjectOnManualCompaction: false`)
- [ ] Tracked skills: `/skill:name`, skill-block в user msg, `read` на `SKILL.md`
- [ ] State переживает `/resume` той же сессии
- [ ] Footer status показывает on/off и count
- [ ] Нет duplicate skill blocks для skills уже в kept window (включая `skill-reinject:inject` в kept slice, §6.4)
- [ ] С [pi-auto-compact](https://github.com/capyup/pi-auto-compact): auto-detect, режим `defer`, нет гонки `sendUserMessage`, continue после compaction работает
- [ ] С pi-auto-compact: ручной `/compact` не ломает ни один extension
- [ ] **Два auto compaction подряд** (в т.ч. второй mid-turn без user prompt): после каждого — tracked skills в контексте или явный skip (`skipped-kept` в `debug`); `/skill-reinject``last compaction: auto`, не `none` (§8 fallback, §6.5.1 steer)
---
## 14. Будущие улучшения (v2+)
- `reinjectOnManualCompaction: true` как осознанный opt-in
- `session_before_compact`: дописывать в custom summary список active skills
- Опциональный `pi.events` протокол с pi-auto-compact (встраивать skill-блоки в follow-up текст — без PR в capyup не обязателен)
- Pi package на npm (`keywords: ["pi-package"]`)
- Опция re-inject только «последнего активного» skill, а не всех
---
## 15. Риски
| Риск | Митигация |
|------|-----------|
| Re-inject раздувает контекст (N больших skills) | `skillReinject.maxSkills` (default unlimited; soft warn > 3) |
| `sendUserMessage` запускает нежеланный turn | `defer` по умолчанию при pi-auto-compact; иначе `triggerTurn: false` |
| Гонка `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 |
---
## 16. Совместимость с [@capyup/pi-auto-compact](https://github.com/capyup/pi-auto-compact)
Типичная установка пользователя: `pi install npm:@capyup/pi-auto-compact`. Extension проактивно компактирует на `turn_start` / `turn_end` / emergency `context` и **автоматически продолжает** работу после auto-compaction.
### 16.1. Как работает pi-auto-compact (релевантное нам)
Источник: [extensions/auto-compact.ts](https://github.com/capyup/pi-auto-compact/blob/main/extensions/auto-compact.ts), [README](https://github.com/capyup/pi-auto-compact/blob/main/README.md).
| Аспект | Поведение |
|--------|-----------|
| Триггер compaction | `ctx.compact({ customInstructions, onComplete, onError })` — не ручной `/compact` |
| Follow-up после auto-compact | `onComplete``setImmediate(() => { if (ctx.isIdle()) pi.sendUserMessage(AUTO_COMPACT_FOLLOW_UP[phase]); })` |
| Ручной `/compact` | **Не** попадает в их `onComplete` → follow-up **не** отправляется |
| Тексты follow-up | `"Auto-compact ran before this turn. Continue with the current task."` и ещё 3 фазы (mid-turn, emergency, session-resume) |
| Гонки | Специально откладывают follow-up в `setImmediate`, чтобы не проиграть flush `compactionQueuedMessages` Pi |
| Keep budget | `keepRecentPercent` (default 15% context window), не `keepRecentTokens` Pi |
### 16.2. Конфликт без интеграции
Если skill-reinject на `session_compact` вызовет `sendUserMessage` синхронно или в том же `setImmediate`:
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`, не через конкурирующий `sendUserMessage` в `session_compact`. Idle: `before_agent_start`; mid-turn (`!isIdle`): `sendMessage` с `deliverAs: "steer"` (§6.5.1) — тот же инвариант §16.5, без user message.
### 16.3. Целевой совместный flow
```text
turn_start / turn_end / emergency
pi-auto-compact: ctx.compact()
├── session_before_compact (наш hook: mark source=auto)
├── … Pi summarization …
├── session_compact (наш hook: pendingReinject := skills kept)
│ │
│ ├── 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, idle-path)
├── inject: <skill>…</skill> × N ← re-inject (если не steer'нули)
└── user prompt: "Auto-compact ran…" ← kickoff pi-auto-compact
agent продолжает с skills + summary + kept tail
```
### 16.4. Автоопределение pi-auto-compact
Без зависимости от npm-пакета и без правок в capyup:
```typescript
function detectPiAutoCompact(pi: ExtensionAPI): boolean {
return pi.getCommands().some((c) => c.name === "auto-compact");
}
```
- Вызывать на `session_start` (`reason: "startup" | "resume" | "reload" | "switch"`).
- Кэшировать в `RuntimeFlags.autoCompactDetected`.
- При `false` → status `delivery: defer (standalone)` или `immediate` по `triggerTurn`.
**Не** полагаться на путь к файлу / `package.json` — только публичный API (`getCommands`).
Опция `skillReinject.autoCompactIntegration`:
| Значение | Поведение |
|----------|-----------|
| `"auto"` (default) | `defer` если `detectPiAutoCompact()`, иначе по `triggerTurn` |
| `"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` |
Команда `/skill-reinject integration auto|defer|immediate|off` — session override для `autoCompactIntegration` (персистить в `skill-reinject:config` entry).
### 16.5. Что мы **не** ломаем в pi-auto-compact
| Инвариант | Как обеспечиваем |
|-----------|------------------|
| Follow-up только когда idle | Не вызываем `sendUserMessage` в `session_compact` / `onComplete` tick |
| Follow-up только для своего auto-compact | Не трогаем `ctx.compact()` / `onComplete` |
| Ручной `/compact` без follow-up | Наш `pendingReinject` на manual: либо ждём user prompt (`defer`), либо skip (default) |
| Emergency `context` truncation | `session_compact` всё равно приходит после `ctx.compact()` — тот же `defer` |
### 16.6. Что pi-auto-compact **не** ломает в нас
| Сценарий | Результат |
|----------|-----------|
| 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`; 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
Оба механизма могут сработать в одной сессии (Pi: после `agent_end`; pi-auto-compact: до turn). Рекомендация в README:
- при использовании pi-auto-compact рассмотреть `"compaction.enabled": false` в [settings.json](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/settings.md) или оставить оба — skill-reinject отработает после **каждого** `session_compact`.
Extension **не** отключает чужой compaction; максимум — `ui.notify` hint при первом detect обоих.
### 16.8. Будущий опциональный протокол (v2, не блокирует v1)
Через [`pi.events`](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/extensions.md):
```typescript
// skill-reinject emits (optional, v2):
pi.events.emit("skill-reinject:pending", { skills: ["foo", "bar"] });
// pi-auto-compact could listen and append to follow-up — requires PR upstream
```
v1 **не требует** изменений в pi-auto-compact.
### 16.9. Константы для тестов (follow-up фразы pi-auto-compact)
Скопировать в `auto-compact.ts` как `PI_AUTO_COMPACT_FOLLOW_UP_PREFIXES` для документирования/тестов, **не** для матчинга в runtime v1:
- `"Auto-compact ran before this turn."`
- `"Auto-compact ran mid-turn."`
- `"Emergency auto-compact ran."`
- `"Auto-compact ran on session resume."`
Runtime v1 матчит follow-up **не нужен** — достаточно `pendingReinject` + любой следующий `before_agent_start`.