PR #3488
Sections
Review

fix(office): stop routines from bricking after their first fire

main ← feature/office-routine-runs-bbv 26 files +512 −68 PR #3488 ↗

Heavy routine runs now close when their linked task finishes, so the next fire is not blocked by a completed task.

Why this change

A heavy routine creates a task and marks its run as task_created. That run never closed when the task finished, so the concurrency gate saw an active run forever and blocked every later fire.

What it does

Architecture, end to end

A routine fire passes the concurrency gate, materialises as a task or wakeup, and later clears the gate when the task finishes.

flowchart LR
  Cron[Cron trigger] --> Dispatch[RoutineService.dispatchRoutineRun]
  Dispatch --> Gate{Concurrency gate}
  Gate -->|no active| Heavy[materialiseHeavyRoutineRun]
  Gate -->|no active| Light[materialiseLightweightRoutineRun]
  Gate -->|active| Skip[skip or coalesce]
  Heavy --> Task[(tasks table)]
  Light --> Wakeup[(agent_wakeup_requests)]
  Task --> Moved[TaskMoved event]
  Moved --> Sync[SyncRunStatus]
  Sync --> Close[UpdateRunStatusIfTaskCreated]
  Close --> Gate
  Gate -->|self-heal| Check[GetTaskTerminalStatus]
  Check --> Close

Key code changes

Drag to pan. Use the + and − buttons to zoom. Click a node to open the full code. The arrows show how the parts interact.

drag to pan · +/− to zoom · click a node for details

Concurrency gate self-heals and closes atomicallyapps/backend/internal/office/routines/service.go ↗
func (s *RoutineService) applyConcurrencyPolicy(ctx context.Context, routine *Routine, run *RoutineRun, fingerprint string) (models.RoutineRunStatus, error)
Click for details →

The gate now repairs a stale active run before it decides to skip or coalesce, and closes runs with an atomic conditional update.

Self-heal before policy
func (s *RoutineService) selfHealIfTaskTerminal(ctx context.Context, routine *Routine, active *RoutineRun) *RoutineRun {
  if active.LinkedTaskID == "" {
    return active
  }
  terminalStatus, err := s.repo.GetTaskTerminalStatus(ctx, active.LinkedTaskID)
  if err != nil {
    s.logger.Warn("check linked task terminal state", zap.String("run_id", active.ID), zap.Error(err))
    return active
  }
  if terminalStatus == "" {
    return active
  }
  if _, err := s.closeOutRun(ctx, active, terminalStatus); err != nil {
    s.logger.Warn("close out stale active run", zap.String("run_id", active.ID), zap.Error(err))
    return active
  }
  return nil
}
Loop with repair
func (s *RoutineService) applyConcurrencyPolicy(ctx context.Context, routine *Routine, run *RoutineRun, fingerprint string) (models.RoutineRunStatus, error) {
  if routine.ConcurrencyPolicy == models.ConcurrencyPolicyAlwaysCreate {
    return "", nil
  }
  for {
    active, err := s.repo.GetActiveRunForFingerprint(ctx, routine.ID, fingerprint)
    if err != nil {
      return "", fmt.Errorf("check active run: %w", err)
    }
    if active == nil {
      return "", nil
    }
    if repaired := s.selfHealIfTaskTerminal(ctx, routine, active); repaired == nil {
      continue
    } else {
      active = repaired
    }
    switch routine.ConcurrencyPolicy {
    case models.ConcurrencyPolicySkipIfActive:
      _ = s.repo.UpdateRunStatus(ctx, run.ID, models.RoutineRunStatusSkipped, "")
      run.Status = models.RoutineRunStatusSkipped
      return models.RoutineRunStatusSkipped, nil
    case models.ConcurrencyPolicyCoalesceIfActive:
      _ = s.repo.UpdateRunCoalesced(ctx, run.ID, active.ID)
      run.Status = models.RoutineRunStatusCoalesced
      return models.RoutineRunStatusCoalesced, nil
    }
  }
}
Atomic close
func (s *RoutineService) closeOutRun(ctx context.Context, run *RoutineRun, terminalStatus string) (bool, error) {
  status := models.RoutineRunStatusDone
  switch terminalStatus {
  case "cancelled":
    status = models.RoutineRunStatusCancelled
  case "failed", "missing":
    status = models.RoutineRunStatusFailed
  }
  closed, err := s.repo.UpdateRunStatusIfTaskCreated(ctx, run.ID, status, run.LinkedTaskID)
  if err != nil {
    return false, fmt.Errorf("update run status: %w", err)
  }
  return closed, nil
}

func (s *RoutineService) SyncRunStatus(ctx context.Context, taskID, terminalStatus string) error {
  if taskID == "" {
    return nil
  }
  run, err := s.repo.GetRoutineRunByLinkedTaskID(ctx, taskID)
  if err != nil {
    return fmt.Errorf("get routine run by linked task: %w", err)
  }
  if run == nil {
    return nil
  }
  _, err = s.closeOutRun(ctx, run, terminalStatus)
  return err
}
Repository adds terminal-state readers and conditional closeapps/backend/internal/office/repository/sqlite/routines.go ↗
func (r *Repository) GetTaskTerminalStatus(ctx context.Context, taskID string) (string, error)
Click for details →

The repo now reads the shared tasks table directly and closes a run only while it is still task_created.

Terminal status reader
func (r *Repository) GetTaskTerminalStatus(ctx context.Context, taskID string) (string, error) {
  var state string
  var archivedAt sql.NullTime
  err := r.ro.QueryRowxContext(ctx, r.ro.Rebind(
    `SELECT COALESCE(state, ''), archived_at FROM tasks WHERE id = ?`), taskID).Scan(&state, &archivedAt)
  if err == sql.ErrNoRows {
    return "missing", nil
  }
  if err != nil {
    return "", err
  }
  if archivedAt.Valid {
    return "cancelled", nil
  }
  switch state {
  case taskStateCompleted:
    return "done", nil
  case taskStateFailed:
    return "failed", nil
  case taskStateCancelled:
    return "cancelled", nil
  default:
    return "", nil
  }
}
Conditional close
func (r *Repository) UpdateRunStatusIfTaskCreated(ctx context.Context, runID string, status models.RoutineRunStatus, linkedTaskID string) (bool, error) {
  now := time.Now().UTC()
  res, err := r.db.ExecContext(ctx, r.db.Rebind(`
    UPDATE office_routine_runs
    SET status = ?, linked_task_id = ?, completed_at = ?
    WHERE id = ? AND status = 'task_created'
  `), status, linkedTaskID, now, runID)
  if err != nil {
    return false, err
  }
  rows, err := res.RowsAffected()
  return rows > 0, err
}
Active means task_created only
func (r *Repository) GetActiveRunForFingerprint(ctx context.Context, routineID, fingerprint string) (*models.RoutineRun, error) {
  var run models.RoutineRun
  err := r.ro.QueryRowxContext(ctx, r.ro.Rebind(`
    SELECT * FROM office_routine_runs
    WHERE routine_id = ? AND dispatch_fingerprint = ?
      AND status = 'task_created'
    ORDER BY created_at DESC LIMIT 1
  `), routineID, fingerprint).StructScan(&run)
  if err == sql.ErrNoRows {
    return nil, nil
  }
  if err != nil {
    return nil, err
  }
  return &run, nil
}
Narrow last_run_at touch
func (r *Repository) TouchRoutineLastRun(ctx context.Context, routineID string, at time.Time) error {
  _, err := r.db.ExecContext(ctx, r.db.Rebind(`
    UPDATE office_routines SET last_run_at = ?, updated_at = ? WHERE id = ?
  `), at, time.Now().UTC(), routineID)
  return err
}
func (s *Service) finalizeDone(ctx context.Context, data *TaskMovedData) error
Click for details →

When a task enters Done or Cancelled, the service closes the linked heavy routine run before it wakes blockers and parents.

finalizeDone
func (s *Service) finalizeDone(ctx context.Context, data *TaskMovedData) error {
  if s.routineRunSyncer != nil {
    terminal := "done"
    if strings.EqualFold(data.ToStepName, "cancelled") {
      terminal = "cancelled"
    }
    if err := s.routineRunSyncer.SyncRunStatus(ctx, data.TaskID, terminal); err != nil {
      s.logger.Warn("sync routine run status", zap.Error(err))
    }
  }
  if err := s.queueBlockersResolvedRuns(ctx, data.TaskID); err != nil {
    s.logger.Error("blocker resolution runs failed", zap.Error(err))
  }
  if data.ParentID != "" {
    if err := s.queueChildrenCompletedRun(ctx, data.ParentID); err != nil {
      s.logger.Warn("children completed run failed", zap.Error(err))
    }
  }
  return nil
}
Wiring and wakeup idempotency handlingapps/backend/internal/backendapp/adapters_office.go ↗
func (a *routineWakeupAdapter) CreateWakeupRequest(ctx context.Context, req *officeroutines.WakeupRequest) error
Click for details →

The adapter translates the SQLite idempotency conflict into the routines sentinel and adds a fail path for lightweight dispatches.

Adapter translation
func (a *routineWakeupAdapter) CreateWakeupRequest(ctx context.Context, req *officeroutines.WakeupRequest) error {
  row := &officesqlite.WakeupRequest{
    ID: req.ID, AgentProfileID: req.AgentProfileID, Source: req.Source,
    Reason: req.Reason, Payload: req.Payload, RequestedAt: req.RequestedAt,
  }
  if req.IdempotencyKey != "" {
    row.IdempotencyKey = sql.NullString{String: req.IdempotencyKey, Valid: true}
  }
  if err := a.repo.CreateWakeupRequest(ctx, row); err != nil {
    if errors.Is(err, officesqlite.ErrWakeupIdempotencyConflict) {
      return officeroutines.ErrWakeupAlreadyRequested
    }
    return err
  }
  return nil
}

func (a *routineWakeupAdapter) FailWakeupRequest(ctx context.Context, requestID, reason string) error {
  return a.repo.MarkWakeupRequestFailed(ctx, requestID, reason)
}
Lightweight dispatch handles conflict and failure
if err := s.wakeup.CreateWakeupRequest(ctx, req); err != nil {
  if errors.Is(err, ErrWakeupAlreadyRequested) {
    return s.finalizeLightweightRun(ctx, run, models.RoutineRunStatusDone)
  }
  s.logger.Warn("create routine wakeup request", zap.String("routine", routine.Name), zap.Error(err))
  return s.finalizeLightweightRun(ctx, run, models.RoutineRunStatusFailed)
}
if err := s.wakeup.Dispatch(ctx, req.ID); err != nil {
  s.logger.Warn("dispatch routine wakeup request", zap.String("wakeup_id", req.ID), zap.Error(err))
  if failErr := s.wakeup.FailWakeupRequest(ctx, req.ID, "dispatch failed"); failErr != nil {
    s.logger.Warn("mark failed routine wakeup request", zap.Error(failErr))
  }
  if finalizeErr := s.finalizeLightweightRun(ctx, run, models.RoutineRunStatusFailed); finalizeErr != nil {
    return finalizeErr
  }
  return fmt.Errorf("dispatch routine wakeup request: %w", err)
}
return s.finalizeLightweightRun(ctx, run, models.RoutineRunStatusDone)
Wire syncer in composition root
routineSvc.SetWakeupEnqueuer(&routineWakeupAdapter{repo: repo, dispatcher: routineWakeupDispatcher})
routineSvc.SetWorkflowEnsurer(&workflowEnsurerAdapter{repo: taskRepo})
routineSvc.SetTaskCreator(&taskCreatorAdapter{taskSvc: services.Task})
if services.Office != nil {
  services.Office.SetRoutineRunSyncer(routineSvc)
}
Terminal state now includes FAILED and new indexesapps/backend/internal/office/repository/sqlite/blockers.go ↗
func (r *Repository) IsTaskInTerminalStep(ctx context.Context, taskID string) (bool, error)
Click for details →

FAILED was missing from the terminal check, so a failed task still blocked dependents and routine gates.

Shared constants and fix
const (
  taskStateCompleted = "COMPLETED"
  taskStateFailed    = "FAILED"
  taskStateCancelled = "CANCELLED"
)

func (r *Repository) IsTaskInTerminalStep(ctx context.Context, taskID string) (bool, error) {
  var state string
  err := r.ro.QueryRowxContext(ctx, r.ro.Rebind(
    `SELECT COALESCE(state, '') FROM tasks WHERE id = ?`), taskID).Scan(&state)
  if err != nil {
    return false, err
  }
  return state == taskStateCompleted || state == taskStateFailed || state == taskStateCancelled, nil
}
Partial indexes for hot paths
CREATE INDEX IF NOT EXISTS idx_office_routine_runs_active_fingerprint
  ON office_routine_runs(routine_id, dispatch_fingerprint, created_at DESC)
  WHERE status = 'task_created';
CREATE INDEX IF NOT EXISTS idx_office_routine_runs_linked_task
  ON office_routine_runs(linked_task_id, created_at DESC)
  WHERE linked_task_id != '';
