DSH plugin toggle hangs on atomic-write lock timeout

TroubleshootingPublished 2026-10-20Author: DeepSeek Plugin Market
DeepSeek HarnessDSHatomic-writepackage.json.lockEPERMplugin management
Disabling a DSH plugin spins for two minutes, then reports an atomic-write writer lock timeout. Why EPERM blocks takeover, and three ways to clear it.

Clicking "disable" starts a spinner that runs for two minutes before returning atomic-write: timed out waiting for the writer lock; deleting package.json.lock from the profile directory makes the retry succeed instantly. This is a common Windows stall in DeepSeek Harness (DSH): the lock file was left behind, and the takeover logic treats EPERM as "the owner is still alive", so it waits out the full timeout (source).

Match the DeepSeek Harness writer lock symptom first

A stall of roughly 120 seconds that reports a writer lock timeout and clears after deleting the lock is this defect; an error after a few seconds is a different permission problem.

  1. Time one disable or enable action on a DSH plugin. Expected: it hangs close to two minutes, not a few seconds.
  2. Record the exact message. Expected: atomic-write: timed out waiting for the writer lock.
  3. Quit DSH, delete the lock, and retry. Expected: the operation completes immediately.
  4. Look for a leftover .lock in the profile directory. Expected: present with an old mtime.
  5. Confirm no second instance uses the same profile. Expected: exactly one DSH process.
TriggerSymptom
DSH crashed or was killedlock file left behind
Antivirus briefly holds the filethe write returns EPERM
Multiple instances on one profilewriters wait on each other
Network or synced drivedelayed handle release

A community source review found that util/atomic-write/src/index.ts:112-125 treats only ESRCH as a dead owner, and the comment at :124 explicitly treats EPERM as alive. The code refuses takeover at :150-152 and raises the timeout at :259-262 after waitMs, which the plugin manager's change() sets to 120000 (#10097).

Why the DeepSeek Harness lock is never taken over

Liveness is decided from one errno, and Windows returns EPERM where the code expects ESRCH, so a stale lock looks alive.

  1. The writer creates package.json.lock and writes its PID. Expected: the lock names its owner.
  2. The writer crashes, leaving the file in place. Expected: nothing cleans it up.
  3. The next writer reads the lock and probes with kill(pid, 0). Expected: an error code comes back.
  4. Windows answers EPERM because the process is inaccessible or a handle is still held. Expected: the code cannot tell the two cases apart.
  5. The code concludes the owner is alive and keeps waiting. Expected: every poll burns the full window.
  6. waitMs expires and the write throws. Expected: the plugin panel stops spinning.

On macOS and Linux a missing process usually yields ESRCH, which is why the two-minute stall is rare there. The platform difference explains why the bug only shows up on some machines.

In short: the code asks "are you alive", Windows answers "I am not allowed to tell you", and the code records that as alive.

Fix 1: remove the stale DeepSeek Harness lock

Once no DSH process is writing the profile, deleting the lock is the fastest recovery; the file holds no data.

  1. Quit DSH completely, including the tray process. Expected: no DSH process remains.
  2. Open ~/.dsh/profiles/<profile>/. Expected: both package.json and the lock are visible.
  3. Copy package.json.lock somewhere for reference. Expected: the scene is preserved.
  4. Delete the lock file. Expected: package.json itself is untouched.
  5. Start DSH and toggle a plugin. Expected: the operation completes without the stall.
  6. Add the profile directory to the antivirus allow list. Expected: fewer repeated holds.
  7. Confirm the plugin list loads. Expected: the disabled state was saved.

Do not delete the lock when another instance uses the same profile, when its mtime keeps changing, or when you are unsure which profile it belongs to.

Fix 2: shorten waitMs so DeepSeek Harness fails fast

Dropping the wait from 120 seconds to 5-10 seconds turns a two-minute hang into a quick, diagnosable error.

  1. Find the plugin manager's change() call and confirm where waitMs comes from. Expected: the 120000 default is visible.
  2. Set the default to 5000 or 10000 and keep it configurable. Expected: the wait is measured in seconds.
  3. Include the lock path, PID, and lock age in the timeout message. Expected: the error identifies the stale lock.
  4. Rebuild, reproduce a stale lock, and confirm the error arrives within ten seconds. Expected: no more two-minute hang.
  5. Watch for false positives. Expected: healthy writes are not cut off.
  6. Disable a batch of plugins. Expected: every action returns quickly.

