Preserve batch cancellation outcomes

This commit is contained in:
2026-08-13 03:02:59 +00:00
parent 4b748c2e53
commit 70cad789ea
11 changed files with 207 additions and 17 deletions

View File

@@ -3,6 +3,7 @@ package app
import (
"context"
"errors"
"fmt"
"path/filepath"
"time"
@@ -100,6 +101,7 @@ type BatchResult struct {
Total int `json:"total"`
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
Canceled int `json:"canceled,omitempty"`
Notification *BatchNotificationResult `json:"notification,omitempty"`
Reports []BatchReportResult `json:"reports"`
}
@@ -143,12 +145,19 @@ type BatchReportResult struct {
type BatchError struct {
Result *BatchResult
Cause error
}
func (e BatchError) Error() string {
if e.Result == nil {
return "batch failed"
}
if errors.Is(e.Cause, context.DeadlineExceeded) {
return fmt.Sprintf("batch %s deadline exceeded", e.Result.Batch)
}
if errors.Is(e.Cause, context.Canceled) || e.Result.Canceled > 0 {
return fmt.Sprintf("batch %s canceled", e.Result.Batch)
}
failedReports := batchReportFailures(e.Result)
if batchNotificationFailed(e.Result) && failedReports == 0 {
if e.Result.Notification.Error != "" {
@@ -159,6 +168,10 @@ func (e BatchError) Error() string {
return fmt.Sprintf("batch %s failed: %d of %d reports failed", e.Result.Batch, failedReports, len(e.Result.Reports))
}
func (e BatchError) Unwrap() error {
return e.Cause
}
func batchNotificationFailed(result *BatchResult) bool {
return result != nil && result.Notification != nil && result.Notification.Status == "failed"
}
@@ -344,7 +357,12 @@ func RunBatchDetailed(ctx context.Context, req BatchRequest) (*BatchResult, erro
if req.Batch == BatchEvening || req.Batch == BatchMorning {
startedAt := now
result := &BatchResult{Batch: req.Batch, StartedAt: startedAt}
for _, planned := range plannedReports {
var cancellation error
for index, planned := range plannedReports {
if cancellation = batchCancellationCause(ctx, nil); cancellation != nil {
appendCanceledBatchReports(result, plannedReports[index:])
break
}
resolved := planned.Resolved
item := batchReportResult(planned)
reportResult, err := generatePromptReport(ctx, promptReportRequest{
@@ -364,14 +382,26 @@ func RunBatchDetailed(ctx context.Context, req BatchRequest) (*BatchResult, erro
copyBatchReportDetails(&item, reportResult)
}
if err != nil {
item.Status = "failed"
item.Error = err.Error()
result.Failed++
if cancellation = batchCancellationCause(ctx, err); cancellation != nil {
item.Status = "canceled"
result.Canceled++
} else {
item.Status = "failed"
item.Error = err.Error()
result.Failed++
}
} else {
item.Status = "succeeded"
result.Succeeded++
}
result.Reports = append(result.Reports, item)
if cancellation == nil {
cancellation = batchCancellationCause(ctx, nil)
}
if cancellation != nil {
appendCanceledBatchReports(result, plannedReports[index+1:])
break
}
}
result.Total = len(result.Reports)
batchNotification := notifyBatch(ctx, req.Config, req.Batch, batchRunID(startedAt, req.Batch), startedAt, result, plannedReports, req.Notifier)
@@ -379,11 +409,38 @@ func RunBatchDetailed(ctx context.Context, req BatchRequest) (*BatchResult, erro
result.Notification = batchNotification
}
result.FinishedAt = time.Now()
return result, nil
return result, cancellation
}
return nil, fmt.Errorf("run is not implemented")
}
func batchCancellationCause(ctx context.Context, err error) error {
if ctx != nil {
if contextErr := ctx.Err(); contextErr != nil {
return contextErr
}
}
if errors.Is(err, context.Canceled) {
return context.Canceled
}
if errors.Is(err, context.DeadlineExceeded) {
return context.DeadlineExceeded
}
return nil
}
func appendCanceledBatchReports(result *BatchResult, plannedReports []plannedBatchReport) {
if result == nil {
return
}
for _, planned := range plannedReports {
item := batchReportResult(planned)
item.Status = "canceled"
result.Reports = append(result.Reports, item)
result.Canceled++
}
}
func copyBatchReportDetails(item *BatchReportResult, result *ReportResult) {
item.LLMDebugPath = result.LLMDebugPath
item.OutputPath = result.OutputPath