Skip to content

Commit 60e94b9

Browse files
author
AGI Developer
committed
fix(orchestration): implement delegation fixes — cherry-pick unavailable, manual injection
Implemented 10+ delegation fixes from upstream analysis (report_orchestration_bug.md): 1. delegationMeta.ts — NEW: per-task delegation metadata file persistence - Resolves globalState eviction race (last-writer-wins corruption) - saveDelegationMeta / readDelegationMeta / deleteDelegationMeta 2. Task.ts — .catch() on all 10 presentAssistantMessage(this) calls - Prevents unhandled rejections that crash delegation flow - All call sites: tool_call_start, delta, end, legacy, text, finalize, partial 3. Task.ts — debouncedEmitTokenUsage.cancel() in dispose() - Prevents zombie callbacks after task disposal 4. AttemptCompletionTool.ts — completed-subtask guard fix - Completed subtasks now emitTaskCompleted() + return immediately - Prevents infinite loop on revisiting completed subtasks from history 5. ClineProvider.ts — delegationInProgress mutex - Prevents concurrent delegation operations for same parent task - isTaskCreationInProgress companion flag 6. ClineProvider.ts — initialStatus race fix in delegateParentAndOpenChild - Removed initialStatus: 'active' from createTask() options - Persists child initial status separately BEFORE parent delegation metadata - Try-catch fallback for getTaskWithId when parent evicted from globalState 7. ClineProvider.ts — delegationMeta persistence in delegateParentAndOpenChild - Saves per-task delegation metadata as disk-based fallback 8. ClineProvider.ts — throw err re-added after rollback in catch block - Ensures caller notified on delegation failure Build: zoo-code-3.56.9.vsix (31.65 MB) Tests: 253 passed (CRT test suite) TypeScript: clean compilation
1 parent a4d3ca4 commit 60e94b9

5 files changed

Lines changed: 722 additions & 66 deletions

File tree

Lines changed: 395 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,395 @@
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

Comments
 (0)