gastownhall/beads · warning
reading last_pull: %w
Error message
reading last_pull: %w
What it means
ReadLastPullTimestamp tolerates a missing file (returns zero time, nil), but if os.ReadFile fails for any OTHER reason — permission denied, path is a directory, I/O error — it wraps the failure as "reading last_pull: %w" and returns it. Callers can then distinguish 'never pulled' from 'cannot read the timestamp file'.
Source
Thrown at internal/linear/staleness.go:39
}
path := filepath.Join(beadsDir, lastPullFileName)
ts := time.Now().UTC().Format(time.RFC3339)
return os.WriteFile(path, []byte(ts+"\n"), 0600)
}
// ReadLastPullTimestamp reads the last pull timestamp from .beads/last_pull.
// Returns the zero time if the file doesn't exist or is unreadable.
func ReadLastPullTimestamp(beadsDir string) (time.Time, error) {
if beadsDir == "" {
return time.Time{}, fmt.Errorf("beadsDir must not be empty")
}
path := filepath.Join(beadsDir, lastPullFileName)
data, err := os.ReadFile(path) // #nosec G304 -- path is constrained to the beads directory.
if err != nil {
if os.IsNotExist(err) {
return time.Time{}, nil
}
return time.Time{}, fmt.Errorf("reading last_pull: %w", err)
}
ts := strings.TrimSpace(string(data))
if ts == "" {
return time.Time{}, nil
}
t, err := time.Parse(time.RFC3339, ts)
if err != nil {
return time.Time{}, fmt.Errorf("parsing last_pull timestamp %q: %w", ts, err)
}
return t, nil
}
// IsPullStale returns true if the last pull is older than the given threshold,
// or if no pull has ever been recorded.
func IsPullStale(beadsDir string, threshold time.Duration) bool {
lastPull, err := ReadLastPullTimestamp(beadsDir)
if err != nil || lastPull.IsZero() {
return trueView on GitHub (pinned to 71377f2769)
Solutions
- Check and fix permissions on <beadsDir>/last_pull (should be 0600, owned by the running user)
- If last_pull is a directory or corrupt, delete it — the next WriteLastPullTimestamp recreates it
- Verify the process user matches the owner of the .beads directory
- Check filesystem mount status (read-only remount, disk full)
Example fix
// repair a corrupt/unreadable last_pull
if err := ReadLastPullTimestamp(beadsDir); err != nil {
os.Remove(filepath.Join(beadsDir, "last_pull"))
WriteLastPullTimestamp(beadsDir)
} Defensive patterns
Strategy: fallback
Validate before calling
// pre-flight: file exists AND is a regular file
path := filepath.Join(beadsDir, "last_pull")
if st, err := os.Stat(path); err == nil && st.IsDir() {
os.Remove(path)
} Type guard
null
Try / catch
t, err := ReadLastPullTimestamp(beadsDir)
if err != nil {
log.Printf("last_pull unreadable, treating as never pulled: %v", err)
t = time.Time{}
} Prevention
- Keep .beads owned by the same user that runs sync commands
- Exclude .beads from permission-rewriting tools and security hardening
- Monitor for unexpected files/directories named last_pull
When it happens
Trigger: os.ReadFile(<beadsDir>/last_pull) fails with an error other than os.IsNotExist: the file exists but is unreadable (permission bits), last_pull is actually a directory, or a disk/IO error occurs.
Common situations: File permissions changed by another user or a security-hardening tool; a directory named last_pull created by mistake; running as a different user than the one that created .beads; read-only filesystem mount.
Related errors
- failed to open file: %w
- read rules directory: %w
- failed to read config.yaml: %w
- reading config: %w
- dolt path is not executable
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/eb50f4a87041d1d4.
Report an issue: GitHub.