PR #3536
Sections
Review

feat(office): add a workspace-wide pause to halt autonomous agent launches ↗

main ← feature/office-wide-kill-swi-6eq 129 files +2847 −312 PR #3536 ↗

This PR adds a workspace-wide kill switch that blocks new autonomous agent launches and sweeps active runs, so operators can halt runaway automation with one action.

Why this change

Office agents launch without human input through routines, the scheduler, and wakeup requests. Operators have no single control to stop all new launches when automation misbehaves.

What it does

Architecture, end to end

Operator pauses workspace. Service writes pause row, runs sweep, and all launch gates read the same record. Resume clears the row and launches resume.

flowchart LR
  Op[Operator UI] --> API[Pause Handler\nGET POST pause resume]
  API --> Svc[Pause Service]
  Svc --> DB[(office_workspace_pauses\npartial unique index)]
  Svc --> Sweep[Halt Sweep]
  Sweep --> Runs[(runs + task_sessions\n+ routine tasks)]
  DB --> Gate{PauseGate\nPauseState}
  Gate --> R1[Routine dispatch]
  Gate --> R2[QueueRun]
  Gate --> R3[Scheduler run processing]
  Gate --> R4[Wakeup dispatcher]
  R1 -- blocked --> Skip[(skipped routine run)]
  R2 -- blocked --> Err409[409 workspace_paused]
  R3 -- blocked --> Outcome[run outcome workspace_paused]
  R4 -- blocked --> Skip2[skip wakeup]

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

type WorkspacePause struct
Click for details →

The pause record is the single source of truth. A workspace is paused when one unreleased row exists.

Model
type WorkspacePause struct {
  ID             string     `json:"id" db:"id"`
  WorkspaceID    string     `json:"workspace_id" db:"workspace_id"`
  Reason         string     `json:"reason" db:"reason"`
  CreatedBy      string     `json:"created_by" db:"created_by"`
  CreatedByKind  string     `json:"created_by_kind" db:"created_by_kind"`
  CreatedAt      time.Time  `json:"created_at" db:"created_at"`
  ReleasedAt     *time.Time `json:"released_at,omitempty" db:"released_at"`
  ReleasedBy     string     `json:"released_by,omitempty" db:"released_by"`
  ReleasedReason string     `json:"released_reason,omitempty" db:"released_reason"`
}
Table and index
CREATE TABLE IF NOT EXISTS office_workspace_pauses (
  id                TEXT PRIMARY KEY,
  workspace_id      TEXT NOT NULL,
  reason            TEXT NOT NULL,
  created_by        TEXT NOT NULL,
  created_by_kind   TEXT NOT NULL,
  created_at        TIMESTAMP NOT NULL,
  released_at       TIMESTAMP,
  released_by       TEXT NOT NULL DEFAULT '',
  released_reason   TEXT NOT NULL DEFAULT ''
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_office_workspace_pause_active
  ON office_workspace_pauses(workspace_id) WHERE released_at IS NULL;
Read gate
func (r *Repository) GetActiveWorkspacePause(ctx context.Context, workspaceID string) (*models.WorkspacePause, error) {
  var pause models.WorkspacePause
  err := r.ro.QueryRowxContext(ctx, r.ro.Rebind(`
    SELECT * FROM office_workspace_pauses
    WHERE workspace_id = ? AND released_at IS NULL
  `), workspaceID).StructScan(&pause)
  if err == sql.ErrNoRows {
    return nil, nil
  }
  if err != nil {
    return nil, err
  }
  return &pause, nil
}
Pause service with insert-retry and sweepapps/backend/internal/office/pause/service.go ↗
func (s *Service) Pause(ctx context.Context, workspaceID, reason, actorID, actorKind string) (*PauseResult, error)
Click for details →

The service owns the pause lifecycle, handles concurrent pause and resume races, and triggers the halt sweep after the row is durable.

Gate predicate
func (s *Service) PauseState(ctx context.Context, workspaceID string) (*models.WorkspacePause, error) {
  return s.repo.GetActiveWorkspacePause(ctx, workspaceID)
}
Pause with detached sweep
func (s *Service) Pause(ctx context.Context, workspaceID, reason, actorID, actorKind string) (*PauseResult, error) {
  if err := s.checkWorkspaceExists(ctx, workspaceID); err != nil {
    return nil, err
  }
  trimmedReason, err := normalizeReason(reason, true)
  if err != nil {
    return nil, err
  }
  active, err := s.attemptPause(ctx, workspaceID, trimmedReason, actorID, actorKind)
  if err != nil {
    return nil, err
  }
  sweepCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), haltSweepTimeout)
  defer cancel()
  sweep := s.runHaltSweep(sweepCtx, workspaceID)
  return &PauseResult{Pause: active, Sweep: sweep}, nil
}
Insert-retry-once
func (s *Service) attemptPause(ctx context.Context, workspaceID, reason, actorID, actorKind string) (*models.WorkspacePause, error) {
  for attempt := 0; attempt < 2; attempt++ {
    active, done, err := s.tryPauseOnce(ctx, workspaceID, reason, actorID, actorKind)
    if err != nil {
      return nil, err
    }
    if done {
      return active, nil
    }
  }
  s.logNoop(ctx, workspaceID, actorID, actorKind, "pause", reason, noopCauseLostRace)
  return nil, ErrPauseContended
}
Halt sweep that cancels runs and executionsapps/backend/internal/office/pause/sweep.go ↗
func (s *Service) runHaltSweep(ctx context.Context, workspaceID string) SweepResult
Click for details →

The sweep cancels queued and claimed runs, releases task checkouts, and cancels live task executions. Each step is best-effort and counts failures.

Sweep
func (s *Service) runHaltSweep(ctx context.Context, workspaceID string) SweepResult {
  var result SweepResult
  runs, err := s.repo.ListInflightRunsForWorkspace(ctx, workspaceID)
  if err != nil {
    s.logger.Warn("halt sweep: list inflight runs failed", zap.String("workspace_id", workspaceID), zap.Error(err))
    result.Failures++
    runs = nil
  }
  runIDs := make([]string, 0, len(runs))
  taskIDSet := map[string]struct{}{}
  for _, run := range runs {
    runIDs = append(runIDs, run.RunID)
    if run.TaskID != "" {
      taskIDSet[run.TaskID] = struct{}{}
    }
  }
  if len(runIDs) > 0 {
    cancelled, err := s.repo.CancelRunsForWorkspace(ctx, runIDs, haltSweepCancelReason)
    result.RunsCancelled = cancelled
    if err != nil {
      result.Failures++
    }
    if err := s.repo.ReleaseCheckoutsForWorkspace(ctx, runIDs); err != nil {
      result.Failures++
    }
  }
  liveRoutineTaskIDs, _ := s.repo.ListLiveRoutineTaskIDsForWorkspace(ctx, workspaceID)
  for _, taskID := range liveRoutineTaskIDs {
    taskIDSet[taskID] = struct{}{}
  }
  liveOfficeTaskIDs, _ := s.repo.ListLiveOfficeTaskIDsForWorkspace(ctx, workspaceID)
  for _, taskID := range liveOfficeTaskIDs {
    taskIDSet[taskID] = struct{}{}
  }
  s.cancelTaskExecutions(ctx, taskIDSet, &result)
  return result
}
Result shape
type SweepResult struct {
  RunsCancelled        int64 `json:"runs_cancelled"`
  ExecutionsCancelled  int   `json:"executions_cancelled"`
  ExecutionsNotRunning int   `json:"executions_not_running"`
  Failures             int   `json:"failures"`
}
Shared PauseGate and launch-site wiringapps/backend/internal/office/shared/interfaces.go ↗
type PauseGate interface
Click for details →

One narrow interface lets every launch site check pause without importing the pause package. Wiring in main.go connects the same service to all gates.

Interface and sentinels
var ErrWorkspacePaused = errors.New("office: workspace paused")
var ErrPauseGateUnavailable = errors.New("office: workspace pause state unavailable")

type PauseGate interface {
  PauseState(ctx context.Context, workspaceID string) (*models.WorkspacePause, error)
}
QueueRun gate
func (s *Service) QueueRun(ctx context.Context, agentInstanceID, reason, payload, idempotencyKey string) (runsservice.QueueOutcome, error) {
  agent, err := s.guardAgentStatus(ctx, agentInstanceID)
  if err != nil {
    return runsservice.QueueOutcomeNone, err
  }
  if err := s.checkPauseGateForAgent(ctx, agent, "queue_run"); err != nil {
    return runsservice.QueueOutcomeNone, err
  }
  if s.runsService != nil {
    return s.runsService.QueueRun(ctx, runsservice.QueueRunRequest{
      Reason: reason, IdempotencyKey: idempotencyKey, Payload: payloadWithAgent(payload, agentInstanceID),
    })
  }
  return s.queueRunInline(ctx, agentInstanceID, reason, payload, idempotencyKey)
}
Routine dispatch gate
func (s *RoutineService) checkPauseGate(ctx context.Context, workspaceID, routineID, triggerID, source string) error {
  if s.pauseGate == nil || workspaceID == "" {
    return nil
  }
  active, err := s.pauseGate.PauseState(ctx, workspaceID)
  if err != nil {
    pause.RecordGateError("routine_dispatch")
    return shared.ErrPauseGateUnavailable
  }
  if active == nil {
    return nil
  }
  pause.RecordBlocked("routine_dispatch")
  s.repo.CreatePauseSkippedRoutineRun(ctx, routineID, triggerID, source, active.ID)
  return &pausedDispatchError{workspaceID: active.WorkspaceID, reason: active.Reason}
}
Wiring in main.go
pauseSvc := officepause.NewService(repo, services.Office, services.Task, log)
routineSvc.SetPauseGate(pauseSvc)
routineWakeupDispatcher.SetPauseGate(pauseSvc)
schedulerSvc.SetPauseGate(pauseSvc)
if services.Office != nil {
  services.Office.SetPauseGate(pauseSvc)
}
func RegisterRoutes(api *gin.RouterGroup, h *Handler)
Click for details →

Three endpoints expose pause state. Mutations require workspace.manage, reject agent callers, and bound the request body.

Routes
func RegisterRoutes(api *gin.RouterGroup, h *Handler) {
  api.GET("/workspaces/:wsId/pause", h.getPause)
  api.POST("/workspaces/:wsId/pause", h.postPause)
  api.POST("/workspaces/:wsId/resume", h.postResume)
}
Pause handler
func (h *Handler) postPause(c *gin.Context) {
  if officeagents.CallerFromContext(c) != nil {
    c.JSON(http.StatusForbidden, gin.H{fieldError: "agent callers may not pause a workspace"})
    return
  }
  workspaceID := c.Param("wsId")
  var body pauseRequestBody
  if !h.bindPauseRequestBody(c, &body, false) {
    return
  }
  result, err := h.svc.Pause(c.Request.Context(), workspaceID, body.Reason, actorID(c), actorKind(c))
  if err != nil {
    h.writeMutationError(c, workspaceID, err)
    return
  }
  c.JSON(http.StatusOK, gin.H{
    fieldWorkspaceID: workspaceID,
    fieldPaused:      true,
    fieldPause:       pausePayload(result.Pause),
    "sweep":          result.Sweep,
  })
}
Scope guard
func requiresOfficeWorkspaceManage(c *gin.Context) bool {
  if c.Request.Method != http.MethodPost || c.Param("wsId") == "" {
    return false
  }
  switch c.FullPath() {
  case officeRoutePrefix + "/workspaces/:wsId/pause", officeRoutePrefix + "/workspaces/:wsId/resume":
    return true
  default:
    return false
  }
}
export function useWorkspacePause(workspaceId: string | null): UseWorkspacePauseResult
Click for details →

The hook and store guard against stale responses by workspace and request tag, and the banner keeps pause visible even when the read fails.

Hook read and mutate
const read = useCallback(async () => {
  if (!workspaceId) return;
  const tag = beginPauseRequest();
  try {
    const res = await getWorkspacePause(workspaceId);
    const activeWorkspaceId = storeApi.getState().workspaces.activeId;
    applyPauseResponse(tag, res.workspaceId, activeWorkspaceId, {
      kind: "read-success", paused: res.paused, record: res.record,
    });
  } catch {
    const activeWorkspaceId = storeApi.getState().workspaces.activeId;
    applyPauseResponse(tag, workspaceId, activeWorkspaceId, { kind: "read-failure" });
  }
}, [workspaceId, beginPauseRequest, applyPauseResponse, storeApi]);
Store guard
applyPauseResponse: (tag, responseWorkspaceId, activeWorkspaceId, outcome): boolean => {
  let applied = false;
  set((draft) => {
    const pause = draft.office.pause;
    if (responseWorkspaceId !== activeWorkspaceId || tag <= pause.appliedSeq) return;
    applied = true;
    pause.appliedSeq = tag;
    switch (outcome.kind) {
      case "read-success":
      case "mutate-success":
        pause.status = "known";
        pause.record = outcome.paused ? outcome.record : null;
        return;
      case "read-failure":
        pause.status = "unknown";
        return;
      case "mutate-failure":
        return;
    }
  });
  return applied;
}
Banner states
export function WorkspacePauseBanner() {
  const activeWorkspaceId = useAppStore((s) => s.workspaces.activeId);
  const { record, status, refresh, pause, retryPause, resume, sweep } = useWorkspacePause(activeWorkspaceId);
  if (!activeWorkspaceId) return null;
  if (record) return <PausedBanner record={record} stale={status === "unknown"} onRefresh={refresh} onResume={resume} sweep={sweep} onRetryPause={retryPause} />;
  if (status === "unknown") return <PauseStateUnavailableBar onRefresh={refresh} onPause={pause} />;
  return <RunningControlBar onRefresh={refresh} onPause={pause} />;
}
Read the changes as a list

Durable pause record and storage

apps/backend/internal/office/models/workspace_pause.go

The pause record is the single source of truth. A workspace is paused when one unreleased row exists.

Model
type WorkspacePause struct {
  ID             string     `json:"id" db:"id"`
  WorkspaceID    string     `json:"workspace_id" db:"workspace_id"`
  Reason         string     `json:"reason" db:"reason"`
  CreatedBy      string     `json:"created_by" db:"created_by"`
  CreatedByKind  string     `json:"created_by_kind" db:"created_by_kind"`
  CreatedAt      time.Time  `json:"created_at" db:"created_at"`
  ReleasedAt     *time.Time `json:"released_at,omitempty" db:"released_at"`
  ReleasedBy     string     `json:"released_by,omitempty" db:"released_by"`
  ReleasedReason string     `json:"released_reason,omitempty" db:"released_reason"`
}
Table and index
CREATE TABLE IF NOT EXISTS office_workspace_pauses (
  id                TEXT PRIMARY KEY,
  workspace_id      TEXT NOT NULL,
  reason            TEXT NOT NULL,
  created_by        TEXT NOT NULL,
  created_by_kind   TEXT NOT NULL,
  created_at        TIMESTAMP NOT NULL,
  released_at       TIMESTAMP,
  released_by       TEXT NOT NULL DEFAULT '',
  released_reason   TEXT NOT NULL DEFAULT ''
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_office_workspace_pause_active
  ON office_workspace_pauses(workspace_id) WHERE released_at IS NULL;
Read gate
func (r *Repository) GetActiveWorkspacePause(ctx context.Context, workspaceID string) (*models.WorkspacePause, error) {
  var pause models.WorkspacePause
  err := r.ro.QueryRowxContext(ctx, r.ro.Rebind(`
    SELECT * FROM office_workspace_pauses
    WHERE workspace_id = ? AND released_at IS NULL
  `), workspaceID).StructScan(&pause)
  if err == sql.ErrNoRows {
    return nil, nil
  }
  if err != nil {
    return nil, err
  }
  return &pause, nil
}

Pause service with insert-retry and sweep

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

The service owns the pause lifecycle, handles concurrent pause and resume races, and triggers the halt sweep after the row is durable.

Gate predicate
func (s *Service) PauseState(ctx context.Context, workspaceID string) (*models.WorkspacePause, error) {
  return s.repo.GetActiveWorkspacePause(ctx, workspaceID)
}
Pause with detached sweep
func (s *Service) Pause(ctx context.Context, workspaceID, reason, actorID, actorKind string) (*PauseResult, error) {
  if err := s.checkWorkspaceExists(ctx, workspaceID); err != nil {
    return nil, err
  }
  trimmedReason, err := normalizeReason(reason, true)
  if err != nil {
    return nil, err
  }
  active, err := s.attemptPause(ctx, workspaceID, trimmedReason, actorID, actorKind)
  if err != nil {
    return nil, err
  }
  sweepCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), haltSweepTimeout)
  defer cancel()
  sweep := s.runHaltSweep(sweepCtx, workspaceID)
  return &PauseResult{Pause: active, Sweep: sweep}, nil
}
Insert-retry-once
func (s *Service) attemptPause(ctx context.Context, workspaceID, reason, actorID, actorKind string) (*models.WorkspacePause, error) {
  for attempt := 0; attempt < 2; attempt++ {
    active, done, err := s.tryPauseOnce(ctx, workspaceID, reason, actorID, actorKind)
    if err != nil {
      return nil, err
    }
    if done {
      return active, nil
    }
  }
  s.logNoop(ctx, workspaceID, actorID, actorKind, "pause", reason, noopCauseLostRace)
  return nil, ErrPauseContended
}

Halt sweep that cancels runs and executions

apps/backend/internal/office/pause/sweep.go

The sweep cancels queued and claimed runs, releases task checkouts, and cancels live task executions. Each step is best-effort and counts failures.

Sweep
func (s *Service) runHaltSweep(ctx context.Context, workspaceID string) SweepResult {
  var result SweepResult
  runs, err := s.repo.ListInflightRunsForWorkspace(ctx, workspaceID)
  if err != nil {
    s.logger.Warn("halt sweep: list inflight runs failed", zap.String("workspace_id", workspaceID), zap.Error(err))
    result.Failures++
    runs = nil
  }
  runIDs := make([]string, 0, len(runs))
  taskIDSet := map[string]struct{}{}
  for _, run := range runs {
    runIDs = append(runIDs, run.RunID)
    if run.TaskID != "" {
      taskIDSet[run.TaskID] = struct{}{}
    }
  }
  if len(runIDs) > 0 {
    cancelled, err := s.repo.CancelRunsForWorkspace(ctx, runIDs, haltSweepCancelReason)
    result.RunsCancelled = cancelled
    if err != nil {
      result.Failures++
    }
    if err := s.repo.ReleaseCheckoutsForWorkspace(ctx, runIDs); err != nil {
      result.Failures++
    }
  }
  liveRoutineTaskIDs, _ := s.repo.ListLiveRoutineTaskIDsForWorkspace(ctx, workspaceID)
  for _, taskID := range liveRoutineTaskIDs {
    taskIDSet[taskID] = struct{}{}
  }
  liveOfficeTaskIDs, _ := s.repo.ListLiveOfficeTaskIDsForWorkspace(ctx, workspaceID)
  for _, taskID := range liveOfficeTaskIDs {
    taskIDSet[taskID] = struct{}{}
  }
  s.cancelTaskExecutions(ctx, taskIDSet, &result)
  return result
}
Result shape
type SweepResult struct {
  RunsCancelled        int64 `json:"runs_cancelled"`
  ExecutionsCancelled  int   `json:"executions_cancelled"`
  ExecutionsNotRunning int   `json:"executions_not_running"`
  Failures             int   `json:"failures"`
}

