feat(watcher): inbox zero — archived raw/ originals are removed, not copied
CI / Lint / Test / Vet (pull_request) Successful in 2m27s
CI / Mirror to GitHub (pull_request) Skipped

After a note is archived into raw/processed/ or raw/failed/, remove the
original from raw/ so raw/ only holds unprocessed input. The .processed /
.failed marker is now only a fallback when the removal itself fails.
Existing markers are still honoured.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-19 20:59:06 +02:00
co-authored by Claude Sonnet 5.5
parent 18d01b64d7
commit 6bf61d75c0
2 changed files with 28 additions and 19 deletions
+21 -9
View File
@@ -127,9 +127,12 @@ func processDir(ctx context.Context, cfg Config, date string) []error {
return errs
}
// processFile reads a file, calls pipeline.Run, copies it to processed/ or failed/,
// and writes a marker file next to the original so the watcher skips it next poll.
// The original file is never deleted, keeping Syncthing-connected vaults (e.g. Obsidian) intact.
// processFile reads a file, calls pipeline.Run, and archives it into processed/
// or failed/. The original is then removed from raw/ so that raw/ only ever
// holds unprocessed input (inbox zero). The removal syncs to Syncthing peers;
// the archive copy under raw/processed/ or raw/failed/ is what they keep. A
// marker is written next to the original only if the removal fails, so a
// stuck file is not re-ingested every poll.
func processFile(ctx context.Context, cfg Config, root *os.Root, rawDir, path, date string) error {
filename := filepath.Base(path)
source := deriveSource(filename)
@@ -154,9 +157,7 @@ func processFile(ctx context.Context, cfg Config, root *os.Root, rawDir, path, d
if cpErr := copyFromRoot(root, rel, dest); cpErr != nil {
return fmt.Errorf("copy to failed: %w", cpErr)
}
if mkErr := os.WriteFile(path+".failed", []byte(runErr.Error()), 0o644); mkErr != nil {
slog.Error("watcher: failed to write .failed marker", "error", mkErr)
}
retire(root, rel, path+".failed", runErr.Error())
slog.Warn("watcher: file failed", "file", filename, "error", runErr)
@@ -182,14 +183,25 @@ func processFile(ctx context.Context, cfg Config, root *os.Root, rawDir, path, d
if err := copyFromRoot(root, rel, dest); err != nil {
return fmt.Errorf("copy to processed: %w", err)
}
if err := os.WriteFile(path+".processed", []byte(date), 0o644); err != nil {
slog.Error("watcher: failed to write .processed marker", "error", err)
}
retire(root, rel, path+".processed", date)
slog.Info("watcher: file processed", "file", filename, "source", source)
return nil
}
// retire removes an archived original from raw/ (inbox zero). If the removal
// fails it falls back to a marker file so the watcher still skips the file.
func retire(root *os.Root, rel, marker, markerBody string) {
rmErr := root.Remove(rel)
if rmErr == nil {
return
}
slog.Error("watcher: failed to remove archived original; writing marker", "file", rel, "error", rmErr)
if err := os.WriteFile(marker, []byte(markerBody), 0o644); err != nil {
slog.Error("watcher: failed to write marker", "marker", marker, "error", err)
}
}
// deriveSource turns a filename into a human-readable source name.
// "shape-up-book.md" → "Shape Up Book"
func deriveSource(filename string) string {