RecoverTask clears task-level needs_attention escalation through the service-owned transition.
(ctx context.Context, id string, note string, actor ActorContext)
| 314 | |
| 315 | // RecoverTask clears task-level needs_attention escalation through the service-owned transition. |
| 316 | func (m *Service) RecoverTask(ctx context.Context, id string, note string, actor ActorContext) (*Task, error) { |
| 317 | if err := requireWriteAuthority(actor); err != nil { |
| 318 | return nil, err |
| 319 | } |
| 320 | trimmedID := strings.TrimSpace(id) |
| 321 | if trimmedID == "" { |
| 322 | return nil, fmt.Errorf("%w: task.id is required", ErrValidation) |
| 323 | } |
| 324 | if actor.Actor.Kind.Normalize() == ActorKindAgentSession { |
| 325 | return nil, autonomyError( |
| 326 | AutonomyForeignRun, |
| 327 | ErrPermissionDenied, |
| 328 | "agent session %q cannot recover task %q without operator scope", |
| 329 | strings.TrimSpace(actor.Actor.Ref), |
| 330 | trimmedID, |
| 331 | ) |
| 332 | } |
| 333 | normalizedNote := strings.TrimSpace(note) |
| 334 | if err := rejectTaskSecretText("task.recover_note", normalizedNote); err != nil { |
| 335 | return nil, err |
| 336 | } |
| 337 | recoveredAt := m.now().UTC() |
| 338 | cleared, err := m.store.ClearTaskNeedsAttention(ctx, NeedsAttentionClearMutation{ |
| 339 | TaskID: trimmedID, |
| 340 | ClearedBy: actor.Actor, |
| 341 | ClearedAt: recoveredAt, |
| 342 | }) |
| 343 | if err != nil { |
| 344 | return nil, err |
| 345 | } |
| 346 | reconciled, err := m.reconcileTaskCascade(ctx, cleared.ID) |
| 347 | if err != nil { |
| 348 | return nil, err |
| 349 | } |
| 350 | if eventErr := m.recordTaskEvent(ctx, reconciled.ID, "", taskEventRecovered, actor, recoveredTaskPayload{ |
| 351 | Status: reconciled.Status, |
| 352 | Note: redactTaskSecretText(normalizedNote), |
| 353 | At: recoveredAt, |
| 354 | }); eventErr != nil { |
| 355 | slog.Error( |
| 356 | "task: recovered event failed after committed recover", |
| 357 | "error", eventErr, |
| 358 | "event_type", taskEventRecovered, |
| 359 | "task_id", reconciled.ID, |
| 360 | ) |
| 361 | } |
| 362 | m.dispatchTaskRecovered(ctx, reconciled, actor, normalizedNote, recoveredAt) |
| 363 | m.autoEnqueueReadyTaskDetached(ctx, reconciled.ID, autoEnqueueTrigger{ |
| 364 | Kind: autoEnqueueTriggerRecover, |
| 365 | Ref: reconciled.ID, |
| 366 | }, actor) |
| 367 | return &reconciled, nil |
| 368 | } |
| 369 | |
| 370 | // ExpireTaskBlocks finalizes expired transient blocks through service-owned transitions. |
| 371 | func (m *Service) ExpireTaskBlocks( |
nothing calls this directly
no test coverage detected