Created: 2026-09-07 · Last-updated: 2026-09-30 · Status: DRAFT
This recipe supports Chapter 19's construction. It uses the same Linux user service and SQLite state as the chapter: your Chapter 10 worker, run by lucy-agent.service, installed with the Chapter 19 learner file's service function. It is not a substitute for completing the chapter or observing a real phone interaction.
A release is a checkout of the course at a reviewed commit, with the virtual environment its lockfile describes. Keep the writable state directory outside every release. agent.env belongs to the operator, has mode 0600, and stays outside source control. Do not print its contents while collecting evidence. Run each command below from the course folder of the release named, because the learner files load one another by path.
Inspect before switching
Choose the current and target release paths explicitly. Do not infer the current interpreter from whichever virtual environment happens to be on your terminal's PATH. Read ExecStart in the installed unit and verify that its paths belong to this installation.
systemctl --user cat lucy-agent.service
systemctl --user show lucy-agent.service --property=ActiveState,SubState,MainPID,NRestartsIn the examples below, set LUCY_STATE, LUCY_CURRENT and LUCY_TARGET to your absolute state directory and the current and target course folders. The unit accepts paths without spaces or systemd expansion characters. Install the target's environment from its committed lock before changing a running service:
cd "$LUCY_TARGET"
uv sync --frozen --python 3.12Review the target's migrations and authority changes. Matching schema versions are necessary for a code-only rollback, but do not prove that the older code keeps the current authority contract.
Run this preflight with the target release's interpreter. It reads the state's schema versions, Chapter 4's meta rows, with SQLite's read-only mode and without opening the store, so it cannot migrate anything while deciding whether the target understands it:
cd "$LUCY_TARGET"
.venv/bin/python - "$LUCY_STATE/agent.sqlite" <<'PY'
import runpy
import sqlite3
import sys
from pathlib import Path
path = Path(sys.argv[1]).resolve()
with sqlite3.connect(path.as_uri() + "?mode=ro", uri=True) as connection:
applied = dict(connection.execute("SELECT key, value FROM meta"))
wake = runpy.run_path("book/textbook/learner/profrod_sovereign_agent_ch10_wakeups_learner.py")
known = {
"stock.version": max(wake["STORE"]["MIGRATIONS"]),
"memory.version": max(wake["MEMORY"]["MEMORY_MIGRATIONS"]),
"skills.version": max(wake["SKILLS"]["SKILL_MIGRATIONS"]),
"work.version": max(wake["CHANNEL"]["QUEUE"]["WORK_MIGRATIONS"]),
"channel.version": max(wake["CHANNEL"]["CHANNEL_MIGRATIONS"]),
"wakeups.version": max(wake["WAKE_MIGRATIONS"]),
}
newer = {key: value for key, value in applied.items() if value > known.get(key, 0)}
if newer:
raise SystemExit(f"REFUSED: the target does not understand {newer}")
print("Known schema; pending:", {k: v for k, v in known.items() if applied.get(k, 0) < v})
PYA refusal leaves the running service alone. Do not delete meta rows to bypass it, run older code against newer state, or replace the database with an old file to make the error disappear. Choose compatible code or design an explicit forward repair. Chapter 4's store also refuses, before changing anything, to open a database newer than its code.
Stop the worker and preserve a backup
Stop the service, then take a backup with the current release. The destination must be new; a repeated command refuses instead of replacing the earlier evidence.
systemctl --user stop lucy-agent.service
cd "$LUCY_CURRENT"
.venv/bin/python - "$LUCY_STATE" <<'PY'
import runpy
import sys
from pathlib import Path
ops = runpy.run_path("book/textbook/learner/profrod_sovereign_agent_ch19_operations_learner.py")
state = Path(sys.argv[1])
store = ops["STORE"]["StateStore"](state / "agent.sqlite")
print(ops["backup"](store, state / "before-upgrade.sqlite"))
store.close()
PYSQLite's backup API includes committed state consistently; copying only the live main database file can miss content still in its write-ahead log. Record the backup's release and schema versions alongside its path, and protect it as sensitive business state.
Stopping the worker is part of the compatibility contract. A startup check cannot stop an old process that already opened the database. Coordinate other tools that can write this state as well. A supplier request already admitted before shutdown may still finish remotely; the shutdown sequence cannot recall an external effect.
Migrate and start the target
Open the state with the target while the worker is stopped: opening the store applies its known forward migrations. Then remove the current unit and install the target's, because the unit names its release's course folder and interpreter.
cd "$LUCY_TARGET"
.venv/bin/python - "$LUCY_STATE" "$LUCY_CURRENT" <<'PY'
import runpy
import sys
from pathlib import Path
wake = runpy.run_path("book/textbook/learner/profrod_sovereign_agent_ch10_wakeups_learner.py")
ops = runpy.run_path("book/textbook/learner/profrod_sovereign_agent_ch19_operations_learner.py")
state, current = Path(sys.argv[1]), Path(sys.argv[2])
db, _ = wake["open_shop"](state / "agent.sqlite")
db.close()
print(ops["service"]("uninstall", state, current / ".venv/bin/python", current))
print(ops["service"]("install", state, Path(".venv/bin/python").absolute(), Path.cwd()))
PY
systemctl --user is-active lucy-agent.serviceAn active process is only the first observation. Schedule a unique brief and verify a finished report from the service, then compare retained orders, stock and spending with the pre-switch observations. A service that repeatedly restarts or holds work indefinitely has not passed the upgrade simply because installation exited zero.
User-service availability after reboot also depends on the host's user-manager configuration. Chapter 19's service experiment enabled lingering and the service came back after an actual reboot of its virtual machine. Check that setting deliberately in deployment rather than assuming an open development login will remain forever.
Roll back code without rolling back business history
For a reviewed, schema-compatible prior release, repeat the preflight and the stop, backup and install sequence with the release roles exchanged. Keep the same state. Verify a new work result and retained business records again. The learner code in this edition has been run through installation, a killed process and a reboot; it has not yet been run through an upgrade or a rollback, and those steps above are unrecorded for it.
Restore is a separate recovery operation
A restore changes authority and starts paused. The source snapshot must have the same schema versions as the active database; if it is older, migrate a copy with the current release and retain the original backup. Inspect that copy before restoring. Chapter 19's restore keeps the active database file, so old connections do not become independent writers to an abandoned file.
Resuming requires the supplier's complete account history, an epoch fence and an exact-digest recovery plan. The plan must contain a current count for every product and an explicit delivery observation for every accepted order, including receipts newer than the local backup. Do not treat backup inventory as a current count. Work the snapshot had running is finished with a report saying it was interrupted; ask again rather than assuming it happened.
The supplier used in the chapter's checkpoint is loopback teaching infrastructure with retained history and an epoch-aware write boundary. A real provider lacking complete discovery or an equivalent fence requires a different recovery contract; an empty lookup is not proof that an old request cannot arrive later.
See docs/evidence/book-ch19/ch19-service-receipt-v1.json in the course for the recorded service run. The final acceptance chapter combines these operational mechanisms with the rest of Lucy's day.
Keep building with Prof Rod
Found this material through a colleague, classroom or shared download? Get the complete book at profrod.ai/book and join the Prof Rod learner community. Bring one result, one question or one failure you learned from. Share this resource with another learner and keep its source links with it so they can find the full course and future updates.