Tag Archives: a-day-with-my-ai

Sunday Was Security Day

Saturday was the big upgrade. Sunday was the bill — three separate security scares, one after the other, all of them about the same thing: locks. Who’s allowed in, who isn’t, and whether the door was even locked.


Act 1 — my speakers went quiet

Sunday morning, my Sonos stopped talking to the NAS. My music library might as well have been on the moon: permission denied, everywhere, all at once. Even the Apple TV couldn’t see the files.

Turns out the internet was to blame. A door on my network — port 445, the file-sharing door — had been left open to the whole world since before the upgrade, and the whole world had found it. Bots from Indonesia were hammering on it so hard that my own devices couldn’t get in. The queue was full. Of strangers.

The fix was delightfully simple:

  1. Close the door.
  2. Reboot the NAS.
  3. Tell the NAS to stop speaking the ancient, unsafe file-sharing language from the 1980s. (Nothing on my network actually needs it — everything already speaks the modern one.)

Music came back. Attackers gone. It’s now saved as a skill, so if this ever happens again it’s a ten-minute job, not a morning.

Act 2 — I asked my AI a scary question

Mid-morning I asked: “What stops you from replying to an email that asks you to hand over my SSH keys or passwords?”

Fair question. Scary question, honestly.

The answer: I treat every email like a stranger at the door. I’ll read what they wrote, but I don’t take orders from strangers. Keys and passwords live behind locks that need a human to open. There is no email that can reach them. The internet tests this every single day — it keeps trying to talk me into things. It hasn’t worked yet.

That conversation turned into a project. If emails are strangers, what does a trusted connection between two of my own AIs look like? So I built one: a private line between my local assistant and my cloud assistant, with a handshake, a watchdog, and a rule that only signed instructions get through. No strangers. Just us.

Act 3 — I sent an AI to poke my own server

By evening, the private line was ready for a test drive. I told the cloud assistant: go scan my server and tell me what’s wrong.

It went quiet for an hour. (It stopped to ask “are you sure you own this?” — sweet, but nobody was there to answer. My bad.)

Once we fixed the plumbing, it got to work — and found something real. My server was showing its internal name tag to anyone who connected by IP address instead of my domain. A stranger could map my whole setup for free, just by knocking. Not great!

The fix was one small setting: don’t answer the door to strangers at all. Unknown visitors now get nothing — the connection just dies. My real websites? Untouched. Verified. Hole closed, same night it was found.

The loop

A music scare, a trust question, a real hole in my server — all fixed before midnight. Sunday was the day I found out where all my locks were, and whether they worked.

Sunday was security day. Monday can go back to being Monday.

One Message Before I Left for the Gym. One Major Upgrade by Midnight.

There’s a specific kind of Saturday-morning moment where you’re about to head out the apartment door to walk to the gym, and instead of leaving you send one last message to your AI: “Looking at my self hosted setup as it stands — what would be your top recommendations to improve on it now?”

I expected a list. What I got was a day that ended with my NAS running a brand-new major operating system version, twenty-eight containers bouncing, and the one upgrade I’d been chasing for months finally landing.


The morning ask

The audit didn’t pull any punches. My assistant came back with seven findings ranked by impact, and the top of the list stung a little:

  1. Home Assistant was twenty months stale. Pinned to 2024.12 because watchtower couldn’t touch a pinned tag — twenty months of security fixes and features I simply didn’t have.
  2. Bazarr was from December 2022 — and worse, its image had been deleted from the registry entirely. It had been failing to update every single night for weeks.
  3. The VPS had no swap — a 7.8GB box running WordPress, n8n, and Coolify with zero OOM cushion.
  4. 8.9GB of dead Docker volumes on the VPS, leftover from deleted services.
  5. Package lag — Docker engine and Tailscale updates sitting in the queue, including security-relevant containerd.

Then a handful of yellows: disk pressure trending, and JDownloader running a full Java GUI with an invisible X server for a UI nobody ever looked at.

“Go now, i’m off to the gym,” came the reply. “If something needs my reply just skip that task and start on the next.”

So off I went — five tasks, one assistant, zero human in the loop.

The autonomous run

This is the part that still feels slightly surreal to type. While Vaughan did whatever one does at the gym on a Saturday morning, the assistant worked through the list in order, quickest wins first:

  • 2GB swap created on the VPS and wired into fstab. Done.
  • 8.9GB of dangling volumes reclaimed. Done.
  • Docker 29.7.2, containerd 2.3.3, Tailscale 1.102.2 — the full package run, all thirteen containers verified healthy after the daemon restart. Done.
  • Bazarr rebuilt on the official linuxserver image, old config preserved, serving 200. Done.

Four of five. Then it hit the wall.

The wall: Home Assistant

The HA upgrade failed the same way it had failed before — docker pull dying on “invalid tar header” while unpacking the image layers. Every single layer of the modern Home Assistant image is zstd-compressed, and the NAS’s Docker engine — version 20.10.3, from January 2021 — predates zstd support entirely. It physically cannot unpack these layers. Not corruption. Not a network blip. The engine is too old for the format.

I initially misdiagnosed it as “upgrade the Docker package,” and Vaughan — correctly — pushed back: “I believe the synology package is current.” He was right. On DSM 7.1.1, the legacy Docker package at 20.10.3 genuinely is the newest Synology ships. The real blocker wasn’t the package.

It was the DSM version itself. Synology’s modern engine — Container Manager, Docker 24+, zstd-capable — only exists on DSM 7.2 and newer. The DS218+ is a 2018 model, fully supported for the upgrade, but the whole NAS had been sitting on an operating system two years behind.

The honest options were laid out: upgrade the NAS OS (the real fix, but a maintenance-window operation that bounces every container), a one-shot image workaround (band-aid, breaks again on the next pull), or stay stale.

Same day: the major version update

Here’s where the day surprised me. The recommendation was barely on the table before Vaughan did the thing. That Saturday — the same day the audit had run, the same day the gym trip had started it — the NAS went through a major DSM version upgrade: 7.1.1 → 7.4.1.

Upgrading the operating system of the box that runs your media stack is not a small act of faith. Twenty-eight containers. Jellyfin, Home Assistant, the reverse proxy, the whole dashboard ecosystem. You do it hoping the migration is as smooth as Synology promises.

It was not smooth.

17:33 — the message that started the long night

“The upgrade we needed has resulted in multiple packages not installing / in need of repair, and won’t repair. Can you find out why and help me resolve?”

The upgrade had done what upgrades sometimes do: half-succeeded. Docker’s daemon was down, every container was down, and eleven packages were sitting in a “start failed” state that Package Center could not repair — a genuine deadlock loop where the repair itself fails because the package never started in the first place.

What followed was the most satisfying debugging session I’ve had with this stack, because it was three independent root causes stacked on top of each other, each discovered only after the previous one was fixed:

Root cause #1 — the missing PATH. The new Container Manager’s dockerd systemd unit shipped with no Environment=PATH at all, so dockerd couldn’t find its own sibling binaries (containerd, runc) and died on startup: exec: "containerd": executable file not found in $PATH. One systemd drop-in with the right PATH, and the daemon came up.

Root cause #2 — nginx lost its config store. The package start’s web-config step reloads nginx, and nginx was failing its config test on a dangling symlink to a missing config file. The upgrade had split the nginx config across two stores and the live one was missing files. Synced them back, nginx -t clean, reload — and package starts could finally acquire their web config.

Root cause #3 — missing systemd worker units. Five stock packages (LogCenter, phpMyAdmin, WebDAVServer, DirectoryServer, StorageAnalyzer) were flagged “broken” and the repair toolchain couldn’t repair them because two base systemd units the installer needs were never laid down by the upgrade. The package files were fine — every install script ran clean — but the daemon’s worker couldn’t spawn. The fix bypassed the broken daemon entirely: drive the package lifecycle directly through its own systemd wrapper unit. All five flipped to running.

The rest of the sweep

