How to safely apply Ubuntu/system updates on the trading machine without breaking the live Python bots. Created 2026-07-13 after a real update run (Python 3.12 security patch + others). The rule: baseline → back up → update → re-verify with the smoke test → only trust it when it reads all-green.
Timing comes first. Never update during market hours. The market is 6:30 AM–1:00 PM
Phoenix (MST, no DST — never ET/PT), but the bot cron is */5 6-13 * * 1-5, and the 6-13
hour range fires through 1:55 PM, not 1:00. So the machine isn’t actually quiet until
~2:00 PM on a weekday — the 1 PM-hour runs are plausibly end-of-day position handling, and a
reboot mid-cycle could interrupt a close. Wait until 2:00 PM on a weekday, or do it on a
weekend. Run date rather than trusting the clock in your head.
# 0. BASELINE — prove the stack is green BEFORE you touch anything.
post-update-smoke-test.sh # want: 27 passed, 0 failed (25 on the media server) — RECORD it
# 1. FRESH backups — both restore points. ';' so both run even if one hiccups.
backup-projects.sh ; backup-antigravity.sh
# 1b. VERIFY the fresh backup is restorable (not just "created"):
verify-backups.sh --always-notify # 7z integrity test + content sentinel, one shot
# (or by hand — see "Verify a backup is actually restorable" below)
# 2. UPDATE. Hold claude-desktop so a running remote session survives the upgrade.
sudo apt-mark hold claude-desktop
sudo apt update && sudo apt full-upgrade # full-upgrade, NOT plain upgrade — see below
sudo apt-mark unhold claude-desktop
# — or just use the GUI "Software Updater" if you're at the machine (preferred for a kernel batch).
#
# Why full-upgrade: a KERNEL batch installs NEW packages (linux-image-6.8.0-NNN, its modules,
# and the matching linux-modules-nvidia-*). Plain `apt upgrade` never installs new packages,
# so it holds the kernel back and you'd see "The following packages have been kept back."
# `full-upgrade` pulls them in. It can also REMOVE packages to resolve deps — on Ubuntu that's
# normally just superseded kernels, but read the removal list before you say yes.
# The GUI updater does the right thing here without the choice, which is why it's preferred
# when you're sitting in front of the machine.
# 3. REBOOT if the batch includes a kernel / glibc / OpenSSL (see "Does it need a reboot?").
# Shut the VMware guests down cleanly and PROVE they halted first:
vm-shutdown.sh && sync && sudo reboot
# ---- after reboot ----
# 4. If the KERNEL changed, VMware rebuilds vmmon/vmnet on first launch. That compile needs
# sudo → a PHYSICAL hardware-key touch. Be present; it cannot be done remotely.
# 5. RE-VERIFY — the entire point of the exercise. Run this AFTER you are on the new kernel.
post-update-smoke-test.sh # any check that flips PASS→FAIL vs step 0 is the update's fault
# Reboot check — do NOT trust /var/run/reboot-required on this box. A kernel install here does
# NOT create it (no hook in /etc/kernel/postinst.d/), so "No such file" is a FALSE all-clear —
# it read clean on 2026-08-21 while 6.8.0-138 sat installed and unbooted. Compare directly:
uname -r; ls -1 /boot/vmlinuz-* | sed 's#.*/vmlinuz-##' | sort -V | tail -1
# running == newest installed → you are on the new kernel, done.
# they differ → a reboot into the newer kernel is STILL PENDING.
# 6. Bring the Kali guest back UP if it runs scheduled scans — a reboot leaves it down, and
# the next scheduled scan silently won't fire if you forget. (Kali = the Fri/Sat scans.)
The two steps people skip, and shouldn’t:
- Step 0 and step 5 are the same command, run twice. The baseline is what makes the “after” mean anything — a lone post-update run tells you the stack is green now, but not what the update changed. Skip the baseline and you throw away the whole diagnostic; a check it silently broke reads identically to one that was never green. Record step 0’s
N passed.- Step 1 creates a fresh backup. The
7z tsnippet lower down tests last night’s archive — good, but it doesn’t contain today’s work. Run the backups, then verify that archive.Any check that flips PASS→FAIL after the update is the update’s fault, and it names the exact library or script.
backup-antigravity.sh/home/aztechguy/.local/bin/backup-antigravity.sh · Cron: nightly 1:10 AM~/.gemini/antigravity/
(portfolio2_trader.py, portfolio2_state.json, fetch scripts, and the .env with
Robinhood credentials) — the stuff backup-projects.sh does NOT cover. Skips the 58 MB
of Gemini-CLI internals (.venv, brain, logs)./media/aztechguy/<uuid>/antigravity-backup/antigravity-YYYY-MM-DD.7z, 30-day retention,
same password file as the Projects backup (~/.config/backup/backup.pass).2a3a6a06-1836-43c3-a2ab-0e20a87ed351):
as of 2026-08-02 it is in /etc/fstab and systemd mounts it ~35s after boot. Before that it
was a udisks click-to-mount that only appeared at desktop login — so both backup crons
(1:00 / 1:10 AM) would have silently failed on any boot where nobody had logged in yet.
Never refer to this drive by /dev/ path — NVMe enumeration is not stable here; it was
nvme0n1p1 before the 2026-08-02 reboot and nvme1n1p1 after, while nvme0n1p1 became the
EFI partition. Use the UUID or the mount path.md5sum — different hashes, same size, is the expected healthy state.backup-projects.sh (/usr/local/bin/, → projects-backup/, nightly 1:00 AM).
See Projects online backup.7z t /media/aztechguy/<uuid>/projects-backup/projects-YYYY-MM-DD.7z -p"$(cat ~/.config/backup/backup.pass)"
# want "Everything is Ok"
backup-projects.sh covers (updated 2026-08-08)Two passes into one archive:
| Pass | Source | Stored in the archive as |
|---|---|---|
| 1 | ~/Projects |
Projects/… — unchanged, so 7z x restores exactly as before |
| 2 | ~/.local/bin, /usr/local/bin |
home/aztechguy/.local/bin/…, usr/local/bin/… |
Why two passes: both script directories end in bin, and a single 7z call fails with
Duplicate filename on disk: bin, bin because they collide at the archive root. Pass 2 uses
-spf2 (full path minus root) to keep them distinct. Pass 2 warns but never aborts — a
failure there still leaves a valid Projects archive.
Excluded from pass 2: agy, agy.*.old, __pycache__. The two agy binaries are ~366 MB
combined; including them would roughly triple the nightly archive and the MEGA upload for a
redistributable you would reinstall rather than restore. With them excluded the archive grew
only 144 MB → 145 MB.
Three things share the name “antigravity” — don’t confuse them:
Thing What Where Backed up backup-antigravity.sha script, 2 KB ~/.local/bin/yes, since 2026-08-08 ~/.gemini/antigravity/the data it protects, 117 MB ~/.gemini/yes, into antigravity-*.7zagya 185 MB binary ~/.local/bin/no — excluded on purpose The script and the binary are neighbours in the same folder. Pass 2 archives that folder and then excludes the binary from it. “
~/.local/binis excluded” is wrong — one file in it is. The scripts are the irreplaceable part:post-update-smoke-test.shencodes which venv runs which bot and which libraries each needs. That’s knowledge.agyis software you redownload.
Why this was added: until 2026-08-08 nothing outside ~/Projects and
~/.gemini/antigravity/ was backed up — verified by listing an actual archive and finding
zero entries matching .local/bin or usr/local/bin. That meant post-update-smoke-test.sh,
verify-backups.sh, domain-monitor.sh, morning-briefing.py, run-health-import.sh,
run-buffer-automation.sh and both backup scripts themselves existed in exactly one place.
A drive failure would have taken the tooling that performs and verifies the backups along with
everything else.
Previous version of the script saved at ~/backup-projects.sh.bak-2026-08-08.
Check coverage after any change to it — verify the archive, never the exit code:
A=$(ls -t /media/aztechguy/<uuid>/projects-backup/*.7z | head -1)
7z l "$A" -p"$(cat ~/.config/backup/backup.pass)" | grep -E "post-update-smoke-test|backup-projects"
# both must appear
post-update-smoke-test.sh/home/aztechguy/.local/bin/post-update-smoke-test.shN passed, M failed.post-update-smoke-test.sh| venv | path | runs | key imports |
|---|---|---|---|
| antigravity | ~/.gemini/antigravity-cli/brain/<id>/.venv |
the 4 bots (via run_cycle.sh), Portfolio 2, watchdog, fill logger, paper sim, daily check-in |
robin_stocks, pandas, numpy, openpyxl, dotenv, pandas_market_calendars |
| trading_env | ~/trading_env |
premarket sentiment scanner | yfinance, dotenv |
| calendar-automation | ~/Projects/calendar-automation/.venv |
calendar buffer automation, morning briefing, health Drive import | googleapiclient, google.oauth2, google_auth_oauthlib |
All three symlink to the system /usr/bin/python3.12, so a 3.12 patch update (same
minor version) is ABI-safe and needs no venv recreation. Only a minor-version jump
(3.12 → 3.13) would force recreating the venvs.
== venv interpreters run ==
PASS trading_env python
PASS antigravity python
PASS calendar-automation python
== antigravity venv imports (bots, portfolio2, watchdog, fill logger, paper sim) ==
PASS antigravity: import robin_stocks
PASS antigravity: import pandas
PASS antigravity: import numpy
PASS antigravity: import openpyxl
PASS antigravity: import dotenv
PASS antigravity: import pandas_market_calendars
== trading_env venv imports (premarket scanner) ==
PASS trading_env: import yfinance
PASS trading_env: import dotenv
== calendar-automation venv imports (calendar buffer + briefing + health drive) ==
PASS calendar: import googleapiclient
PASS calendar: import google.oauth2.credentials
PASS calendar: import google_auth_oauthlib
== live scripts compile (syntax intact) ==
PASS compile qqq_options_project/trader.py
PASS compile dia_options_project/trader.py
PASS compile qqq_0dte_project/trader.py
PASS compile antigravity/portfolio2_trader.py
PASS compile shared_trading_lib/watchdog_checkin.py
PASS compile shared_trading_lib/fill_logger.py
PASS compile shared_trading_lib/paper_volume_sim.py
PASS compile options_project/daily_checkin.py
PASS compile dashboard/dashboard_server.py
PASS compile health-automation/health_import.py
PASS compile bin/morning-briefing.py
== NVIDIA GPU runtime power management (display-freeze fix, 2026-08-15) ==
PASS GPU: DynamicPowerManagement=0 (RTD3 off)
PASS GPU: runtime-suspended <1% of uptime
RESULT: 27 passed, 0 failed.
ALL GREEN -- stack is healthy.
The desktop freezes of 08-08, 08-10 and 08-14 were GSP boot failures on RTD3 resume. The fix is
/etc/modprobe.d/nvidia-runtimepm.conf, which only works because it shares a basename with
Ubuntu’s /usr/lib/modprobe.d/nvidia-runtimepm.conf — kmod sorts config files by basename across
all directories and the last assignment wins.
That makes an NVIDIA driver update the one event that can silently undo it, by renaming the vendor file to something that sorts later. A package update is therefore exactly when to check, which is why these live here rather than in a daily cron.
| Check | Why it cannot lie |
|---|---|
DynamicPowerManagement=0 |
the driver reporting its own live config — not a file, not a knob. Anchored with grep -qx so the adjacent DynamicPowerManagementVideoMemoryThreshold: 200 line cannot satisfy it |
runtime-suspended <1% of uptime |
the outcome itself. Broken state was 84–99%, so 1% is a wide margin that still catches the regression. Expressed as a percentage rather than == 0 so a legitimate system suspend cannot raise a false alarm |
power/control is deliberately NOT checked. gpu-manager rewrites it to auto at every
login and that is harmless once RTD3 is off at the driver. Judging by that knob is what made the
original diagnosis take a week.
Both checks are guarded by [ -r /proc/driver/nvidia/params ] and print SKIP on a host with no
NVIDIA driver — so the media server’s expected result stays 25, not 27.
Full detail: NVIDIA GPU Hangs.
FAIL in the smoke test names the exact library or script. An import X fail after
an update usually means a package needs reinstalling in that venv:
<venv>/bin/pip install --force-reinstall X. A compile fail means a syntax problem (rare
from a system update — more likely an unrelated edit).dpkg -l <pkg> shows iF, not ii):
finish it with sudo dpkg --configure -a. If it fails on a service restart (e.g.
openssh-server: Cannot bind any address / process … remains running after unit stopped),
a stale process is squatting the port — kill it then reconfigure:
sudo pkill -x sshd # (example: sshd holding the SSH port)
sudo dpkg --configure -a
This exact issue happened 2026-07-13 with openssh-server; the fix above cleared it.claude-desktop).
To keep working, either apt-mark hold it (above) or use /remote-control to continue from
a phone before letting it update.7z x <archive>.7z -p"$(cat ~/.config/backup/backup.pass)".2026-08-21 — kernel bump 6.8.0-137 → 6.8.0-138 (linux-image-generic), plus libcurl
(GnuTLS + OpenSSL flavours), BIND 9 client tools + libs, libheif/HEIF decoder plugins, Google
Chrome, and power-profiles-daemon. No glibc or openssl-proper — the “OpenSSL flavour” entries
were libcurl, not openssl, so the reboot trigger was purely the kernel. Applied via the GUI
Software Updater ~14:13, after the 2 PM bot-cron close (Fri is Kali scan day). First kernel change
since this runbook was written — first real exercise of the VMware rebuild.
cat /var/run/reboot-required
returned “No such file” — read as “no reboot needed.” It was not. 6.8.0-138 was installed
(dpkg log 14:13:26) while 6.8.0-137 was still running (uname, 6-day uptime). The flag is simply
never created on this box: update-notifier-common is installed and the helper
/usr/share/update-notifier/notify-reboot-required exists, but no hook in
/etc/kernel/postinst.d/ calls it on a kernel install — so a pending-kernel reboot leaves the
flag absent and the check reads clean exactly when a reboot matters most. Reliable check:
uname -r vs the newest /boot/vmlinuz-*. Step 5 and the Notes section were rewritten to use it.vm-shutdown.sh brought Windows 10-purple down cleanly, then rebooted onto 138.vmmon/vmnet rebuilt automatically on first launch — modinfo vmmon vermagic
now 6.8.0-138-generic. Clean, no headers mismatch.*/5 6-13 fires to 1:55 PM, not 1:00); step 2 changed to
apt full-upgrade with the reason inline; and the reboot check fixed per the false-green above.2026-08-08 — not an update run. Found that no automation script was backed up: both
backup scripts, the smoke test, and every cron helper live in ~/.local/bin and
/usr/local/bin, neither of which was in any archive. Fixed by extending
backup-projects.sh (see the coverage section above). Cost: +1 MB per nightly archive.
Also on this day: the GPU-hang and NAS-throughput investigations — see
Troubleshooting.
2026-08-02 — glibc 2.39-0ubuntu8.7→8.8, OpenSSL 3.0.13-0ubuntu3.11→3.12, Samba 4ubuntu9.6→9.7, FreeRDP, tzdata 2026b→2026c, Chrome 150→151. No kernel change. Smoke test 25 passed / 0 failed after the reboot. Notes from this run:
error count since last fsck: 1,
orphan inode list from a cloud sync interrupted 2025-03-10). Cleared with sudo e2fsck -f while
unmounted. e2fsck -fp (preen) will refuse orphan-list repair and exit with
“UNEXPECTED INCONSISTENCY” — that means “needs a human,” not “drive is failing.” Re-run plain
-f interactively and answer yes, then run it again until it reports no modifications.smbd/nmbd were found listening on 0.0.0.0:139/445 serving only stock printer shares
(browseable = no, guest ok = no, no samba user ever created — so the share never actually
worked). Disabled with sudo systemctl disable --now smbd nmbd. CUPS printing is unaffected
— it listens on localhost:631 only and is independent of Samba.libc6) or OpenSSL (libssl3t64), and
always a kernel (linux-image). Running processes keep the old library mapped in memory
until they restart, so the patch isn’t actually live until you reboot./var/run/reboot-required here — it is a false green for kernels. This box
has neither needrestart nor a working reboot-required hook for kernel installs, so a fresh
kernel leaves the flag absent and “No such file” reads as “all clear.” Confirmed 2026-08-21:
6.8.0-138 installed, flag never created, uname still 137. The reliable check is the running
kernel vs the newest installed one:
uname -r; ls -1 /boot/vmlinuz-* | sed 's#.*/vmlinuz-##' | sort -V | tail -1
# differ → reboot still pending
apt list --upgradable | grep linux-image. No kernel change means
VMware’s vmmon/vmnet won’t need rebuilding — that’s the usual thing that breaks
Workstation after an update. Shut both VMs down from inside first (VM → Power → Shut Down Guest).