Shared PauseGate and launch-site wiring

apps/backend/internal/office/shared/interfaces.go

One narrow interface lets every launch site check pause without importing the pause package. Wiring in main.go connects the same service to all gates.

Interface and sentinels
var ErrWorkspacePaused = errors.New("office: workspace paused")
var ErrPauseGateUnavailable = errors.New("office: workspace pause state unavailable")

type PauseGate interface {
  PauseState(ctx context.Context, workspaceID string) (*models.WorkspacePause, error)
}
QueueRun gate
func (s *Service) QueueRun(ctx context.Context, agentInstanceID, reason, payload, idempotencyKey string) (runsservice.QueueOutcome, error) {
  agent, err := s.guardAgentStatus(ctx, agentInstanceID)
  if err != nil {
    return runsservice.QueueOutcomeNone, err
  }
  if err := s.checkPauseGateForAgent(ctx, agent, "queue_run"); err != nil {
    return runsservice.QueueOutcomeNone, err
  }
  if s.runsService != nil {
    return s.runsService.QueueRun(ctx, runsservice.QueueRunRequest{
      Reason: reason, IdempotencyKey: idempotencyKey, Payload: payloadWithAgent(payload, agentInstanceID),
    })
  }
  return s.queueRunInline(ctx, agentInstanceID, reason, payload, idempotencyKey)
}
Routine dispatch gate
func (s *RoutineService) checkPauseGate(ctx context.Context, workspaceID, routineID, triggerID, source string) error {
  if s.pauseGate == nil || workspaceID == "" {
    return nil
  }
  active, err := s.pauseGate.PauseState(ctx, workspaceID)
  if err != nil {
    pause.RecordGateError("routine_dispatch")
    return shared.ErrPauseGateUnavailable
  }
  if active == nil {
    return nil
  }
  pause.RecordBlocked("routine_dispatch")
  s.repo.CreatePauseSkippedRoutineRun(ctx, routineID, triggerID, source, active.ID)
  return &pausedDispatchError{workspaceID: active.WorkspaceID, reason: active.Reason}
}
Wiring in main.go
pauseSvc := officepause.NewService(repo, services.Office, services.Task, log)
routineSvc.SetPauseGate(pauseSvc)
routineWakeupDispatcher.SetPauseGate(pauseSvc)
schedulerSvc.SetPauseGate(pauseSvc)
if services.Office != nil {
  services.Office.SetPauseGate(pauseSvc)
}

HTTP surface and authorization

apps/backend/internal/office/pause/handler.go

Three endpoints expose pause state. Mutations require workspace.manage, reject agent callers, and bound the request body.

Routes
func RegisterRoutes(api *gin.RouterGroup, h *Handler) {
  api.GET("/workspaces/:wsId/pause", h.getPause)
  api.POST("/workspaces/:wsId/pause", h.postPause)
  api.POST("/workspaces/:wsId/resume", h.postResume)
}
Pause handler
func (h *Handler) postPause(c *gin.Context) {
  if officeagents.CallerFromContext(c) != nil {
    c.JSON(http.StatusForbidden, gin.H{fieldError: "agent callers may not pause a workspace"})
    return
  }
  workspaceID := c.Param("wsId")
  var body pauseRequestBody
  if !h.bindPauseRequestBody(c, &body, false) {
    return
  }
  result, err := h.svc.Pause(c.Request.Context(), workspaceID, body.Reason, actorID(c), actorKind(c))
  if err != nil {
    h.writeMutationError(c, workspaceID, err)
    return
  }
  c.JSON(http.StatusOK, gin.H{
    fieldWorkspaceID: workspaceID,
    fieldPaused:      true,
    fieldPause:       pausePayload(result.Pause),
    "sweep":          result.Sweep,
  })
}
Scope guard
func requiresOfficeWorkspaceManage(c *gin.Context) bool {
  if c.Request.Method != http.MethodPost || c.Param("wsId") == "" {
    return false
  }
  switch c.FullPath() {
  case officeRoutePrefix + "/workspaces/:wsId/pause", officeRoutePrefix + "/workspaces/:wsId/resume":
    return true
  default:
    return false
  }
}