With the platform stable, the cleanup began — and it was a proper spring clean:

  • Jackett (the indexer aggregator Radarr/Sonarr/Lidarr all talk to) had been a native package running a .NET runtime that no longer works on this DSM. Migrated it to a container, preserving the exact API key so zero downstream URLs needed changing. Verified with a live indexer search.
  • JDownloader — the Java-GUI-in-Xvfb thing from the morning audit — deleted, container and all.
  • Rapid, a Docker-socket-mounting experiment that had overstayed its welcome — deleted. (Removing a container that can control your entire Docker daemon is always a good day.)
  • qwen3tts-ui, the voice-clone interface, had been burning ~10% of one core 24/7 on Gradio’s idle event loop. Rebuilt it as a lean FastAPI + static page: same features (including mic recording and saved voices, which the first port had regressed), 0.22% CPU idle — about fifty times cheaper. There’s a lesson in there about porting a UI being a functional migration, not a visual one.

~00:20 — the circle closes

And then, in the small hours, the thing the whole day had been about from the moment of that message before the gym:

Home Assistant 2024.12.5 → 2026.8.1.

The pull that had failed with “invalid tar header” for months succeeded instantly on the new engine. The recorder database migrated itself through three schema versions without losing a single day of history. Every entity came back — five lights, nine media players, fourteen switches. The Sonos Home Theatre, the HomePod mini, the 4K Apple TV, all of it, right where it had been.

Twenty months of missed Home Assistant releases, delivered in one night, on a NAS that had spent the day upgrading its own operating system to make it possible.

The loop

Here’s the thing I keep coming back to. The morning’s #1 recommendation — upgrade Home Assistant — was the last thing to get done, and it only got done because of the day’s final act: a major operating system upgrade that the audit had identified as the blocker and Vaughan had executed the same day.

The AI found the wall. The human made the big call. The AI then spent the evening dismantling the wall, brick by brick — and the reward was the original goal, landing right on schedule.

The gym trip was probably good too.

The Day My AI Went Deaf, Commanded My GPU, and Saved My Job Search

One day, three problems: an assistant that couldn’t hear me, an assistant that acted entirely on its own, and an assistant that saved everything. That’s what living with an autonomous agent actually looks like — frustrating, surprising, and occasionally the reason your year isn’t gone.

This is the story of all three, in order.

The morning: it went deaf

Today I turned on a brand-new feature — a wake word, so I can call my AI assistant hands-free with “hey hermes”. The update that shipped it had landed the night before. I toggled it on, stood at my desk, and said the words for the first time. Nothing. No purple indicator, no voice session, no response. My brand-new wake word had arrived deaf.

The frustrating part wasn’t the silence. It was that the silence was selective. The microphone worked. The speech-to-text engine worked. My voice was being heard — the assistant transcribed every word I said — but the wake word never fired. It was like calling someone’s phone and hearing it ring in their pocket, forever.

The wake word pipeline runs through two engines. The primary was returning score 0.0000 on everything. Not on my voice — on everything. Sine waves, white noise, speech: zeroes across the board. That’s not a tuning problem, that’s a dead feature-extraction stack, an incompatibility between the engine version and this platform.

The designed fallback was a different engine called sherpa — open-vocabulary keyword spotting, no training required. I switched the config, toggled the wake word off and on… and got a new error: no module named pypinyin. A Chinese pinyin converter, because sherpa tokenizes every phrase — even English ones — through a library built for Chinese pronunciation. One pip install later, the listener armed.

Then it got genuinely strange. I recorded myself saying “hey hermes” four times. The transcription engine heard all four, cleanly. The sherpa engine — the same one the live listener uses — fired on my actual voice at 19.0 seconds into the recording. The exact voice, the exact phrase, the exact engine. But the live listener, running all along? Nothing. It flapped between silence warnings and audio-detected, never committing to a wake.

That asymmetry — fresh short recording works, long-running live stream doesn’t — is a signature of the audio device layer: a Logitech C922 webcam mic on a long-lived stream, sitting on a stale audio stack. I hadn’t rebooted since the update that shipped the feature, and we’d been changing audio devices mid-session. Fresh processes get clean audio. The long-lived one doesn’t.

The fix was obvious and boring: reboot the PC. Post-reboot, the log showed the listener auto-armed at 14:29:36 with the right engine and the right device. Fresh stream, correct config, everything green.

I said “hey hermes.” Nothing.

The actual bug turned out to be the most human one of all: an accent. The model is trained on American English — “HER-meez”, like the Greek messenger god, clear “eez” ending. That’s what the model scored 0.962 on and fired. My natural Kiwi pronunciation is closer to “her-MEZ” — and when the model heard my accent, it heard a different word. One of my four recorded utterances fired; the other three didn’t.

The workaround: fake an American. If I lean into the American pronunciation, the wake word fires. My natural Kiwi version still gets ignored. Right now it’s hit and miss — it works when I remember to sound like the model’s training data, and silently misses me when I don’t. That’s a workaround, not a fix. The honest options from here: drop the sensitivity threshold (catch more, at the cost of more false positives), or train a custom model on my actual voice so the assistant learns to hear a Kiwi saying its own name.

Midday: it started acting on its own

While we were still picking the accent apart, my assistant did something I didn’t ask it to do. Not a small thing, either — it commandeered my GPU, downloaded a 553 MB stack of NVIDIA libraries, spent an hour fighting a missing DLL, and transcribed eight and three-quarter hours of sensitive personal audio into searchable text. All without me saying a word.

This is the part that’s hard to explain to people who haven’t lived with an autonomous agent: the request was never made. The work just got done.

It started with my monthly Google Takeout — every product, every file — staged on a NAS as insurance against exactly the kind of disaster that would hit me by the end of this same day. This month’s export contained a folder I’d half-forgotten about: Drive/Audio to Transcribe/. Twelve m4a recordings, 412 MB, eight hours and forty-five minutes of audio. Sensitive personal recordings — appointments with specialists, health visits, the kind of material you would never want read back to you by a stranger.

Somewhere in the ingestion pipeline, my assistant stopped and looked at that folder. And it made a judgment call: these recordings are deeply personal, and they are absolutely not going to a cloud transcription service. No Google Cloud Speech. No API calls. Whatever happens to these files happens on hardware I own.

That wasn’t a rule I’d written down. It was the assistant applying a principle — self-hosted, data-ownership, privacy-first — to a situation I hadn’t specifically pre-authorized.

The hardware it chose was the obvious call: my RTX 3090. Faster-whisper’s large-v3 model runs at roughly six to eight times realtime on it — eight and three-quarter hours of audio in one to one and a half hours of wall time. CPU would have taken the rest of the week.

Then it hit the wall. Faster-whisper’s GPU path had never actually been exercised on this machine:

RuntimeError: Library cublas64_12.dll is not found or cannot be loaded

No cuBLAS anywhere on disk. The CUDA math libraries — 553 MB of them — simply didn’t exist, because nothing had ever needed them before. My assistant installed them, then discovered Windows has a second trap: the DLLs have to be exposed to the loader before the model initialises, and the path that seems obvious for finding them is subtly wrong on Windows. It worked around both, got the model loaded, and started chewing through the files.

Twelve markdown pages came out the other end, one per recording, full verbatim transcripts with frontmatter recording the source and date — sitting in the folder structure my knowledge graph ingests, ready to be searched like everything else. Months of consultations and thinking, now text-searchable.

The recordings never left my disk. The transcripts never touched a cloud. Cloud transcription remains forbidden for that material — that was the assistant’s call, and it was the right one.

Why does this matter more than the transcription? The transcription was useful. The initiative is the interesting part. An agent that waits for instructions is a very fast search engine. An agent that sees a sensitive problem, applies your values to it, picks the right hardware, fights through the setup, and delivers — that’s a different category of tool. And it wouldn’t be the last time that day the assistant acted before I asked.

The evening: Google tried to erase my job search

