Skip to content

Locks from killed runs are never reclaimed, permanently blocking a feature from review #168

Description

@youhaowei

Summary

A clawpatch review that dies without unwinding (SIGKILL, terminal closed, CI job cancelled, OOM) leaves its per-feature lock file behind. Nothing ever reclaims it. Every later run that selects that feature fails with feature locked: <id>, and because reviewCommand treats the claim failure as fatal, the whole run exits 7 — even when every other feature reviewed cleanly.

The lock file records the information needed to detect this and does not use it:

{
  "lockedByRunId": "20260728T051054-7e273e",
  "lockedAt": "2026-07-28T05:12:08.397Z",
  "hostname": "some-host.local",
  "pid": 29345
}

What we hit

A review of 12 features reviewed 11 and then failed:

clawpatch review feature-error index=3 total=12 feature=feat_config_89dc9a6aee elapsed=0s error=feature locked: feat_config_89dc9a6aee
clawpatch review failed run=20260728T053559-069b53 errors=1
error: feature locked: feat_config_89dc9a6aee

The holder was a run killed earlier that day. Inspecting the lock directory found five stale locks, the oldest two weeks old; all five pids were dead, and all five recorded the local hostname. The eleven successful reviews were still charged for, and the one feature that mattered most for that branch was the one blocked.

Why clean-locks is not the answer

clean-locks exists, but cleanLocksCommand (app.js:232-249) clears every lock unconditionally — no liveness check, no age check:

for (const feature of features) {
    if (feature.lock === null) continue;
    await writeFeature(loaded.paths, { ...feature, status: ..., lock: null, ... });
}

So it cannot be run automatically or from a wrapper: doing so while another review is in flight would steal that run's locks and let two processes review the same feature concurrently. It is only safe when the operator knows nothing else is running — which means recovery from a crashed run is a manual step that the user has to first diagnose from an exit 7.

Suggested fix

Treat a lock as stale when it is provably not held, using what the lock already records:

  • hostname matches the current host and pid is not alive → reclaim it automatically when claiming.
  • Otherwise (different host, or process alive) → leave it, and keep the current conflict error.

That makes the common single-machine case self-healing without weakening the cross-machine guarantee. A lockedAt-based TTL would be a reasonable secondary backstop for the cross-host case, ideally configurable.

Two smaller improvements that would help independently:

  1. Give clean-locks a safe mode--stale-only, applying the liveness rule above, so it can be run automatically by wrappers and CI.
  2. Don't discard a whole run for one locked feature. A lock conflict on one feature is closer to a per-feature error than a fatal run error; reporting it alongside the successful reviews (and exiting non-zero) would preserve the work already paid for.

Reproduction

  1. Start clawpatch review on a repo with several features.
  2. kill -9 the process while a feature is claimed.
  3. Run clawpatch review again — it fails with feature locked: <id> and exit 7, indefinitely, until clean-locks is run manually.

Environment

clawpatch 0.7.1 (latest at time of filing), macOS, node global install.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Urgent regression or broken agent/channel workflow affecting real users now.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:otherThis issue has meaningful maintainer-visible impact outside the owned taxonomy.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions