sipeed/picoclaw · error

failed to load store: %w

Error message

failed to load store: %w

What it means

CronService.Start() first calls loadStore, which reads the JSON store file at cs.storePath (a missing file is fine - fresh empty store) and json.Unmarshals it. Any other read error (permission denied, path is a directory, I/O error) or an Unmarshal failure (truncated/corrupt JSON, schema mismatch) is wrapped as "failed to load store". Start aborts and the cron loop never launches.

Source

Thrown at pkg/cron/service.go:92

		onJob:     onJob,
		gronx:     gronx.New(),
		wakeChan:  make(chan struct{}),
	}
	// Initialize and load store on creation
	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() {

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Validate the file: `jq . <storePath>` - if jq complains, restore from backup or delete the file (jobs are lost but the service starts fresh)
  2. Fix ownership/permissions so the service user can read it: `chown <svcuser> <storePath>` (file is written 0600)
  3. If a schema/version mismatch is suspected, downgrade to the version that wrote the file, export jobs, then re-import
  4. Check the exact underlying error in the wrapped chain - Unmarshal errors point at the byte offset of the corruption

Example fix

# before: start with a corrupt store, service refuses to boot
picoclaw-daemon  # Start() -> failed to load store

# after: verify and repair before starting
jq empty /var/lib/picoclaw/cron.json || cp /var/lib/picoclaw/cron.json.bak /var/lib/picoclaw/cron.json
chown picoclaw:picoclaw /var/lib/picoclaw/cron.json
picoclaw-daemon
Defensive patterns

Strategy: validation

Validate before calling

func storeFileSound(path string) error {
    data, err := os.ReadFile(path)
    if os.IsNotExist(err) {
        return nil // fresh store is fine
    }
    if err != nil {
        return fmt.Errorf("cron store unreadable: %w", err)
    }
    if !json.Valid(data) {
        return fmt.Errorf("cron store %s is not valid JSON", path)
    }
    return nil
}

Type guard

func isStoreCorruption(err error) bool {
    return err != nil && strings.Contains(err.Error(), "failed to load store") &&
        (strings.Contains(err.Error(), "invalid character") ||
         strings.Contains(err.Error(), "unexpected end of JSON"))
}

Try / catch

if err := svc.Start(); err != nil {
    if isStoreCorruption(err) {
        // quarantine the corrupt file and start fresh rather than crashing forever
        os.Rename(storePath, storePath+".corrupt")
        return svc.Start()
    }
    return err
}

Prevention

When it happens

Trigger: NewCronService(storePath, handler) then Start() where the store file was truncated by a crash during a non-atomic write (older version without WriteFileAtomic), a root-owned 0600 file being read by a non-root service, someone hand-edited the JSON, or a future schema version wrote fields this build cannot parse.

Common situations: Upgrading picoclaw across store-format changes; moving data dirs between users; disk-full events that previously truncated the JSON; manually editing cron.json and leaving a trailing comma; restoring a backup with wrong ownership.

Related errors


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