diff --git a/README.md b/README.md index 3f66d5d..2072c1a 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,7 @@ Manual invocation is fine while you're actively driving the machine, but long-ru - **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. +- **Devbox variants** (`*-devbox.*`): if you run `mempalace-session` inside a long-lived container (e.g. [opencode-devbox](https://gitea.jordbo.se/joakimp/opencode-devbox)), the scheduler lives on the **host** and uses `docker exec` to reach the tool inside the container. Systemd and cron variants are included; both guard against "container currently stopped" so the timer is safe to leave enabled across dev cycles. Quick-start (Linux / systemd, weekly Mon 03:00 local): @@ -284,7 +285,20 @@ launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/se.jordbo.mempalace-se 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. +Quick-start (host-side scheduling for a long-running opencode-devbox container): + +```bash +# systemd on the host → docker exec into the container +mkdir -p ~/.config/systemd/user +cp contrib/systemd/mempalace-session-devbox.{service,timer} ~/.config/systemd/user/ +systemctl --user daemon-reload +systemctl --user enable --now mempalace-session-devbox.timer +# If your container isn't named 'opencode-devbox' or its user isn't +# 'developer', run `systemctl --user edit mempalace-session-devbox.service` +# to set CONTAINER / CONTAINER_USER via an override. +``` + +See [`contrib/README.md`](contrib/README.md) for full install/verify/uninstall recipes, tuning, chooser table (host vs. devbox), and devbox/container caveats. The full operational routine (triggers, cadence, verification) is in [`ARCHITECTURE.md`](ARCHITECTURE.md) §5. ### Containerized (devbox) notes diff --git a/contrib/README.md b/contrib/README.md index 87bc0ed..4cb89df 100644 --- a/contrib/README.md +++ b/contrib/README.md @@ -184,25 +184,115 @@ crontab -e # remove the mempalace-session line by hand | Situation | Pick | |---|---| -| 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 | +| Desktop / laptop, mempalace installed directly on the host, modern systemd-based Linux | `systemd/mempalace-session.{service,timer}` | +| macOS (any recent version), mempalace on the host | `launchd/se.jordbo.mempalace-session.plist` | +| Long-running Linux devbox or server, mempalace on the host | `systemd/mempalace-session.{service,timer}` | +| **opencode-devbox (or similar) container with mempalace *inside* it** | **`systemd/mempalace-session-devbox.{service,timer}`** (preferred) or `cron/mempalace-session-devbox.cron` (simpler) | +| BSD, Alpine, or Linux distro without systemd | `cron/mempalace-session.cron` | +| You already have a cron-based job scheduler on the box | any `cron/*.cron` template | | You want logs in `journalctl` (Linux) or Console.app (macOS) rather than a file | systemd user timer / launchd | -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. +If you're not sure: **systemd on Linux, launchd on macOS, cron only when neither is available. Use the `-devbox` variant when mempalace lives inside a long-running container rather than on the host.** All templates wrap the same `mempalace-session` command — the difference is purely in *where* the scheduler lives and whether it needs to `docker exec` into a container to reach the tool. --- ## Running inside a container (devbox) -Inside a Docker-based devbox, neither systemd nor cron typically runs by default. Two options: +If you run opencode inside a long-lived container like [opencode-devbox](https://gitea.jordbo.se/joakimp/opencode-devbox), neither systemd nor cron is running inside that container — they're host-level services. The correct pattern is **host-side scheduling that `docker exec`s into the running container**. The `*-devbox` templates in `contrib/systemd/` and `contrib/cron/` implement exactly this. -1. **Schedule on the host, not the container** — have the host run `docker exec -u mempalace-session` on a timer. The container must be long-running (not per-invocation) for this to work. -2. **Run a systemd-in-container setup** — viable but usually not worth the complexity for this alone. +Preconditions: -For most devbox users, a simple weekly manual run via `mempalace-session` (or a host-side cron that shells into the container) is the pragmatic choice. The tool is cheap enough that skipping a week costs nothing — dedup will catch up on the next run. +- **Long-lived container.** `docker compose up -d` with `restart: unless-stopped` (or equivalent). If the container is ephemeral (per-invocation), this pattern doesn't apply. +- **`mempalace-session` is already installed inside the container.** opencode-devbox bakes it in via `mempalace-toolkit`, so a running devbox already satisfies this. +- **Host user can talk to docker.** Member of the `docker` group on Linux, or Docker Desktop running under the current login session on macOS. +- **Canonical container name is `opencode-devbox`.** If you renamed it via `container_name:` or docker-compose project naming, adjust `CONTAINER` in the template. + +Both devbox templates guard against "container currently stopped" — they no-op silently if `docker ps` shows no running container with the expected name. That makes the timer safe to leave enabled across dev cycles where you tear the container down and bring it back up. + +### systemd user timer (host-side, devbox variant) + +```bash +# Install +mkdir -p ~/.config/systemd/user +cp contrib/systemd/mempalace-session-devbox.service ~/.config/systemd/user/ +cp contrib/systemd/mempalace-session-devbox.timer ~/.config/systemd/user/ +systemctl --user daemon-reload +systemctl --user enable --now mempalace-session-devbox.timer + +# Keep the timer running across logout (typical on dev hosts) +sudo loginctl enable-linger "$USER" +``` + +**Customize container name / user** (if you don't use the defaults): + +```bash +systemctl --user edit mempalace-session-devbox.service +# In the override that opens, set: +# [Service] +# Environment=CONTAINER=my-devbox-name +# Environment=CONTAINER_USER=my-user +``` + +Or edit the shipped service file in place before copying. + +**Verify:** + +```bash +systemctl --user list-timers mempalace-session-devbox.timer +systemctl --user status mempalace-session-devbox.service +journalctl --user -u mempalace-session-devbox --since today + +# Force a run right now (while the container is up) +systemctl --user start mempalace-session-devbox.service +``` + +**Behaviour when the container is down:** `ExecCondition` fails, service is marked "condition failed" (considered a successful no-op by systemd), no alert noise. Bring the container back up and the next scheduled fire will run normally. + +**Uninstall:** + +```bash +systemctl --user disable --now mempalace-session-devbox.timer +rm ~/.config/systemd/user/mempalace-session-devbox.{service,timer} +systemctl --user daemon-reload +``` + +### cron (host-side, devbox variant) + +```bash +# Read the template — it has CONTAINER / CONTAINER_USER at the top. +# Adjust if your setup differs from opencode-devbox defaults. +cat contrib/cron/mempalace-session-devbox.cron + +# Install (preserves existing crontab entries) +(crontab -l 2>/dev/null; cat contrib/cron/mempalace-session-devbox.cron) | crontab - + +# Ensure the log directory exists +mkdir -p ~/.cache/mempalace-session + +# Verify +crontab -l | grep mempalace-session-devbox +tail -f ~/.cache/mempalace-session/cron-devbox.log +``` + +**Uninstall:** + +```bash +crontab -e # remove the mempalace-session-devbox line by hand +``` + +### When to pick devbox variants vs direct ones + +| Setup | Templates to use | +|---|---| +| mempalace installed directly on the host (no devbox) | `mempalace-session.{service,timer}`, `mempalace-session.cron`, `se.jordbo.mempalace-session.plist` | +| opencode-devbox container is where opencode + mempalace live | `mempalace-session-devbox.{service,timer}`, `mempalace-session-devbox.cron` | +| Both (rare — mempalace on host AND inside a separate devbox) | Install both; they write to separate palaces. | + +The in-container mempalace sees only the container's opencode.db and palace (via named volumes). The host's mempalace, if installed, sees only the host's. Two parallel palaces don't cross-pollinate — decide where you want the source of truth to live and schedule accordingly. + +### Alternative not documented here + +"Run systemd inside the container" is technically viable (systemd-in-docker images exist) but adds non-trivial complexity for the sake of a once-weekly batch job. The host-scheduled approach above is equivalent in outcome and much simpler. Skip systemd-in-container unless you already have other reasons to need it. --- diff --git a/contrib/cron/mempalace-session-devbox.cron b/contrib/cron/mempalace-session-devbox.cron new file mode 100644 index 0000000..ba6eff9 --- /dev/null +++ b/contrib/cron/mempalace-session-devbox.cron @@ -0,0 +1,38 @@ +# Sample crontab entry for mempalace-session inside a long-running +# opencode-devbox container, scheduled from the DOCKER HOST (not inside +# the container — containers typically don't run cron). +# +# Host-side cron runs `docker exec` against the running container once a +# week. Requires: +# - Container is long-lived (compose up, restart: unless-stopped, etc.) +# - User running cron has rights to talk to the docker socket +# (usually means being in the `docker` group, or on macOS having +# Docker Desktop running for the current login session) +# - `mempalace-session` is already installed inside the container +# (opencode-devbox bakes it in via cli_utils → mempalace-toolkit) +# +# Install: +# # Replace CONTAINER / USER / HOST_USER to match your setup, then: +# (crontab -l 2>/dev/null; cat contrib/cron/mempalace-session-devbox.cron) | crontab - +# +# Adjust CONTAINER and USER below before installing. +# +# Design notes: +# - `docker ps --filter name=... --filter status=running` makes the job a +# no-op if the container is down, so the timer is harmless on machines +# where the devbox is currently stopped. No mail/warning/noise. +# - Exec runs as `developer` (the opencode-devbox user). Change `-u` +# if you named your user something else. +# - Output is captured in a log under the HOST user's home, not inside +# the container — so you can inspect it from outside. + +CONTAINER=opencode-devbox +CONTAINER_USER=developer + +# HOST_USER is used only to anchor the log path. Replace with your host +# username (or leave the $HOME substitution if your cron implementation +# expands it — most do). +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +# m h dom mon dow command +0 3 * * 1 /bin/sh -c 'docker ps --filter "name=^/${CONTAINER}$" --filter "status=running" -q | grep -q . && docker exec -u "${CONTAINER_USER}" "${CONTAINER}" mempalace-session >> "$HOME/.cache/mempalace-session/cron-devbox.log" 2>&1' diff --git a/contrib/systemd/mempalace-session-devbox.service b/contrib/systemd/mempalace-session-devbox.service new file mode 100644 index 0000000..ecda4d6 --- /dev/null +++ b/contrib/systemd/mempalace-session-devbox.service @@ -0,0 +1,34 @@ +[Unit] +Description=Mine opencode session history into MemPalace (inside opencode-devbox container) +Documentation=https://gitea.jordbo.se/joakimp/mempalace-toolkit +# Only run if docker is reachable AND the container is currently up. +# The ExecCondition below gives us the "container running" check at fire +# time; these unit-level conditions just skip pointless scheduling churn +# on boxes where docker isn't even installed. +ConditionPathExists=/usr/bin/docker + +[Service] +Type=oneshot +# Environment knobs — set these in +# ~/.config/systemd/user/mempalace-session-devbox.service.d/override.conf +# via `systemctl --user edit mempalace-session-devbox.service` +# (or just edit this file in place before installing). +Environment=CONTAINER=opencode-devbox +Environment=CONTAINER_USER=developer + +# Skip cleanly if the container isn't running. ExecCondition failing +# marks the unit as "condition failed" (success from systemd's POV, no +# alert noise). If this Condition passes, ExecStart fires. +ExecCondition=/bin/sh -c 'docker ps --filter "name=^/${CONTAINER}$" --filter "status=running" -q | grep -q .' +ExecStart=/usr/bin/docker exec -u ${CONTAINER_USER} ${CONTAINER} mempalace-session + +# Host-side log (journalctl --user -u mempalace-session-devbox --since today) +# captures the captured stdout/stderr from the `docker exec` above. + +# 2h runaway ceiling — matches the in-container service unit. Enough for +# a cold-start mine on a large corpus plus post-mine repair. +TimeoutStartSec=7200 + +# Low priority on the host side. The work actually happens inside the +# container; the host-side dockerd call is negligible. +Nice=5 diff --git a/contrib/systemd/mempalace-session-devbox.timer b/contrib/systemd/mempalace-session-devbox.timer new file mode 100644 index 0000000..70d94bb --- /dev/null +++ b/contrib/systemd/mempalace-session-devbox.timer @@ -0,0 +1,17 @@ +[Unit] +Description=Weekly opencode-devbox → MemPalace session mine (host-scheduled) +Documentation=https://gitea.jordbo.se/joakimp/mempalace-toolkit + +[Timer] +# Every Monday at 03:00 local time. Adjust to taste. +# See `systemctl --user list-timers mempalace-session-devbox.timer` for +# the resolved next-run time after enabling. +OnCalendar=Mon 03:00 +# If the host was off at the scheduled time, run at next boot. +Persistent=true +# Randomize up to 30 minutes to spread load if you fleet this. +RandomizedDelaySec=30m +AccuracySec=1m + +[Install] +WantedBy=timers.target