Failed wakeup becomes terminal
func (r *Repository) MarkWakeupRequestFailed(ctx context.Context, id, reason string) error {
  now := time.Now().UTC()
  _, err := r.db.ExecContext(ctx, r.db.Rebind(`
    UPDATE agent_wakeup_requests
    SET status = ?, reason = ?, finished_at = ?
    WHERE id = ? AND status = ?
  `), WakeupStatusFailed, reason, now, id, WakeupStatusQueued)
  return err
}
Read the changes as a list

Concurrency gate self-heals and closes atomically

apps/backend/internal/office/routines/service.go

The gate now repairs a stale active run before it decides to skip or coalesce, and closes runs with an atomic conditional update.

Self-heal before policy
func (s *RoutineService) selfHealIfTaskTerminal(ctx context.Context, routine *Routine, active *RoutineRun) *RoutineRun {
  if active.LinkedTaskID == "" {
    return active
  }
  terminalStatus, err := s.repo.GetTaskTerminalStatus(ctx, active.LinkedTaskID)
  if err != nil {
    s.logger.Warn("check linked task terminal state", zap.String("run_id", active.ID), zap.Error(err))
    return active
  }
  if terminalStatus == "" {
    return active
  }
  if _, err := s.closeOutRun(ctx, active, terminalStatus); err != nil {
    s.logger.Warn("close out stale active run", zap.String("run_id", active.ID), zap.Error(err))
    return active
  }
  return nil
}
Loop with repair
func (s *RoutineService) applyConcurrencyPolicy(ctx context.Context, routine *Routine, run *RoutineRun, fingerprint string) (models.RoutineRunStatus, error) {
  if routine.ConcurrencyPolicy == models.ConcurrencyPolicyAlwaysCreate {
    return "", nil
  }
  for {
    active, err := s.repo.GetActiveRunForFingerprint(ctx, routine.ID, fingerprint)
    if err != nil {
      return "", fmt.Errorf("check active run: %w", err)
    }
    if active == nil {
      return "", nil
    }
    if repaired := s.selfHealIfTaskTerminal(ctx, routine, active); repaired == nil {
      continue
    } else {
      active = repaired
    }
    switch routine.ConcurrencyPolicy {
    case models.ConcurrencyPolicySkipIfActive:
      _ = s.repo.UpdateRunStatus(ctx, run.ID, models.RoutineRunStatusSkipped, "")
      run.Status = models.RoutineRunStatusSkipped
      return models.RoutineRunStatusSkipped, nil
    case models.ConcurrencyPolicyCoalesceIfActive:
      _ = s.repo.UpdateRunCoalesced(ctx, run.ID, active.ID)
      run.Status = models.RoutineRunStatusCoalesced
      return models.RoutineRunStatusCoalesced, nil
    }
  }
}
Atomic close
func (s *RoutineService) closeOutRun(ctx context.Context, run *RoutineRun, terminalStatus string) (bool, error) {
  status := models.RoutineRunStatusDone
  switch terminalStatus {
  case "cancelled":
    status = models.RoutineRunStatusCancelled
  case "failed", "missing":
    status = models.RoutineRunStatusFailed
  }
  closed, err := s.repo.UpdateRunStatusIfTaskCreated(ctx, run.ID, status, run.LinkedTaskID)
  if err != nil {
    return false, fmt.Errorf("update run status: %w", err)
  }
  return closed, nil
}

func (s *RoutineService) SyncRunStatus(ctx context.Context, taskID, terminalStatus string) error {
  if taskID == "" {
    return nil
  }
  run, err := s.repo.GetRoutineRunByLinkedTaskID(ctx, taskID)
  if err != nil {
    return fmt.Errorf("get routine run by linked task: %w", err)
  }
  if run == nil {
    return nil
  }
  _, err = s.closeOutRun(ctx, run, terminalStatus)
  return err
}

Repository adds terminal-state readers and conditional close

apps/backend/internal/office/repository/sqlite/routines.go

The repo now reads the shared tasks table directly and closes a run only while it is still task_created.

