sipeed/picoclaw · error

failed to save store: %w

Error message

failed to save store: %w

What it means

After loading and recomputing next-run times, Start() persists the store via saveStoreUnsafe -> fileutil.WriteFileAtomic(storePath, data, 0600). Failure here aborts Start before the run loop goroutine launches. Causes: parent directory of storePath missing, not writable, disk full, or a read-only filesystem. Note load succeeded a moment earlier, so this is a write-path problem, not data corruption.

Source

Thrown at pkg/cron/service.go:97

	cs.loadStore()
	return cs
}

func (cs *CronService) Start() error {
	cs.mu.Lock()
	defer cs.mu.Unlock()

	if cs.running {
		return nil
	}

	if err := cs.loadStore(); err != nil {
		return fmt.Errorf("failed to load store: %w", err)
	}

	cs.recomputeNextRuns()
	if err := cs.saveStoreUnsafe(); err != nil {
		return fmt.Errorf("failed to save store: %w", err)
	}

	cs.stopChan = make(chan struct{})
	if cs.wakeChan == nil {
		cs.wakeChan = make(chan struct{})
	}
	cs.running = true
	go cs.runLoop(cs.stopChan)

	return nil
}

func (cs *CronService) Stop() {
	cs.mu.Lock()
	defer cs.mu.Unlock()

	if !cs.running {
		return

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Ensure the parent directory exists and is writable by the service user: `install -d -m 700 -o <svcuser> $(dirname <storePath>)`
  2. Check space: `df -h $(dirname <storePath>)` and free space if full
  3. Check SELinux/AppArmor denials in the audit log if perms look fine
  4. Verify the storePath value passed to NewCronService matches an actually-mounted location (`mount | grep <dir>`)

Example fix

// before: Start on a path whose dir may not exist
svc := cron.NewCronService(storePath, handler)
if err := svc.Start(); err != nil { return err }

// after: pre-flight the write path
if err := os.MkdirAll(filepath.Dir(storePath), 0o700); err != nil {
    return fmt.Errorf("cron store dir: %w", err)
}
if f, err := os.OpenFile(storePath, os.O_RDWR|os.O_CREATE, 0o600); err != nil {
    return fmt.Errorf("cron store not writable: %w", err)
} else { f.Close() }
if err := svc.Start(); err != nil { return err }
Defensive patterns

Strategy: try-catch

Validate before calling

func storeDirWritable(storePath string) error {
    dir := filepath.Dir(storePath)
    if err := os.MkdirAll(dir, 0o700); err != nil {
        return err
    }
    f, err := os.OpenFile(storePath, os.O_RDWR|os.O_CREATE, 0o600)
    if err != nil {
        return fmt.Errorf("cron store not writable: %w", err)
    }
    return f.Close()
}

Try / catch

if err := svc.Start(); err != nil {
    if strings.Contains(err.Error(), "failed to save store") {
        // write-path problem: surface fs diagnostics instead of retry-looping
        return fmt.Errorf("cron store write failed (check dir perms/space for %s): %w", storePath, err)
    }
    return err
}

Prevention

When it happens

Trigger: NewCronService(storePath, ...) where the directory of storePath does not exist (MkdirAll is not called by Start) or is owned by another user; disk filled between deploy and start; container with read-only data mount; storePath pointing into a tmpfs that was not mounted this boot.

Common situations: Data dir removed by a cleanup script between restarts; daemon runs as non-root but /var/lib/picoclaw is root-owned 0755; migration to a new host where the volume was not attached; SELinux denial on the directory.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/85ca3e2eff265244. Report an issue: GitHub.