fix(report): do not fail a written report when progress projection throws
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled

Staging wrote four ready chapters then marked the job schema-invalid 34ms
later with no summary telemetry. Keep chapter persistence and assemble
going if onProgress or a non-lease heartbeat error fails.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-03 09:54:14 +08:00
parent f127ada413
commit fbd6e48036
5 changed files with 101 additions and 18 deletions
+13 -6
View File
@@ -3155,6 +3155,13 @@ async function generateSectionedPersonalReport(
return failSchema("sectioned_dependencies_missing");
}
const writePlans = plan.sections.filter((entry) => entry.kind === "thematic" && entry.disposition === "write");
const emitProgress = async (phase: string, completed: number): Promise<void> => {
try {
await deps.onProgress?.({ phase, completed, total: writePlans.length });
} catch (error) {
rethrowIfAborted(error, deps.signal);
}
};
for (const entry of writePlans) {
await sectionService.ensure({
userId: deps.userId, requestId: deps.requestId, sectionId: entry.id, maxAttempts: 2,
@@ -3177,7 +3184,7 @@ async function generateSectionedPersonalReport(
});
if (!blockedRow) throw new Error("section_block_failed");
blocked.push(blockedRow);
await deps.onProgress?.({ phase: `section:${entry.id}`, completed: ready.length + blocked.length, total: writePlans.length });
await emitProgress(`section:${entry.id}`, ready.length + blocked.length);
continue;
}
const outputBudget = sectionOutputTokenBudget(entry.targetCharacters.max);
@@ -3204,7 +3211,7 @@ async function generateSectionedPersonalReport(
const completed = await sectionService.complete({ userId: deps.userId, requestId: deps.requestId, sectionId: entry.id, payload: output });
if (!completed) throw new Error("section_complete_failed");
ready.push(completed);
await deps.onProgress?.({ phase: `section:${entry.id}`, completed: ready.length + blocked.length, total: writePlans.length });
await emitProgress(`section:${entry.id}`, ready.length + blocked.length);
break;
} catch (error) {
rethrowIfAborted(error, deps.signal);
@@ -3214,7 +3221,7 @@ async function generateSectionedPersonalReport(
});
if (!blockedRow) throw new Error("section_block_failed");
blocked.push(blockedRow);
await deps.onProgress?.({ phase: `section:${entry.id}`, completed: ready.length + blocked.length, total: writePlans.length });
await emitProgress(`section:${entry.id}`, ready.length + blocked.length);
break;
}
}
@@ -3232,13 +3239,13 @@ async function generateSectionedPersonalReport(
});
if (!blockedRow) throw new Error("section_block_failed");
blocked.push(blockedRow);
await deps.onProgress?.({ phase: `section:${entry.id}`, completed: ready.length + blocked.length, total: writePlans.length });
await emitProgress(`section:${entry.id}`, ready.length + blocked.length);
}
}
if (writePlans.length > 0 && ready.length === 0) {
return failSchema("all_sections_blocked");
}
await deps.onProgress?.({ phase: "summary", completed: writePlans.length, total: writePlans.length });
await emitProgress("summary", writePlans.length);
let summary;
try {
summary = await deps.agent.generateSummary(ready.map((item) => ({
@@ -3260,7 +3267,7 @@ async function generateSectionedPersonalReport(
missingEvidence: [sectionFailureReason(item.lastErrorCode)], conflictNotes: [], evidenceRefs: refs,
};
});
await deps.onProgress?.({ phase: "assemble", completed: writePlans.length, total: writePlans.length });
await emitProgress("assemble", writePlans.length);
try {
const candidate = assembleReportDocumentV2({
reportId: deps.reportId, generatedAt: (deps.now ?? (() => new Date()))().toISOString(),
@@ -338,14 +338,22 @@ export function createPersonalReportWorker(deps: PersonalReportWorkerDeps) {
signal: controller.signal,
sectionService: deps.sectionService,
onProgress: async (progress) => {
await refreshLease();
if (heartbeatError !== null) throw heartbeatError;
const percent = progress.total > 0
? Math.min(89, 30 + Math.floor((progress.completed / progress.total) * 55))
: 30;
await deps.jobs.updateProgress({
jobId: job.id, leaseToken: job.leaseToken!, phase: progress.phase, percent,
});
try {
await refreshLease();
if (heartbeatError !== null) throw heartbeatError;
const percent = progress.total > 0
? Math.min(89, 30 + Math.floor((progress.completed / progress.total) * 55))
: 30;
await deps.jobs.updateProgress({
jobId: job.id, leaseToken: job.leaseToken!, phase: progress.phase, percent,
});
} catch (error) {
if (isLeaseLoss(error) || heartbeatError !== null) {
heartbeatError = heartbeatError ?? error;
controller.abort(error);
throw error;
}
}
},
});
await heartbeatChain;