Terminal status reader
func (r *Repository) GetTaskTerminalStatus(ctx context.Context, taskID string) (string, error) {
  var state string
  var archivedAt sql.NullTime
  err := r.ro.QueryRowxContext(ctx, r.ro.Rebind(
    `SELECT COALESCE(state, ''), archived_at FROM tasks WHERE id = ?`), taskID).Scan(&state, &archivedAt)
  if err == sql.ErrNoRows {
    return "missing", nil
  }
  if err != nil {
    return "", err
  }
  if archivedAt.Valid {
    return "cancelled", nil
  }
  switch state {
  case taskStateCompleted:
    return "done", nil
  case taskStateFailed:
    return "failed", nil
  case taskStateCancelled:
    return "cancelled", nil
  default:
    return "", nil
  }
}
Conditional close
func (r *Repository) UpdateRunStatusIfTaskCreated(ctx context.Context, runID string, status models.RoutineRunStatus, linkedTaskID string) (bool, error) {
  now := time.Now().UTC()
  res, err := r.db.ExecContext(ctx, r.db.Rebind(`
    UPDATE office_routine_runs
    SET status = ?, linked_task_id = ?, completed_at = ?
    WHERE id = ? AND status = 'task_created'
  `), status, linkedTaskID, now, runID)
  if err != nil {
    return false, err
  }
  rows, err := res.RowsAffected()
  return rows > 0, err
}
Active means task_created only
func (r *Repository) GetActiveRunForFingerprint(ctx context.Context, routineID, fingerprint string) (*models.RoutineRun, error) {
  var run models.RoutineRun
  err := r.ro.QueryRowxContext(ctx, r.ro.Rebind(`
    SELECT * FROM office_routine_runs
    WHERE routine_id = ? AND dispatch_fingerprint = ?
      AND status = 'task_created'
    ORDER BY created_at DESC LIMIT 1
  `), routineID, fingerprint).StructScan(&run)
  if err == sql.ErrNoRows {
    return nil, nil
  }
  if err != nil {
    return nil, err
  }
  return &run, nil
}
Narrow last_run_at touch
func (r *Repository) TouchRoutineLastRun(ctx context.Context, routineID string, at time.Time) error {
  _, err := r.db.ExecContext(ctx, r.db.Rebind(`
    UPDATE office_routines SET last_run_at = ?, updated_at = ? WHERE id = ?
  `), at, time.Now().UTC(), routineID)
  return err
}

TaskMoved now clears the routine gate

apps/backend/internal/office/service/event_subscribers.go

When a task enters Done or Cancelled, the service closes the linked heavy routine run before it wakes blockers and parents.

finalizeDone
func (s *Service) finalizeDone(ctx context.Context, data *TaskMovedData) error {
  if s.routineRunSyncer != nil {
    terminal := "done"
    if strings.EqualFold(data.ToStepName, "cancelled") {
      terminal = "cancelled"
    }
    if err := s.routineRunSyncer.SyncRunStatus(ctx, data.TaskID, terminal); err != nil {
      s.logger.Warn("sync routine run status", zap.Error(err))
    }
  }
  if err := s.queueBlockersResolvedRuns(ctx, data.TaskID); err != nil {
    s.logger.Error("blocker resolution runs failed", zap.Error(err))
  }
  if data.ParentID != "" {
    if err := s.queueChildrenCompletedRun(ctx, data.ParentID); err != nil {
      s.logger.Warn("children completed run failed", zap.Error(err))
    }
  }
  return nil
}

Wiring and wakeup idempotency handling

apps/backend/internal/backendapp/adapters_office.go

The adapter translates the SQLite idempotency conflict into the routines sentinel and adds a fail path for lightweight dispatches.

Adapter translation
func (a *routineWakeupAdapter) CreateWakeupRequest(ctx context.Context, req *officeroutines.WakeupRequest) error {
  row := &officesqlite.WakeupRequest{
    ID: req.ID, AgentProfileID: req.AgentProfileID, Source: req.Source,
    Reason: req.Reason, Payload: req.Payload, RequestedAt: req.RequestedAt,
  }
  if req.IdempotencyKey != "" {
    row.IdempotencyKey = sql.NullString{String: req.IdempotencyKey, Valid: true}
  }
  if err := a.repo.CreateWakeupRequest(ctx, row); err != nil {
    if errors.Is(err, officesqlite.ErrWakeupIdempotencyConflict) {
      return officeroutines.ErrWakeupAlreadyRequested
    }
    return err
  }
  return nil
}

