Skip to content

fix(execution-workspaces): clear closed_at on reopen via PATCH (SHA-2492)#12

Open
Mlaz-code wants to merge 1 commit into
sharpapi-on-v2026.428.0from
sha-2492-patch-normalize-closed-at
Open

fix(execution-workspaces): clear closed_at on reopen via PATCH (SHA-2492)#12
Mlaz-code wants to merge 1 commit into
sharpapi-on-v2026.428.0from
sha-2492-patch-normalize-closed-at

Conversation

@Mlaz-code
Copy link
Copy Markdown
Owner

Summary

Fixes the first half of SHA-2492execution_workspaces rows ending up in (status='active', closed_at=stamped) after a status-only PATCH.

The PATCH route at server/src/routes/execution-workspaces.ts:456-468 accepts a status patch but never touches closed_at. Transitioning a row from archived/cleanup_failed back to active/idle via the API leaves closed_at populated. isClosedIsolatedExecutionWorkspace ORs both signals, so the row fails-closed and any subsequent comment/PATCH on a linked issue 409s "linked to closed workspace" even though the workspace is in use.

Triggered live on 2026-05-08 when Coordinator PATCHed {status:'active'} on an SHA-2105 workspace to unblock the issue — succeeded, but left the row contradictory. We've found 1037 of these on the prod paperclip DB, all from manual cleanup recipes that paired closed_at with status='idle'. Tracked in Paperclip-issue SHA-2492.

Changes

  • packages/shared/src/execution-workspace-guards.ts — export CLOSED_EXECUTION_WORKSPACE_STATUSES + new isClosedExecutionWorkspaceStatus(status) helper so the route + the existing isClosedIsolatedExecutionWorkspace guard share one source of truth.
  • packages/shared/src/index.ts — re-export the two new symbols.
  • server/src/routes/execution-workspaces.ts — in the non-archive PATCH branch, if existing.status is closed and the requested status is open, also clear closedAt and (unless the request explicitly sets it) cleanupReason.
  • server/src/__tests__/execution-workspaces-routes.test.ts — two new route tests: (a) reopen clears closedAt+cleanupReason; (b) status-not-changed leaves both fields untouched.

Companion work (separate PRs / steps, not in this branch)

  1. Recurring offender stopped/shared-tmp/worktree-cleanup.sh (daily worktree-cleanup.timer, originally SHA-1847) was the prime accreting source. Edited 2026-05-12 to write status='archived' instead of 'idle'; verified live on the 2026-05-13 04:33 EDT fire.
  2. DB invariantCHECK (closed_at IS NULL OR status IN ('archived','cleanup_failed')), NOT VALID + post-backfill VALIDATE. Separate PR.
  3. BackfillUPDATE execution_workspaces SET status='archived' WHERE closed_at IS NOT NULL AND status NOT IN ('archived','cleanup_failed') on the prod paperclip DB (1037 rows expected; pre-audit confirmed 0 rows have a non-terminal source issue, so bulk archive is safe). Manual step pending Board approval.

Test plan

  • bash -n on the script fix; observed archived rows produced on the next timer fire
  • New route tests cover reopen-clears + no-op paths
  • CI runs full suite on this branch
  • After merge: PATCH a real archived row and confirm closed_at is NULL on the response

…492)

When a PATCH transitions an execution_workspace row from a closed status
(archived/cleanup_failed) back to an open one (active/idle), the route
left closed_at and cleanup_reason populated. That produces a row that's
"closed-by-timestamp, open-by-status" — isClosedIsolatedExecutionWorkspace
ORs both signals, so subsequent issue comments/PATCHes 409 with "linked
to closed workspace" even though the workspace is in use.

Triggered by SHA-2105 on 2026-05-08: Coordinator PATCHed status=active
on an idle/closed_at-stamped row to unblock the issue; the row stayed
contradictory and 1037 sibling rows had been quietly accumulating from
manual cleanup recipes that paired closed_at with status='idle'.

Fix: in the non-archive PATCH branch, if existing.status is closed and
the requested status is open, also clear closedAt and (unless explicitly
set in the request) cleanupReason. Exports CLOSED_EXECUTION_WORKSPACE_STATUSES
and a new isClosedExecutionWorkspaceStatus helper so the same set drives
the route + the existing guard.

Adds two route tests covering the reopen-clears-closedAt path and the
no-op-when-status-not-changed path.

Companion changes coming in a follow-up: a CHECK constraint making the
contradictory state representationally impossible, plus a backfill of
the 1037 existing rows.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant