fix(ai): JSONL fallback returns 'failed' on partial-insert (was 'applied')

Previous version reported `status: 'applied'` whenever at least one root
landed, with a partial-failure note in `message`. But the orchestrator's
retry / minimal-skills / batch_design-fallback chain checks
`status === 'applied'` to decide whether to bypass retry — a partial
insert (e.g. 1/5 roots landed because `defaultParentId` was stale)
would short-circuit retry and leave the user with a degraded design
that the system never tried to fix.

Now any failed root flips the dispatch to `status: 'failed'` so the
orchestrator's retry path can take over. The successful partial inserts
are still surfaced in `insertedNodes` so the surrounding history-batch
wrapper can roll them back / clean up — `failed` with non-empty
`insertedNodes` is a legitimate combination meaning "side effects
happened but the dispatch did not complete its contract".

Three outcomes now:
- All N roots land → `applied` with full count.
- 1..N-1 land → `failed` with partial-success `insertedNodes` and a
  message naming the parent id + failed root ids.
- 0 land → `failed` with empty `insertedNodes` and the same diagnostic.
This commit is contained in:
Fini 2026-05-05 01:04:47 +08:00
parent f6bc47b347
commit 4043e9915f

View file

@ -501,25 +501,47 @@ function applyBatchDesignAsJsonl(jsonl: string, ctx: DispatchContext): DispatchR
failedIds.push(root.id);
}
}
if (inserted.length === 0) {
// Any failure to land a root is a hard failure — even a single missing
// root usually means the defaultParentId is stale, the model emitted a
// dangling _parent reference, or the doc state has drifted. Reporting
// `applied` for partial inserts would lie to the orchestrator: it
// checks `status === 'applied'` to decide whether to skip retry, so a
// partial would silently degrade the design without ever exercising
// the orchestrator's retry / minimal-skills / batch_design fallback
// chain. We still surface the partial-success roots in `insertedNodes`
// so the caller can clean them up (or roll back through the history
// batch wrapper) instead of leaving orphans.
if (failedIds.length > 0) {
const failedList = `${failedIds.slice(0, 4).join(', ')}${failedIds.length > 4 ? '…' : ''}`;
if (inserted.length === 0) {
return {
status: 'failed',
route: 'batch-design-dsl',
toolName: 'batch_design',
message:
`batch_design.operations parsed as JSONL (${tree.length} root(s)) but none landed — ` +
`parent id '${parentId ?? 'page-root'}' may be stale or unresolved. ` +
`Failed root ids: ${failedList}`,
insertedNodes: [],
};
}
return {
status: 'failed',
route: 'batch-design-dsl',
toolName: 'batch_design',
message:
`batch_design.operations parsed as JSONL (${tree.length} root(s)) but none landed — ` +
`parent id '${parentId ?? 'page-root'}' may be stale or unresolved. ` +
`Failed root ids: ${failedIds.slice(0, 4).join(', ')}${failedIds.length > 4 ? '…' : ''}`,
insertedNodes: [],
`batch_design.operations partial failure: ${inserted.length}/${tree.length} root(s) ` +
`landed, ${failedIds.length} did not (parent id '${parentId ?? 'page-root'}'). ` +
`Failed root ids: ${failedList}`,
insertedNodes: inserted,
};
}
const totalLive = countDescendants(inserted);
const partial = failedIds.length > 0 ? ` (${failedIds.length} root(s) failed to insert)` : '';
return {
status: 'applied',
route: 'batch-design-dsl',
toolName: 'batch_design',
message: `Applied batch_design.operations as JSONL (${inserted.length}/${tree.length} root node(s); ${totalLive} total)${partial}.`,
message: `Applied batch_design.operations as JSONL (${tree.length} root node(s); ${totalLive} total).`,
insertedNodes: inserted,
};
}