Frontend state, banner, and controls

apps/web/hooks/domains/office/use-workspace-pause.ts

The hook and store guard against stale responses by workspace and request tag, and the banner keeps pause visible even when the read fails.

Hook read and mutate
const read = useCallback(async () => {
  if (!workspaceId) return;
  const tag = beginPauseRequest();
  try {
    const res = await getWorkspacePause(workspaceId);
    const activeWorkspaceId = storeApi.getState().workspaces.activeId;
    applyPauseResponse(tag, res.workspaceId, activeWorkspaceId, {
      kind: "read-success", paused: res.paused, record: res.record,
    });
  } catch {
    const activeWorkspaceId = storeApi.getState().workspaces.activeId;
    applyPauseResponse(tag, workspaceId, activeWorkspaceId, { kind: "read-failure" });
  }
}, [workspaceId, beginPauseRequest, applyPauseResponse, storeApi]);
Store guard
applyPauseResponse: (tag, responseWorkspaceId, activeWorkspaceId, outcome): boolean => {
  let applied = false;
  set((draft) => {
    const pause = draft.office.pause;
    if (responseWorkspaceId !== activeWorkspaceId || tag <= pause.appliedSeq) return;
    applied = true;
    pause.appliedSeq = tag;
    switch (outcome.kind) {
      case "read-success":
      case "mutate-success":
        pause.status = "known";
        pause.record = outcome.paused ? outcome.record : null;
        return;
      case "read-failure":
        pause.status = "unknown";
        return;
      case "mutate-failure":
        return;
    }
  });
  return applied;
}
Banner states
export function WorkspacePauseBanner() {
  const activeWorkspaceId = useAppStore((s) => s.workspaces.activeId);
  const { record, status, refresh, pause, retryPause, resume, sweep } = useWorkspacePause(activeWorkspaceId);
  if (!activeWorkspaceId) return null;
  if (record) return <PausedBanner record={record} stale={status === "unknown"} onRefresh={refresh} onResume={resume} sweep={sweep} onRetryPause={retryPause} />;
  if (status === "unknown") return <PauseStateUnavailableBar onRefresh={refresh} onPause={pause} />;
  return <RunningControlBar onRefresh={refresh} onPause={pause} />;
}

Data and storage

One active row per workspace controls all gates. History rows stay for audit. Skipped routine runs link to the pause that blocked them.

FieldTypeNotes
office_workspace_pauses.idtext PKuuid for the pause event
workspace_idtextworkspace that is paused
reasontextoperator reason, max 500 code points
created_by / created_by_kindtextactor id and kind user
created_attimestampwhen pause started
released_attimestamp nullablenull means active, set on resume
released_by / released_reasontextwho resumed and why
idx_office_workspace_pause_activepartial unique indexunique on workspace_id where released_at is null
office_routine_runs.pause_idtext FKlinks skipped run to blocking pause, unique per routine and pause
office_activity_logtableworkspace_paused, workspace_resumed, workspace_pause_noop entries

Risk

6 / 10 Medium
1 low5 medium10 high

Why this score

  • Touches every autonomous launch path; a gate bug blocks all new work or lets paused work through.
  • Adds a new table and partial unique index with concurrent insert and resume races that need correct handling.
  • Halt sweep is best-effort across runs, checkouts, and executions; partial failures must not hide the pause.

Trade-offs and review notes

Where to look first

  1. Check PauseState error handling at each gate: routine dispatch, QueueRun, scheduler run processing, and wakeup dispatcher all fail closed with the right status.
  2. Verify the insert-retry-once and CAS release handle concurrent pause and resume without duplicate rows or lost audit entries.
  3. Confirm the sweep covers taskless runs, heavy routine tasks, and live Office sessions, and that failures increment the single failures count.
  4. Review frontend tag and workspace guards in applyPauseResponse and the banner stale handling.