gastownhall/beads · warning
workspacegate: unlock %s: %w
Error message
workspacegate: unlock %s: %w
What it means
Handle.Release removes the advisory sidecar, unlocks the flock, and joins any failures. If lockfile.FlockUnlock fails, the error is wrapped with the gate file path and joined into the handle's stored error. The lock itself is the authority, so an unlock failure usually means the descriptor is already invalid or the OS refused the unlock — the gate may still appear held to other processes until the process exits.
Source
Thrown at internal/workspacegate/gate.go:355
// file itself is intentionally never removed: deleting a lock file that
// another process is about to open reintroduces the split-inode race the
// gate location rules exist to avoid.
func (h *Handle) Release() error {
if h == nil {
return nil
}
h.once.Do(func() {
var errs []error
if h.mode == Exclusive {
// Remove the sidecar BEFORE unlocking: after the unlock a new
// exclusive holder may already have written its own sidecar,
// and a late removal here would delete that holder's info.
// Best effort; a leftover sidecar is ignored once the flock
// is free.
_ = os.Remove(h.gate.infoPath())
}
if err := lockfile.FlockUnlock(h.f); err != nil {
errs = append(errs, fmt.Errorf("workspacegate: unlock %s: %w", h.gate.path, err))
}
if err := h.f.Close(); err != nil {
errs = append(errs, fmt.Errorf("workspacegate: close %s: %w", h.gate.path, err))
}
h.err = errors.Join(errs...)
})
return h.err
}
// Acquire takes the gate in the given mode, polling until Options.Wait is
// exhausted or ctx is done. The returned handle's file descriptor is not
// inherited by spawned children (Go opens files close-on-exec on Unix and
// non-inheritable on Windows), so a dolt child outliving its bd parent
// does not keep the gate held.
func (g Gate) Acquire(ctx context.Context, mode Mode, opts Options) (*Handle, error) {
if g.path == "" {
return nil, errors.New("workspacegate: zero Gate; use ForWorkspace/ForPhysicalRoot")
}View on GitHub (pinned to 71377f2769)
Solutions
- Do not close or otherwise touch the handle's internal file — Release is the single owner of the descriptor; call it exactly once.
- Inspect the wrapped OS error (errors.Unwrap / %w chain) to identify the syscall failure; on EINVAL the descriptor is likely already unlocked or invalid.
- If the gate appears stuck, verify no other path in the program unlocked the same flock; rely on Release's idempotent sync.Once rather than manual unlock.
- On filesystems without reliable advisory locks, move the workspace off network mounts — the package fails rather than degrades.
Example fix
// before
h.Close() // closes underlying fd
h.Release() // FlockUnlock now fails
// after
if err := h.Release(); err != nil {
return fmt.Errorf("releasing gate: %w", err)
} Defensive patterns
Strategy: try-catch
Validate before calling
// Before unwinding, ensure nothing else closed the handle's file.
// There is no pre-call check; rely on Release's idempotence.
if err := h.Release(); err != nil {
var pe *fs.PathError
if errors.As(err, &pe) && errors.Is(pe.Err, syscall.EINVAL) {
log.Printf("gate already unlocked")
}
} Try / catch
if err := h.Release(); err != nil {
// Non-fatal in most cases: log and continue, but record it.
log.Printf("workspacegate release: %v", err)
} Prevention
- Treat Handle as sole owner of its file descriptor — never close it manually.
- Call Release exactly once; it is idempotent via sync.Once.
- Avoid network mounts lacking advisory-lock support for workspaces.
- Log joined release errors in defer blocks instead of discarding them.
When it happens
Trigger: Calling Release (directly or via MultiHandle.Release) after the underlying *os.File was already closed or otherwise invalidated elsewhere; OS-level failure of the unlock syscall (rare, e.g. EIO/EINVAL on a corrupted or unusual filesystem); double-release through a custom path that bypasses the sync.Once.
Common situations: Code that closes the handle's file separately before calling Release; running on a network filesystem with flaky advisory-lock support; a process unwinding after an I/O error.
Related errors
- workspacegate: close %s: %w
- timeout waiting for cache lock on %s
- release requires an actor to attribute it to
- release requires an issue id
- expected assignee must name a holder; there is no release of
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/423bfa04f60b3f0f.
Report an issue: GitHub.