A shorter wait only surfaces the failure sooner; it does not remove the stale lock. Without the PID and lock age in the message, users still have to dig through the directory.

It also shrinks the headroom for normal writes. A larger package.json or a slow disk can exceed ten seconds, so keep the cap configurable.

Fix 3: take over by PID and lock age in DeepSeek Harness

The root fix replaces "EPERM means alive" with "take over when the PID is gone or the lock is old", without weakening mutual exclusion.

  1. Read the PID and creation time from the lock file. Expected: both fields are available.
  2. Probe the PID first; fall back to lock age when it belongs to another user. Expected: a dead owner is reclaimed and a live one still waits.
  3. Allow takeover after a threshold such as ten minutes with no heartbeat, and log a warning. Expected: stale locks are reclaimed.
  4. Rename the old lock to .stale before creating a new one. Expected: only one writer creates the new lock.
  5. Test four cases: ESRCH, EPERM with a missing PID, EPERM with an old lock, and a healthy owner. Expected: all four pass.
  6. Note the clock-skew caveat for network drives in the pull request. Expected: maintainers know the limit of age checks.

Automatic takeover risks two processes deciding the lock is expired at the same moment. An atomic rename or a file lock must back it up, or exclusion turns into a race.

Verify and set bounds after a DeepSeek Harness lock

Verify both halves: stale locks are reclaimed and live locks still exclude, or the fix creates a new problem.

  1. Create a lock with a nonexistent PID. Expected: it is taken over.
  2. Create a fresh lock whose PID is alive. Expected: the writer waits instead of stealing it.
  3. Toggle a plugin normally. Expected: no timeout.
  4. Start a second instance and write concurrently. Expected: mutual exclusion still holds.
  5. Toggle plugins ten times. Expected: no lock is left behind.
  6. Inspect the profile directory. Expected: no pile of .stale files.

Four limits apply: never delete package.json, never delete a lock while multiple instances run, antivirus is a frequent cause so allow-list the directory, and no official fixed version appears in the discussion. When the plugin list is blank but the CLI works, see plugin hub blank while the CLI works.

FAQ

Why does a DeepSeek Harness plugin toggle spin for two minutes and then report an atomic-write lock timeout?

A DeepSeek Harness lock file was left behind and the process cannot confirm whether its owner exited. The code treats only ESRCH as a dead owner and EPERM as alive, so it waits the full 120 seconds before throwing. Deleting the lock lets the next write create a fresh one.

Is it safe to delete package.json.lock in a DeepSeek Harness profile?

It is safe once no DeepSeek Harness process is writing the profile. The lock holds no data and exists only for mutual exclusion, so the next write recreates it. Never delete package.json itself.

Why does the DeepSeek Harness lock linger on Windows but rarely on macOS?

DeepSeek Harness on Windows sees EPERM more often than ESRCH when a file is held, including after a crash or an antivirus scan. The code treats EPERM as a live owner, so it never takes over.

How do I stop DeepSeek Harness from hanging for two minutes on a stale lock next time?

DeepSeek Harness can lower waitMs and include the PID and lock age in the error, or reclaim a lock whose PID is gone or whose age is excessive. The first change is small; the second needs careful concurrency handling.

Related Terms

atomic-write
atomic-write is the DeepSeek Harness utility that writes configuration files safely by writing a temporary file and replacing the target atomically. A lock file ensures only one writer enters at a time.— https://github.com/deepseek-ai/deepseek-harness/discussions/10097
writer lock
The writer lock is atomic-write's mutex, usually materialized as package.json.lock. Other writers wait while it is held and receive an error once the timeout expires.— https://github.com/deepseek-ai/deepseek-harness/discussions/10097
ESRCH and EPERM
ESRCH means the target process does not exist; EPERM means the operation is not permitted even though the process may exist. The takeover logic treats ESRCH as an exited owner and EPERM as a live one.— https://github.com/deepseek-ai/deepseek-harness/discussions/10097

Sources