From 720245e010b0c2e1e7b3a2276dbd6514fbf0ab8c Mon Sep 17 00:00:00 2001 From: Joakim Persson Date: Thu, 30 Apr 2026 06:51:17 +0000 Subject: [PATCH] Add macOS launchd template, bringing automation parity to macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ship a launchd user agent plist alongside the existing systemd and cron templates so macOS users can schedule mempalace-session without falling back to cron. launchd is the macOS-native equivalent of a systemd user timer: same scheduling model, same log conventions, same single-instance guarantees. - contrib/launchd/se.jordbo.mempalace-session.plist: - Label uses reverse-DNS from the jordbo.se domain for consistency with other user-installed launchd jobs; fork the prefix if reusing this template in a different org. - ProgramArguments points at /Users/USER/.local/bin/mempalace-session (USER is substituted at install time, same pattern as contrib/cron/). - EnvironmentVariables.PATH covers ~/.local/bin, Apple Silicon Homebrew, Intel Homebrew, and system defaults — launchd agents get a minimal PATH by default and the wrapper needs to find mempalace + python3. - StartCalendarInterval matches systemd unit's schedule: Monday 03:00 local. - RunAtLoad=false — load shouldn't trigger a run; schedule does. - ProcessType=Background + LowPriorityIO=true + Nice=10 mirror the systemd unit's Nice=10 + IOSchedulingClass=idle. macOS's automatic App Nap and resource throttling for Background jobs yields to interactive work cleanly. - ExitTimeOut=7200 matches systemd's TimeoutStartSec=7200. - StandardOut/ErrorPath under ~/Library/Logs/ so Console.app surfaces them. - contrib/README.md gains a full launchd section: - Caveat table comparing to systemd (Persistent=true isn't quite matched; RandomizedDelaySec has no equivalent; overlap prevention is automatic). - Install recipe using launchctl bootstrap (modern) with a fallback note for legacy launchctl load -w on older macOS. - Verify section shows launchctl list, launchctl print, log tails, and launchctl kickstart for manual testing. - Uninstall via launchctl bootout. - Chooser table updated: macOS now explicitly points at launchd, not cron. - ARCHITECTURE.md §5, SKILL.md Quick automation pitch, and README.md Keeping it fresh section all updated to mention the three scheduler options and give per-platform quick-starts. Plist XML validated with plistlib. --- ARCHITECTURE.md | 22 ++++- README.md | 17 +++- SKILL.md | 20 +++- contrib/README.md | 81 ++++++++++++++- .../launchd/se.jordbo.mempalace-session.plist | 99 +++++++++++++++++++ 5 files changed, 223 insertions(+), 16 deletions(-) create mode 100644 contrib/launchd/se.jordbo.mempalace-session.plist diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index fb2e4f0..bd14f65 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -230,13 +230,14 @@ The first two are live; the third is batched. They're complementary, not alterna Pick one: -1. **systemd user timer** (recommended on modern Linux). Survives reboots, optional `Persistent=true` catch-up, logs to `journalctl`, background I/O priority. Templates in [`contrib/systemd/`](contrib/systemd/). -2. **cron** (simpler, works anywhere). Templates in [`contrib/cron/`](contrib/cron/). -3. **Manual** — run `mempalace-session` opportunistically. Fine on machines where you're in and out frequently; less fine on long-running devboxes. +1. **systemd user timer** (recommended on Linux). Survives reboots, optional `Persistent=true` catch-up, logs to `journalctl`, background I/O priority. Templates in [`contrib/systemd/`](contrib/systemd/). +2. **launchd user agent** (recommended on macOS). The macOS-native equivalent — runs without a login session, logs to `~/Library/Logs/`, single-instance guarantees, `ProcessType=Background` throttling. Templates in [`contrib/launchd/`](contrib/launchd/). +3. **cron** (simplest, works on BSD and systemd-less Linux distros). Templates in [`contrib/cron/`](contrib/cron/). +4. **Manual** — run `mempalace-session` opportunistically. Fine on machines where you're in and out frequently; less fine on long-running devboxes. -Install recipes, verification commands, and uninstall steps for all three are in [`contrib/README.md`](contrib/README.md). +Install recipes, verification commands, and uninstall steps for all four are in [`contrib/README.md`](contrib/README.md). -Quick-start (systemd user timer): +Quick-start (systemd user timer, Linux): ```bash mkdir -p ~/.config/systemd/user @@ -248,6 +249,17 @@ sudo loginctl enable-linger "$USER" systemctl --user list-timers mempalace-session.timer ``` +Quick-start (launchd user agent, macOS): + +```bash +sed "s|USER|$USER|g" contrib/launchd/se.jordbo.mempalace-session.plist \ + > ~/Library/LaunchAgents/se.jordbo.mempalace-session.plist +mkdir -p ~/Library/Logs +launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/se.jordbo.mempalace-session.plist +launchctl enable "gui/$(id -u)/se.jordbo.mempalace-session" +launchctl list | grep mempalace-session +``` + Quick-start (cron): ```bash diff --git a/README.md b/README.md index 3524ea5..bee218c 100644 --- a/README.md +++ b/README.md @@ -74,10 +74,11 @@ mempalace-docs /workspace/my_project Manual invocation is fine while you're actively driving the machine, but long-running devboxes benefit from a weekly automated mine. [`contrib/`](contrib/) ships ready-to-install templates: -- **systemd user timer** (recommended on modern Linux): survives reboots, catches missed runs, logs to `journalctl`. -- **cron**: simpler, works everywhere, no user-unit awareness needed. +- **systemd user timer** (recommended on Linux): survives reboots, catches missed runs, logs to `journalctl`. +- **launchd user agent** (recommended on macOS): native-equivalent — logs to `~/Library/Logs/`, single-instance guarantees, `ProcessType=Background` throttling. +- **cron**: simplest, works on BSD and systemd-less distros. No user-unit awareness needed. -Quick-start (systemd, weekly Mon 03:00 local): +Quick-start (Linux / systemd, weekly Mon 03:00 local): ```bash mkdir -p ~/.config/systemd/user @@ -87,6 +88,16 @@ systemctl --user enable --now mempalace-session.timer sudo loginctl enable-linger "$USER" # optional, for headless boxes ``` +Quick-start (macOS / launchd, same schedule): + +```bash +sed "s|USER|$USER|g" contrib/launchd/se.jordbo.mempalace-session.plist \ + > ~/Library/LaunchAgents/se.jordbo.mempalace-session.plist +mkdir -p ~/Library/Logs +launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/se.jordbo.mempalace-session.plist +launchctl enable "gui/$(id -u)/se.jordbo.mempalace-session" +``` + See [`contrib/README.md`](contrib/README.md) for full install/verify/uninstall recipes, tuning, and devbox/container caveats. The full operational routine (triggers, cadence, verification) is in [`ARCHITECTURE.md`](ARCHITECTURE.md) §5. ### Containerized (devbox) notes diff --git a/SKILL.md b/SKILL.md index 7f0bcdc..97ef932 100644 --- a/SKILL.md +++ b/SKILL.md @@ -141,10 +141,11 @@ A machine running only one of these has half a memory. Agents loading the `mempa ### Quick automation pitch -If the user has a devbox and hasn't set up automation, offer it: +If the user has a devbox and hasn't set up automation, offer it. Pick the scheduler that matches the OS: + +**Linux (systemd user timer):** ```bash -# One-time setup (modern Linux, systemd user timer) cd ~/mempalace-toolkit mkdir -p ~/.config/systemd/user cp contrib/systemd/*.{service,timer} ~/.config/systemd/user/ @@ -153,7 +154,20 @@ systemctl --user enable --now mempalace-session.timer sudo loginctl enable-linger "$USER" # optional, for headless boxes ``` -Full install/verify/uninstall recipes for both systemd and cron are in [`contrib/README.md`](../../contrib/README.md). The quick-start above defaults to weekly runs on Monday 03:00 local time with a ≤30 min randomized delay. +**macOS (launchd user agent):** + +```bash +cd ~/mempalace-toolkit +sed "s|USER|$USER|g" contrib/launchd/se.jordbo.mempalace-session.plist \ + > ~/Library/LaunchAgents/se.jordbo.mempalace-session.plist +mkdir -p ~/Library/Logs +launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/se.jordbo.mempalace-session.plist +launchctl enable "gui/$(id -u)/se.jordbo.mempalace-session" +``` + +**BSD or systemd-less Linux (cron):** see [`contrib/cron/`](../../contrib/cron/). + +Full install/verify/uninstall recipes for all three are in [`contrib/README.md`](../../contrib/README.md). All three default to weekly runs on Monday 03:00 local time. ## Failure Modes & Fixes diff --git a/contrib/README.md b/contrib/README.md index 5e406be..87bc0ed 100644 --- a/contrib/README.md +++ b/contrib/README.md @@ -66,6 +66,76 @@ systemctl --user daemon-reload --- +## launchd user agent (macOS) + +**Why:** the macOS-native equivalent of a systemd user timer. Runs without a Terminal window open, logs to `~/Library/Logs/`, single-instance guarantees baked in, background-priority scheduling via `ProcessType=Background`. No Homebrew or third-party scheduler required. + +**Caveats vs. systemd:** + +| Systemd feature | launchd equivalent | Notes | +|---|---|---| +| `Persistent=true` catches missed runs | Partial — `StartCalendarInterval` fires on next system-awake time | If the Mac is fully off at scheduled time, the run is skipped. Sleep-at-schedule → fires on wake. | +| `RandomizedDelaySec=30m` | None native | Single-user machines rarely need jitter; add a `sleep $((RANDOM % 1800))` wrapper if you do. | +| `ConditionPathExists` | None native | `mempalace-session` exits cleanly when the opencode DB is missing, so no guard is strictly needed. | +| Lock file for overlap prevention | Automatic | launchd refuses to start a second instance of the same `Label` while one is running. | + +**Install:** + +```bash +# Substitute your username into the template +sed "s|USER|$USER|g" contrib/launchd/se.jordbo.mempalace-session.plist \ + > ~/Library/LaunchAgents/se.jordbo.mempalace-session.plist + +# Ensure the log directory exists +mkdir -p ~/Library/Logs + +# Modern load (macOS 10.11+). "gui/$(id -u)" targets your login session. +launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/se.jordbo.mempalace-session.plist +launchctl enable "gui/$(id -u)/se.jordbo.mempalace-session" +``` + +> On older macOS or if you hit permissions errors with `bootstrap`, fall back to the legacy form: +> `launchctl load -w ~/Library/LaunchAgents/se.jordbo.mempalace-session.plist` + +**Verify:** + +```bash +# Quick check — is the job registered? +launchctl list | grep mempalace-session + +# Detailed state (next run time, last exit code, throttling) +launchctl print "gui/$(id -u)/se.jordbo.mempalace-session" + +# Run log tail +tail -f ~/Library/Logs/mempalace-session.log +tail -f ~/Library/Logs/mempalace-session.err.log + +# Force a run right now (outside the schedule), for testing +launchctl kickstart -p "gui/$(id -u)/se.jordbo.mempalace-session" +``` + +**Uninstall:** + +```bash +launchctl bootout "gui/$(id -u)" ~/Library/LaunchAgents/se.jordbo.mempalace-session.plist +rm ~/Library/LaunchAgents/se.jordbo.mempalace-session.plist +# Optional — keep the old logs for post-mortem, or delete them: +# rm ~/Library/Logs/mempalace-session{,.err}.log +``` + +### What the plist does + +- `Label=se.jordbo.mempalace-session` — reverse-DNS label; shows up in `launchctl list` and Console.app. Change the prefix if you're forking this for a different org. +- `ProgramArguments` — absolute path to `mempalace-session`. Template uses `/Users/USER/.local/bin/mempalace-session`; the install `sed` substitutes your actual username. +- `EnvironmentVariables.PATH` — covers `~/.local/bin`, Apple Silicon Homebrew (`/opt/homebrew/bin`), Intel Homebrew (`/usr/local/bin`), and system defaults. launchd agents get a minimal PATH by default, and `mempalace-session` needs to find `mempalace` + `python3`. +- `StartCalendarInterval` — `Weekday=1, Hour=3, Minute=0` = Monday 03:00. Omit any key to match "any" (e.g. drop `Weekday` for daily). +- `RunAtLoad=false` — don't run on load/reboot, only on schedule. Flip to `true` if you want a run at every boot. +- `ProcessType=Background` + `LowPriorityIO=true` + `Nice=10` — macOS throttles this job's CPU and I/O so it yields to interactive work. +- `ExitTimeOut=7200` — 2h ceiling, matches the systemd unit. +- `StandardOut/ErrorPath` — `~/Library/Logs/` is the macOS convention; Console.app picks these up automatically. + +--- + ## cron **Why:** simpler, ubiquitous, works on any UNIX. No `loginctl enable-linger` dance, no user-units awareness required. @@ -114,13 +184,14 @@ crontab -e # remove the mempalace-session line by hand | Situation | Pick | |---|---| -| Desktop / laptop, modern systemd-based distro | systemd user timer | -| Long-running devbox or server, wants "Persistent=true" catch-up | systemd user timer | -| macOS, BSD, or distro without systemd | cron | +| Desktop / laptop, modern systemd-based Linux distro | systemd user timer | +| macOS (any recent version) | launchd user agent | +| Long-running Linux devbox or server, wants "Persistent=true" catch-up | systemd user timer | +| BSD, Alpine, or Linux distro without systemd | cron | | You already have a cron-based job scheduler on the box | cron | -| You want logs in `journalctl` rather than a file | systemd user timer | +| You want logs in `journalctl` (Linux) or Console.app (macOS) rather than a file | systemd user timer / launchd | -If you're not sure, pick systemd. `Persistent=true` alone is worth it on any box that ever sleeps or reboots. +If you're not sure: **systemd on Linux, launchd on macOS, cron only when neither is available**. All three wrap the same `mempalace-session` command — the difference is purely in *how* the box remembers to run it. --- diff --git a/contrib/launchd/se.jordbo.mempalace-session.plist b/contrib/launchd/se.jordbo.mempalace-session.plist new file mode 100644 index 0000000..c5684fe --- /dev/null +++ b/contrib/launchd/se.jordbo.mempalace-session.plist @@ -0,0 +1,99 @@ + + + + + + + Label + se.jordbo.mempalace-session + + + ProgramArguments + + /Users/USER/.local/bin/mempalace-session + + + + EnvironmentVariables + + PATH + /Users/USER/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin + HOME + /Users/USER + + + + StartCalendarInterval + + Weekday + 1 + Hour + 3 + Minute + 0 + + + + RunAtLoad + + + + ProcessType + Background + LowPriorityIO + + Nice + 10 + + + ExitTimeOut + 7200 + + + StandardOutPath + /Users/USER/Library/Logs/mempalace-session.log + StandardErrorPath + /Users/USER/Library/Logs/mempalace-session.err.log + +