func (a *routineWakeupAdapter) FailWakeupRequest(ctx context.Context, requestID, reason string) error {
  return a.repo.MarkWakeupRequestFailed(ctx, requestID, reason)
}
Lightweight dispatch handles conflict and failure
if err := s.wakeup.CreateWakeupRequest(ctx, req); err != nil {
  if errors.Is(err, ErrWakeupAlreadyRequested) {
    return s.finalizeLightweightRun(ctx, run, models.RoutineRunStatusDone)
  }
  s.logger.Warn("create routine wakeup request", zap.String("routine", routine.Name), zap.Error(err))
  return s.finalizeLightweightRun(ctx, run, models.RoutineRunStatusFailed)
}
if err := s.wakeup.Dispatch(ctx, req.ID); err != nil {
  s.logger.Warn("dispatch routine wakeup request", zap.String("wakeup_id", req.ID), zap.Error(err))
  if failErr := s.wakeup.FailWakeupRequest(ctx, req.ID, "dispatch failed"); failErr != nil {
    s.logger.Warn("mark failed routine wakeup request", zap.Error(failErr))
  }
  if finalizeErr := s.finalizeLightweightRun(ctx, run, models.RoutineRunStatusFailed); finalizeErr != nil {
    return finalizeErr
  }
  return fmt.Errorf("dispatch routine wakeup request: %w", err)
}
return s.finalizeLightweightRun(ctx, run, models.RoutineRunStatusDone)
Wire syncer in composition root
routineSvc.SetWakeupEnqueuer(&routineWakeupAdapter{repo: repo, dispatcher: routineWakeupDispatcher})
routineSvc.SetWorkflowEnsurer(&workflowEnsurerAdapter{repo: taskRepo})
routineSvc.SetTaskCreator(&taskCreatorAdapter{taskSvc: services.Task})
if services.Office != nil {
  services.Office.SetRoutineRunSyncer(routineSvc)
}

Terminal state now includes FAILED and new indexes

apps/backend/internal/office/repository/sqlite/blockers.go

FAILED was missing from the terminal check, so a failed task still blocked dependents and routine gates.

Shared constants and fix
const (
  taskStateCompleted = "COMPLETED"
  taskStateFailed    = "FAILED"
  taskStateCancelled = "CANCELLED"
)

func (r *Repository) IsTaskInTerminalStep(ctx context.Context, taskID string) (bool, error) {
  var state string
  err := r.ro.QueryRowxContext(ctx, r.ro.Rebind(
    `SELECT COALESCE(state, '') FROM tasks WHERE id = ?`), taskID).Scan(&state)
  if err != nil {
    return false, err
  }
  return state == taskStateCompleted || state == taskStateFailed || state == taskStateCancelled, nil
}
Partial indexes for hot paths
CREATE INDEX IF NOT EXISTS idx_office_routine_runs_active_fingerprint
  ON office_routine_runs(routine_id, dispatch_fingerprint, created_at DESC)
  WHERE status = 'task_created';
CREATE INDEX IF NOT EXISTS idx_office_routine_runs_linked_task
  ON office_routine_runs(linked_task_id, created_at DESC)
  WHERE linked_task_id != '';
Failed wakeup becomes terminal
func (r *Repository) MarkWakeupRequestFailed(ctx context.Context, id, reason string) error {
  now := time.Now().UTC()
  _, err := r.db.ExecContext(ctx, r.db.Rebind(`
    UPDATE agent_wakeup_requests
    SET status = ?, reason = ?, finished_at = ?
    WHERE id = ? AND status = ?
  `), WakeupStatusFailed, reason, now, id, WakeupStatusQueued)
  return err
}

Data and storage

office_routine_runs tracks each fire. Only task_created gates the next fire; terminal states clear the gate.

FieldTypeNotes
idTEXT PKrun id
routine_idTEXT FKparent routine
statusenumreceived, task_created, done, cancelled, failed, skipped, coalesced
dispatch_fingerprintTEXThash of title, description, assignee
linked_task_idTEXTheavy run task id, empty for lightweight
coalesced_into_run_idTEXTtarget run when coalesced
started_atTIMESTAMPfire time
completed_atTIMESTAMPset on terminal close

Risk

6 / 10 Medium
1 low5 medium10 high

Why this score

  • Concurrency gate now mutates runs on read path; a bug could close the wrong run.
  • New cross-table read from tasks table couples office repo to task lifecycle states.
  • Atomic WHERE status = task_created prevents races but needs correct caller handling.

Trade-offs and review notes

Where to look first

  1. Verify SyncRunStatus maps Done, Cancelled, Failed, missing, and archived correctly.
  2. Check that GetActiveRunForFingerprint filters only task_created and self-heal loops.
  3. Confirm MarkWakeupRequestFailed only touches queued rows and lightweight runs finalize as failed.
  4. Review the two partial indexes match the hot queries and do not bloat writes.