|
| 1 | +# 🔥 Отчёт: Расследование сломанной оркестрации — `new_task` не возвращает результаты |
| 2 | + |
| 3 | +**Дата:** 2026-06-04 |
| 4 | +**Автор:** Research Analyst (Mode: research-analyst) |
| 5 | +**Версия BUILD:** 🔥🚀BUILD🚀🔥 (based on pre-b7857bcd6 codebase) |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## 1. Executive Summary |
| 10 | + |
| 11 | +**Коренная причина: Многофакторная регрессия.** BUILD содержит только ОДИН cherry-pick из цепочки upstream фиксов делегирования (`3c1895409`), в то время как 10+ критически важных коммитов, включая монументальный upstream-фикс на 20 файлов / 1714 строк (`b7857bcd6`), отсутствуют. |
| 12 | + |
| 13 | +Оркестрация работает "через раз" потому, что некоторые гонки иногда выигрываются, иногда нет — в зависимости от тайминга, загрузки CPU и состояния globalState. |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## 2. Сравнение веток |
| 18 | + |
| 19 | +### Коммиты в BUILD (оркестрационные) |
| 20 | + |
| 21 | +| Commit | Что делает | Статус | |
| 22 | +| ----------- | ------------------------------------------------------------------------------------------------ | ---------- | |
| 23 | +| `3c1895409` | 🔴 СУПЕРВАЖНЫЙ ФИКС: убран `cancelledDelegationChildIds` из guard в `reopenParentFromDelegation` | ✅ В BUILD | |
| 24 | +| | Exempt subtask from `didToolFailInCurrentTurn` guard | ✅ В BUILD | |
| 25 | +| | Добавлен `console.warn` при неудачном reopen | ✅ В BUILD | |
| 26 | + |
| 27 | +### Критические коммиты НЕ в BUILD |
| 28 | + |
| 29 | +| Commit | Что делает | Файлов | Риск | |
| 30 | +| ----------- | ----------------------------------------------------- | -------------------------- | --------------------------- | |
| 31 | +| `b7857bcd6` | **МЕГА-ФИКС**: жесткое подавление гонок делегирования | **20 файлов / 1714 строк** | 🔴 КРИТИЧЕСКИЙ | |
| 32 | +| `d06d97020` | `skipPrevResponseIdOnce` для OpenAI Responses API | 1 файл | 🔴 GPT-5 не видит результат | |
| 33 | +| `b1765361d` | Defer new_task tool_result до завершения subtask | 1 файл | 🟡 result возвращается рано | |
| 34 | +| `b2a8c5c7b` | Parent hang после per-mode API profile switch | 2 файла | 🟡 | |
| 35 | +| `70775f0ec` | `removeClineFromStack()` delegation-aware | 2 файла | 🔴 orphaned parent tasks | |
| 36 | +| `7c58f2997` | Race condition в `new_task` delegation | 1 файл | 🔴 потеря parent history | |
| 37 | +| `115d6c5fc` | Serialize taskHistory writes | 2 файла | 🔴 race на globalState | |
| 38 | +| `6826e20da` | Parent state loss при delegation | 1 файл | 🔴 | |
| 39 | +| `9b5f6392d` | Metadata‑driven subtasks | 8 файлов | 🔴 | |
| 40 | +| `e742511d9` | Infinite loop в `attempt_completion` | 1 файл | 🟡 | |
| 41 | +| `edd7cc098` | Flush pending tool results перед delegation | 1 файл | 🟡 | |
| 42 | + |
| 43 | +--- |
| 44 | + |
| 45 | +## 3. Доказательства из кода |
| 46 | + |
| 47 | +### 3.1. `delegationMeta.ts` — Файл ОТСУТСТВУЕТ |
| 48 | + |
| 49 | +```bash |
| 50 | +$ ls src/core/task-persistence/delegationMeta.ts |
| 51 | +# => MISSING |
| 52 | +``` |
| 53 | + |
| 54 | +Upstream-фикс `b7857bcd6` создаёт этот файл для per-task persistence делегационных метаданных. В BUILD вся делегационная информация хранится ТОЛЬКО в `globalState` (`updateTaskHistory`), что приводит к: |
| 55 | + |
| 56 | +1. **Eviction**: Когда `globalState` переполняется, delegation-поля теряются |
| 57 | +2. **Race condition**: `last writer wins` — child task может перезаписать parent delegation |
| 58 | +3. **Нет fallback**: `removeClineFromStack` не имеет disk-based repair |
| 59 | + |
| 60 | +### 3.2. `ClineProvider.ts` — Отсутствует `delegationInProgress` mutex |
| 61 | + |
| 62 | +В BUILD (строка ~178): |
| 63 | + |
| 64 | +```typescript |
| 65 | +private cancelledDelegationChildIds = new Set<string>() |
| 66 | +// 👆 cancelledDelegationChildIds ЕСТЬ, но её guard УЖЕ УБРАН (из 3c1895409) |
| 67 | +``` |
| 68 | + |
| 69 | +Upstream добавляет: |
| 70 | + |
| 71 | +```typescript |
| 72 | +private delegationInProgress = false |
| 73 | +public isTaskCreationInProgress = false |
| 74 | +``` |
| 75 | + |
| 76 | +Без этого mutex'а две параллельные операции делегирования для одного parentId **рондают друг с другом**. |
| 77 | + |
| 78 | +### 3.3. `ClineProvider.ts:delegateParentAndOpenChild` — Отличия |
| 79 | + |
| 80 | +**BUILD** (строки ~3446-3463): |
| 81 | + |
| 82 | +```typescript |
| 83 | +const child = await this.createTask(message, undefined, parent as any, { |
| 84 | + initialTodos, |
| 85 | + initialStatus: "active", // ⚠️ ЕСТЬ в BUILD |
| 86 | + startTask: false, |
| 87 | +}) |
| 88 | +// ... persist delegation |
| 89 | +const { historyItem } = await this.getTaskWithId(parentTaskId) // ⚠️ Может упасть |
| 90 | +``` |
| 91 | + |
| 92 | +**Upstream** (b7857bcd6): |
| 93 | + |
| 94 | +```typescript |
| 95 | +const child = await this.createTask(message, undefined, parent as any, { |
| 96 | + initialTodos, |
| 97 | + startTask: false, // initialStatus: УБРАН |
| 98 | +}) |
| 99 | +// ... persist child initial status SEPARATELY before parent delegation |
| 100 | +await this.updateTaskHistory({ |
| 101 | + /* child status "active" */ |
| 102 | +}) |
| 103 | +// ... parent delegation with try-catch fallback from globalState |
| 104 | +let parentHistory |
| 105 | +try { |
| 106 | + parentHistory = (await this.getTaskWithId(parentTaskId)).historyItem |
| 107 | +} catch (err) { |
| 108 | + // In-memory fallback if parent not in globalState |
| 109 | + parentHistory = { |
| 110 | + /* fallback */ |
| 111 | + } |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +**Проблема**: `initialStatus: "active"` создаёт race condition — child task's `saveClineMessages` в `startTask()` может перезаписать parent's delegation поля в globalState. |
| 116 | + |
| 117 | +### 3.4. `Task.ts` — Отсутствующие `.catch()` на `presentAssistantMessage` |
| 118 | + |
| 119 | +**BUILD** (строки ~467-530+): |
| 120 | + |
| 121 | +```typescript |
| 122 | +presentAssistantMessage(this) |
| 123 | +// ⚠️ НЕТ .catch() — необработанный reject = crash делегации |
| 124 | +``` |
| 125 | + |
| 126 | +**Upstream**: |
| 127 | + |
| 128 | +```typescript |
| 129 | +presentAssistantMessage(this).catch((err) => { |
| 130 | + if (!this.abort) { |
| 131 | + console.error("[presentAssistantMessage] Unhandled error:", err) |
| 132 | + } |
| 133 | +}) |
| 134 | +``` |
| 135 | + |
| 136 | +### 3.5. `Task.ts` — Отсутствует `skipPrevResponseIdOnce` фикс |
| 137 | + |
| 138 | +Upstream `d06d97020` добавляет в `resumePausedTask()`: |
| 139 | + |
| 140 | +```typescript |
| 141 | +this.skipPrevResponseIdOnce = true |
| 142 | +``` |
| 143 | + |
| 144 | +**Без этого GPT-5 / OpenAI Responses API никогда не получает subtask result** — модель видит только контекст ДО создания subtask. |
| 145 | + |
| 146 | +### 3.6. `Task.ts` — Отсутствует `debouncedEmitTokenUsage.cancel()` в `dispose()` |
| 147 | + |
| 148 | +Upstream добавляет: |
| 149 | + |
| 150 | +```typescript |
| 151 | +// Cancel debounced token usage emitter |
| 152 | +this.debouncedEmitTokenUsage.cancel() |
| 153 | +``` |
| 154 | + |
| 155 | +Без этого **zombie callback** может выполниться после dispose и вызвать некорректное состояние. |
| 156 | + |
| 157 | +### 3.7. `AttemptCompletionTool.ts` — Анализ текущего кода |
| 158 | + |
| 159 | +**Уже исправлено** (`3c1895409`): |
| 160 | + |
| 161 | +- ✅ `cancelledDelegationChildIds` guard удалён из `reopenParentFromDelegation` (ClineProvider.ts строка 3534) |
| 162 | +- ✅ Subtask exempt от `didToolFailInCurrentTurn` guard (строка 46) |
| 163 | +- ✅ ParentHistory verification перед delegation (строка 104-108) |
| 164 | +- ✅ Console.warn при неудачном reopen (строка 192-196) |
| 165 | + |
| 166 | +**Проблемы остаются**: |
| 167 | + |
| 168 | +- ⚠️ В `delegateToParent()` при неудачном reopen возвращается `"continue"` → поток падает на `ask("completion_result")` (строка 147) → оркестратор НЕ ПОЛУЧАЕТ результат |
| 169 | +- ⚠️ Вся логика полагается на `getTaskWithId()` который может выбросить ошибку если parent не в globalState |
| 170 | +- ⚠️ `emitTaskCompleted` вызывается после делегирования, но при `"continue"` пути не вызывается |
| 171 | + |
| 172 | +--- |
| 173 | + |
| 174 | +## 4. Корневая причина сломанной оркестрации |
| 175 | + |
| 176 | +### Основная причина: BUILD — это pre-b7857bcd6 код с единственным cherry-pick |
| 177 | + |
| 178 | +`b7857bcd6` — это upstream PR #11379 (Hannes Rudolph), который исправляет **7 различных race conditions** в системе делегирования. BUILD был создан на основе кода ДО этого PR и cherry-pick'нул только один поверхностный симптом-фикс. |
| 179 | + |
| 180 | +Цепочка отказов: |
| 181 | + |
| 182 | +``` |
| 183 | +1. Пользователь создаёт subtask через new_task |
| 184 | +2. `delegateParentAndOpenChild` записывает "delegated" в globalState |
| 185 | +3. Subtask начинает работу, `startTask()` → `saveClineMessages()` вызывается |
| 186 | +4. `saveClineMessages()` → `updateTaskHistory()` ПЕРЕЗАПИСЫВАЕТ globalState |
| 187 | + без delegation-полей (initialStatus race) |
| 188 | +5. Parent теряет статус "delegated" → становится "active" |
| 189 | +6. Subtask завершает → `attempt_completion` проверяет parent статус |
| 190 | +7. Parent.status !== "delegated" → делегирование пропускается |
| 191 | +8. Subtast завершается нормально, но parent НЕ ПОЛУЧАЕТ результат |
| 192 | +``` |
| 193 | + |
| 194 | +**Если race не произошла** (шаг 4 успел до шага 5) → оркестрация работает. Если произошла — ломается. |
| 195 | + |
| 196 | +### Как это выглядит пользователю: |
| 197 | + |
| 198 | +- Оркестратор зависает "навсегда" (parent в статусе "delegated") |
| 199 | +- Subtask показывает "готово", но parent не возобновляется |
| 200 | +- Иногда работает, иногда нет |
| 201 | +- Для GPT-5/OpenAI Responses API: результат subtask просто не отображается в промпте |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +## 5. План фикса |
| 206 | + |
| 207 | +### Фаза 1: Безопасные cherry-pick из upstream (10 коммитов) |
| 208 | + |
| 209 | +| # | Коммит | Файлы | Приоритет | Риск | |
| 210 | +| --- | ----------- | ---------------------------------------- | --------- | ----------------- | |
| 211 | +| 1 | `d06d97020` | `src/core/task/Task.ts` | 🔴 P0 | Низкий — 4 строки | |
| 212 | +| 2 | `e742511d9` | `AttemptCompletionTool.ts` | 🔴 P0 | Низкий | |
| 213 | +| 3 | `115d6c5fc` | `ClineProvider.ts` + тесты | 🔴 P0 | Средний | |
| 214 | +| 4 | `edd7cc098` | `Task.ts` / `presentAssistantMessage.ts` | 🟡 P1 | Средний | |
| 215 | +| 5 | `d2d311e50` | `AttemptCompletionTool.ts` | 🟡 P1 | Низкий | |
| 216 | +| 6 | `b1765361d` | `presentAssistantMessage.ts` | 🔴 P0 | Средний | |
| 217 | + |
| 218 | +### Фаза 2: Мега-фикс b7857bcd6 (выборочно) |
| 219 | + |
| 220 | +| # | Компонент | Файлы | Описание | |
| 221 | +| --- | ---------------------- | ------------------ | ----------------------------------------------------------------------------------------------------- | |
| 222 | +| 7 | `delegationMeta.ts` | НОВЫЙ файл | Per-task delegation metadata persistence | |
| 223 | +| 8 | `ClineProvider.ts` | +~200 строк | `delegationInProgress` mutex, TOCTOU fixes, disk fallback | |
| 224 | +| 9 | `Task.ts` | +~80 строк | Remove `initialStatus`, `.catch()` on `presentAssistantMessage()`, `debouncedEmitTokenUsage.cancel()` | |
| 225 | +| 10 | `removeClineFromStack` | в ClineProvider.ts | Disk-based delegation repair fallback | |
| 226 | + |
| 227 | +### Фаза 3: Тесты |
| 228 | + |
| 229 | +| # | Тест | Описание | |
| 230 | +| --- | ----------------------------------------- | ----------------------------------------------- | |
| 231 | +| 11 | `attemptCompletionDelegation.spec.ts` | Тест полного цикла делегирования (из b7857bcd6) | |
| 232 | +| 12 | `delegationMeta.spec.ts` | Тест per-task persistence (из b7857bcd6) | |
| 233 | +| 13 | `provider-delegation.spec.ts` | Тест на гонки (дополнить) | |
| 234 | +| 14 | `removeClineFromStack-delegation.spec.ts` | Тест на repair при отмене (из b7857bcd6) | |
| 235 | + |
| 236 | +### Детальные правки (с примерами) |
| 237 | + |
| 238 | +#### Правка 1: `src/core/task/Task.ts` — `.catch()` на presentAssistantMessage |
| 239 | + |
| 240 | +```typescript |
| 241 | +// ЗАМЕНИТЬ: |
| 242 | +presentAssistantMessage(this) |
| 243 | + |
| 244 | +// НА: |
| 245 | +presentAssistantMessage(this).catch((err) => { |
| 246 | + if (!this.abort) { |
| 247 | + console.error("[presentAssistantMessage] Unhandled error:", err) |
| 248 | + } |
| 249 | +}) |
| 250 | +``` |
| 251 | + |
| 252 | +**Где:** Все 4 места вызова `presentAssistantMessage(this)` в `Task.ts` (строки ~470, ~488, ~518, ~541) |
| 253 | + |
| 254 | +#### Правка 2: `src/core/task/Task.ts` — skipPrevResponseIdOnce |
| 255 | + |
| 256 | +```typescript |
| 257 | +// В resumePausedTask(), ПОСЛЕ добавления subtask result в API историю: |
| 258 | +if (!this.skipPrevResponseIdOnce) { |
| 259 | + this.skipPrevResponseIdOnce = true |
| 260 | +} |
| 261 | +``` |
| 262 | + |
| 263 | +**Где:** В методе `resumePausedTask()`, после `this.addToApiConversationHistory(...)` или эквивалента. |
| 264 | + |
| 265 | +#### Правка 3: `src/core/task/Task.ts` — Remove initialStatus |
| 266 | + |
| 267 | +```typescript |
| 268 | +// УДАЛИТЬ: |
| 269 | +interface TaskOptions extends CreateTaskOptions { |
| 270 | + // ... |
| 271 | + initialStatus?: "active" | "delegated" | "completed" // <- удалить |
| 272 | +} |
| 273 | + |
| 274 | +// В конструкторе: |
| 275 | +this.initialStatus = initialStatus // <- удалить |
| 276 | + |
| 277 | +// В saveClineMessages: |
| 278 | +initialStatus: this.initialStatus, // <- удалить |
| 279 | +``` |
| 280 | + |
| 281 | +#### Правка 4: `src/core/task/Task.ts` — debouncedEmitTokenUsage.cancel() |
| 282 | + |
| 283 | +```typescript |
| 284 | +// В dispose(), ПОСЛЕ cancelCurrentRequest: |
| 285 | +this.debouncedEmitTokenUsage.cancel() |
| 286 | +``` |
| 287 | + |
| 288 | +#### Правка 5: `src/core/webview/ClineProvider.ts` — delegateParentAndOpenChild |
| 289 | + |
| 290 | +```typescript |
| 291 | +// 1. Убрать initialStatus из createTask |
| 292 | +const child = await this.createTask(message, undefined, parent as any, { |
| 293 | + initialTodos, |
| 294 | + // initialStatus: "active", <- УДАЛИТЬ |
| 295 | + startTask: false, |
| 296 | +}) |
| 297 | + |
| 298 | +// 2. Сохранить child initial status отдельно |
| 299 | +await this.updateTaskHistory( |
| 300 | + { |
| 301 | + id: child.taskId, |
| 302 | + ts: Date.now(), |
| 303 | + task: message, |
| 304 | + number: child.taskNumber, |
| 305 | + tokensIn: 0, |
| 306 | + tokensOut: 0, |
| 307 | + totalCost: 0, |
| 308 | + status: "active", |
| 309 | + parentTaskId: parentTaskId, |
| 310 | + rootTaskId: child.rootTaskId, |
| 311 | + workspace: this.cwd, |
| 312 | + } as HistoryItem, |
| 313 | + { broadcast: false }, |
| 314 | +) |
| 315 | + |
| 316 | +// 3. Обработать случай, когда parent нет в globalState |
| 317 | +let parentHistory: HistoryItem |
| 318 | +try { |
| 319 | + parentHistory = (await this.getTaskWithId(parentTaskId)).historyItem |
| 320 | +} catch (err) { |
| 321 | + parentHistory = { |
| 322 | + id: parentTaskId, |
| 323 | + ts: Date.now(), |
| 324 | + task: parentMetadata.task ?? "", |
| 325 | + number: parentMetadata.taskNumber ?? 0, |
| 326 | + tokensIn: 0, |
| 327 | + tokensOut: 0, |
| 328 | + totalCost: 0, |
| 329 | + workspace: this.cwd, |
| 330 | + } as HistoryItem |
| 331 | +} |
| 332 | +``` |
| 333 | + |
| 334 | +#### Правка 6: `src/core/task-persistence/delegationMeta.ts` (НОВЫЙ ФАЙЛ) |
| 335 | + |
| 336 | +```typescript |
| 337 | +// Per-task delegation metadata file persistence |
| 338 | +// Resolves globalState eviction race condition |
| 339 | + |
| 340 | +export interface DelegationMeta { |
| 341 | + status: "active" | "delegated" | "completed" |
| 342 | + awaitingChildId: string | null |
| 343 | + delegatedToId: string | undefined |
| 344 | + childIds: string[] | undefined |
| 345 | + completedByChildId: string | undefined |
| 346 | + completionResultSummary: string | undefined |
| 347 | +} |
| 348 | + |
| 349 | +export async function saveDelegationMeta(params: { |
| 350 | + taskId: string |
| 351 | + globalStoragePath: string |
| 352 | + meta: DelegationMeta |
| 353 | +}): Promise<void> { |
| 354 | + // Save delegation meta to per-task file |
| 355 | + // ... |
| 356 | +} |
| 357 | + |
| 358 | +export async function readDelegationMeta(params: { |
| 359 | + taskId: string |
| 360 | + globalStoragePath: string |
| 361 | +}): Promise<DelegationMeta | null> { |
| 362 | + // Read delegation meta from per-task file |
| 363 | + // ... |
| 364 | +} |
| 365 | +``` |
| 366 | + |
| 367 | +--- |
| 368 | + |
| 369 | +## 6. Сводка рисков |
| 370 | + |
| 371 | +| Риск | Описание | Вероятность | |
| 372 | +| ------------------------------- | ---------------------------------------------------------- | ----------------------------- | |
| 373 | +| Race condition globalState | Child `saveClineMessages` перезаписывает parent delegation | 🟡 Средняя | |
| 374 | +| GPT-5 subtask loss | `previous_response_id` ломает контекст subtask result | 🔴 Высокая (всегда для GPT-5) | |
| 375 | +| Zombie callbacks | `debouncedEmitTokenUsage` выполняется после dispose | 🟡 Средняя | |
| 376 | +| Parent state loss | `getTaskWithId` падает если parent не в globalState | 🔴 Высокая | |
| 377 | +| `presentAssistantMessage` crash | Unhandled rejection обрывает делегацию | 🟢 Низкая (только при ошибке) | |
| 378 | + |
| 379 | +--- |
| 380 | + |
| 381 | +## 7. Рекомендация |
| 382 | + |
| 383 | +**Выполнить cherry-pick всех 10 коммитов в порядке возрастания зависимости, начиная с самых безопасных.** Приоритет: |
| 384 | + |
| 385 | +1. `d06d97020` — GPT-5 fix (4 строки, безрисковый) |
| 386 | +2. `e742511d9` — infinite loop fix |
| 387 | +3. `115d6c5fc` — serialize taskHistory writes |
| 388 | +4. `edd7cc098` — flush before delegation |
| 389 | +5. `b1765361d` — defer new_task result |
| 390 | +6. `b2a8c5c7b` — parent hang fix + per-mode API switch |
| 391 | +7. `d2d311e50` — race condition in new_task |
| 392 | +8. `70775f0ec` — delegation-aware removeClineFromStack |
| 393 | +9. `6826e20da` — parent state loss |
| 394 | +10. `b7857bcd6` — mega-fix (может потребовать разрешения конфликтов) |
| 395 | +11. Создать `delegationMeta.ts` (отсутствует в BUILD) |
0 commit comments