gastownhall/beads · error

proxy.Shutdown: publish stop epoch: %w

Error message

proxy.Shutdown: publish stop epoch: %w

What it means

proxy.Shutdown begins by advancing the stop epoch so any in-flight start attempt aborts instead of republishing after the stop returns. If advanceStopEpoch fails — typically an I/O error writing the stop-epoch control file in rootDir — Shutdown returns immediately with the 'proxy.Shutdown: publish stop epoch: %w' prefix and never attempts to stop the proxy or backend.

Source

Thrown at internal/storage/dbproxy/proxy/shutdown.go:86

	}
	proxyRecoverable := shutdownErr.proxyErr == nil ||
		errors.Is(shutdownErr.proxyErr, ErrUnverifiableProcess)
	backendRecoverable := shutdownErr.backendErr == nil ||
		errors.Is(shutdownErr.backendErr, ErrUnverifiableProcess)
	return proxyRecoverable && backendRecoverable
}

// Shutdown stops the verified proxy and backend processes for rootDir.
//
// Advancing the stop epoch first makes every start attempt which began before
// this call terminally abort instead of retrying after its child is stopped.
// The proxy spawn marker covers the smaller release-lock-before-exec window:
// Shutdown waits for that marked attempt to either acquire proxy.lock or fail,
// then verifies and stops whatever it published. All waits are bounded by
// shutdownConfirmDeadline.
func Shutdown(rootDir string) error {
	if err := advanceStopEpoch(rootDir); err != nil {
		return fmt.Errorf("proxy.Shutdown: publish stop epoch: %w", err)
	}

	proxyLock, proxyErr := stopAndAcquire(
		rootDir,
		LockFileName,
		PIDFileName,
		pidfile.KindProxy,
		killRecordChecks{
			VerifyRoot:       true,
			CheckSpawnMarker: true,
		},
	)
	if proxyLock != nil {
		defer proxyLock.Unlock()
	}

	backendLock, backendErr := stopAndAcquire(
		rootDir,

View on GitHub (pinned to 71377f2769)

Solutions

  1. Verify rootDir is writable by the current user and has free space (touch <rootDir>/.wtest; df -h <rootDir>).
  2. Fix permissions/ownership on the workspace directory.
  3. Inspect the wrapped cause after the prefix for the exact syscall error and address it.
  4. Once writable, re-run the shutdown; the epoch will publish and stopping can proceed.
  5. If rootDir moved, run the shutdown from the correct workspace path.

Example fix

// before (shell)
bd dolt stop   # proxy.Shutdown: publish stop epoch: open .../stop-epoch: permission denied
// after
sudo chown -R "$USER" .beads   # or move to a writable workspace
bd dolt stop
Defensive patterns

Strategy: validation

Validate before calling

info, err := os.Stat(rootDir)
if err != nil {
    return fmt.Errorf("workspace rootDir missing: %w", err)
}
if f, err := os.OpenFile(filepath.Join(rootDir, ".wtest"), os.O_CREATE|os.O_WRONLY, 0o644); err != nil {
    return fmt.Errorf("cannot write stop-epoch in %s: %w", rootDir, err)
} else {
    f.Close(); os.Remove(filepath.Join(rootDir, ".wtest"))
}

Try / catch

if err := proxy.Shutdown(rootDir); err != nil {
    if strings.Contains(err.Error(), "publish stop epoch") {
        // fix writability/space of rootDir, then retry Shutdown
    }
    return err
}

Prevention

When it happens

Trigger: Calling proxy.Shutdown (or bd dolt stop) when the stop-epoch file in rootDir cannot be written: read-only rootDir, disk full, permission denied, rootDir missing, or the epoch file path is otherwise unwritable.

Common situations: Workspace on a read-only or full filesystem; running as a different user than the process that started the proxy; rootDir moved/renamed between start and stop; SELinux/AppArmor denying writes.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/3dee8f1f5535d18d. Report an issue: GitHub.