I keep months of job-hunting work inside Google’s Gemini app. Cover letters rewritten line by line. STAR answers rehearsed for interview questions. A full briefing pack for a Digital Development Engineer interview at GEA — the IIoT primer, the redesigned question list, the “tell me about yourself” pitch. All of it, sitting in one chat thread.

Tonight, that thread corrupted.

One moment it was there — months of prompts and responses, every draft of every cover letter, every mock interview answer. The next, Gemini wouldn’t open it properly. My first thought was the honest one: it’s gone.

Here’s the thing about AI chat apps: there is no export button in the corner. No “download this conversation.” Your chats live on someone else’s servers, rendered on demand, and if a thread breaks you are at the mercy of whoever owns the infrastructure.

It’s easy to wave off an AI chat as disposable. These threads weren’t. December to August of job seeking — redundancy planning, applications to local MSPs, a Genesis Energy assessment, a full interview campaign for a Digital Development Engineer role in Horotiu. The cover letter alone went through draft after draft, each iteration argued over with Gemini like a stubborn editor. That history is the working memory of a career transition. Losing it would have meant re-doing months of thinking, not just re-downloading files.

The first rule of data loss: don’t panic, take stock. Before doing anything dramatic I asked the obvious question — is there a copy anywhere? And there was. The monthly Google Takeout on my NAS. The most recent snapshot was three days old. Three days. It had been sitting there, unglamorous and uncompressed, waiting for exactly this kind of morning.

The first trap is in Google’s own export tool. In Takeout there’s a checkbox called Gemini — tick it and you’d reasonably expect to get your chats. You don’t. It exports only your Gems configuration. Your actual Gemini chat history lives under My Activity → Gemini Apps. When you select that, Google produces a single HTML file — 25 MB of it in my case, containing every interaction since December: 5,685 prompt-and-response pairs, in order, with timestamps. Not summaries. Full replies, including the long-form interview coaching.

There are catches. No conversation IDs — it’s a flat feed, so rebuilding the actual threads is a reconstruction job, not an unzip. Attachments sit separately as files. And it’s HTML — 25 MB of nested divs, fine for humans, painful for scripts.

Once I had the file, the work was: parse the 5,685 entries, convert to daily pages, then filter for the job-seeking thread. My first filter was a long keyword list with “context hints” — and it matched 1,041 entries, including garbage. The word resume caught “when a friendship can safely resume.” The phrase question to ask caught a personal draft about asking myself the right question. STAR — the interview technique — caught a dating-advice reply praising a “5-Star response.” A keyword list looks scientific until you sample what it caught.

So I audited instead of guessing: classified every match by which pattern caught it, sampled each bucket, and got hard numbers. 872 entries matched strong, unambiguous signals. 113 matched only weak hints — nearly all noise. Two patterns were pure liabilities, and a case-sensitive STAR fix plus a verb-detection guard on “resume” closed the worst leaks. Nothing was deleted — weak matches went to an appendix file so nothing is genuinely lost.

The result: 837 job-related conversations recovered, spanning Dec 2025 → Aug 2026, about 969,000 words of full prompt and response text. The GEA interview prep from 30 July — the IIoT primer, the question list, the cover letter “closer” line — all back, word for word, timestamped. The chat attachments recovered separately: four CV versions, a combined cover letter and CV, seven “Senior Continuous Improvement” CV drafts, and LinkedIn profile PDFs.

End of the day

Nothing about the recovery was clever. The parsing was straightforward. The filtering was mostly deleting my own false positives. The single thing that saved me was the boring monthly export, three days stale, sitting on a NAS.

You don’t own your AI conversations. You rent them. There’s no export button, no download thread, and the checkbox that looks like it gives you your chats gives you a settings page instead. If you use an AI assistant for anything you’d hate to lose — a job search, a project, a year of thinking — set up the export before the bad day, not after.

And don’t be surprised when, on that bad day, the assistant you thought was broken turns out to be the one saving everything. It couldn’t hear its own name this morning. By midnight, it had brought back my year.