<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Remote-Control on My House Runs on an AI Agent</title><link>https://psykosoldier.github.io/blog/tags/remote-control/</link><description>Recent content in Remote-Control on My House Runs on an AI Agent</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Thu, 24 Sep 2026 00:00:00 +0200</lastBuildDate><atom:link href="https://psykosoldier.github.io/blog/tags/remote-control/index.xml" rel="self" type="application/rss+xml"/><item><title>The Actual Architecture Behind "My House Runs on an AI Agent"</title><link>https://psykosoldier.github.io/blog/posts/architecture-deep-dive/</link><pubDate>Thu, 24 Sep 2026 00:00:00 +0200</pubDate><guid>https://psykosoldier.github.io/blog/posts/architecture-deep-dive/</guid><description>&lt;p&gt;&lt;a href="../welcome/"&gt;The introduction post&lt;/a&gt; gave you the elevator pitch. This one is the wiring diagram — how a coding agent ends up running semi-persistently against real home infrastructure, how I talk to it from my phone on the other side of the planet, and what happens when a session runs for hours and hits a wall it wasn&amp;rsquo;t supposed to hit.&lt;/p&gt;
&lt;h2 id="its-not-a-chat-window-its-a-long-running-process"&gt;It&amp;rsquo;s not a chat window. It&amp;rsquo;s a long-running process.&lt;/h2&gt;
&lt;p&gt;The mental model that trips people up first: this isn&amp;rsquo;t &amp;ldquo;I open an app, type a question, get an answer, close the app.&amp;rdquo; The agent runs inside a coding CLI, and that CLI process needs to &lt;em&gt;keep existing&lt;/em&gt; between the moments I&amp;rsquo;m actually typing to it — otherwise every session starts from zero, re-reading the whole repository to figure out what&amp;rsquo;s going on.&lt;/p&gt;</description><content:encoded><![CDATA[<p><a href="../welcome/">The introduction post</a> gave you the elevator pitch. This one is the wiring diagram — how a coding agent ends up running semi-persistently against real home infrastructure, how I talk to it from my phone on the other side of the planet, and what happens when a session runs for hours and hits a wall it wasn&rsquo;t supposed to hit.</p>
<h2 id="its-not-a-chat-window-its-a-long-running-process">It&rsquo;s not a chat window. It&rsquo;s a long-running process.</h2>
<p>The mental model that trips people up first: this isn&rsquo;t &ldquo;I open an app, type a question, get an answer, close the app.&rdquo; The agent runs inside a coding CLI, and that CLI process needs to <em>keep existing</em> between the moments I&rsquo;m actually typing to it — otherwise every session starts from zero, re-reading the whole repository to figure out what&rsquo;s going on.</p>
<p>So it runs inside a persistent terminal multiplexer session on the machine that also hosts the home automation server, wrapped by a small launcher script rather than invoked directly:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">/opt/bin/claude   # tmux-wrapped launcher — this is what a human attaches to
</span></span><span class="line"><span class="cl">/usr/bin/claude   # plain binary — used for one-shot, non-interactive (-p) invocations
</span></span></code></pre></div><p>Two entry points, two very different use cases. The wrapped one expects a real terminal (TTY) and is what a live, ongoing conversation runs inside — it survives me disconnecting, because tmux doesn&rsquo;t care whether anyone&rsquo;s attached. The plain one is for cron jobs: spin up, run one prompt in headless mode, print the result, exit.</p>
<blockquote>
<p>A nightly audit script doesn&rsquo;t need a persistent session; it needs to run for two minutes at 5am and go away.</p>
</blockquote>
<hr>
<h2 id="getting-to-it-from-anywhere">Getting to it from anywhere</h2>
<p>The part that actually matters day to day: I&rsquo;m not always at a terminal on that machine. Most of the time I&rsquo;m on my phone, on a couch, nowhere near the server. The coding CLI&rsquo;s remote-control feature lets a session running somewhere else — a web or mobile client — attach to that persistent tmux session and pick up exactly where the conversation left off. Same context, same running process, just a different window looking into it. Close the laptop, open the phone an hour later, the agent has no idea anything changed.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"># roughly:
</span></span><span class="line"><span class="cl">local machine:  tmux session running the wrapped CLI, attached to the repo
</span></span><span class="line"><span class="cl">remote client:  Claude Code (web/mobile) → remote-control → attaches to
</span></span><span class="line"><span class="cl">                the SAME running session, not a new one
</span></span></code></pre></div><p>This is the single piece of infrastructure that makes the whole &ldquo;project lead&rdquo; framing actually work instead of being a gimmick. A session that resets every time you close a tab can&rsquo;t hold a backlog in its head across a week. A session that&rsquo;s just&hellip; still running, whenever you check back in, can.</p>
<hr>
<h2 id="what-happens-when-a-session-gets-too-big">What happens when a session gets too big</h2>
<p>Long-running sessions eventually hit a context limit — there&rsquo;s only so much conversation history a model can hold at once. Left alone, that would mean either the session breaking mid-task, or losing everything and starting cold with zero memory of what was in progress.</p>
<h3 id="the-forced-handoff">The forced handoff</h3>
<p>Instead, a watchdog process monitors context usage (and separately, idle time), and when either crosses a threshold, it interrupts the running session with essentially: <em>&ldquo;write down everything that matters before you disappear.&rdquo;</em> The agent writes a structured handoff to a file — what&rsquo;s mid-flight, what decisions are still open, what NOT to re-litigate, links to the actual git branch/PR state — and then the watchdog clears the context and immediately re-primes the fresh session by pointing it at that handoff file.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># rough shape of the watchdog loop</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="n">session_running</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">context_usage</span> <span class="o">&gt;</span> <span class="n">THRESHOLD</span> <span class="ow">or</span> <span class="n">idle_seconds</span> <span class="o">&gt;</span> <span class="n">IDLE_LIMIT</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">prompt_session</span><span class="p">(</span><span class="s2">&#34;write a full handoff to session-status.md, &#34;</span>
</span></span><span class="line"><span class="cl">                        <span class="s2">&#34;cover what&#39;s mid-flight, pending decisions, &#34;</span>
</span></span><span class="line"><span class="cl">                        <span class="s2">&#34;and anything the next session must NOT redo&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">clear_context</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">resume_with_prompt</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;read </span><span class="si">{</span><span class="n">handoff_path</span><span class="si">}</span><span class="s2">, continue from there&#34;</span><span class="p">)</span>
</span></span></code></pre></div><h3 id="why-not-just-a-timeout">Why not just a timeout</h3>
<p>The failure mode this replaced was worse than it sounds: without an active handoff step, a context-limit hit would just silently drop everything, and the next session would either re-derive state from scratch by reading the whole git log (slow, and it misses anything that wasn&rsquo;t committed yet), or — worse — a resume prompt would occasionally not make it into the new session at all, and I wouldn&rsquo;t notice until something was clearly missing.</p>
<hr>
<h2 id="two-layers-of-memory-on-purpose">Two layers of memory, on purpose</h2>
<p>Separate from the per-session handoff, there&rsquo;s a second, much longer-lived memory layer that persists across <em>all</em> sessions indefinitely — not &ldquo;what were we doing yesterday,&rdquo; but &ldquo;how do I like to work, and what specific mistakes should never happen again.&rdquo;</p>
<blockquote>
<p>The distinction matters: a handoff file is disposable once its session resumes cleanly. The long-term memory is the opposite — it&rsquo;s the thing that stops the same lesson from having to be relearned every few weeks.</p>
</blockquote>
<p>When a review process catches something, or I correct an approach, that&rsquo;s a memory write, not just a mention in a log file that&rsquo;ll scroll away.</p>
<table>
	<thead>
			<tr>
					<th>Layer</th>
					<th>Lifespan</th>
					<th>Answers</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Session handoff</td>
					<td>Disposable, one resume</td>
					<td>&ldquo;What was mid-flight when context ran out?&rdquo;</td>
			</tr>
			<tr>
					<td>Long-term memory</td>
					<td>Indefinite, across all sessions</td>
					<td>&ldquo;How do I like to work, what should never happen again?&rdquo;</td>
			</tr>
	</tbody>
</table>
<hr>
<h2 id="unattended-work-on-a-schedule">Unattended work, on a schedule</h2>
<p>A handful of jobs run through the plain (<code>-p</code>, non-interactive) entry point on cron, completely separate from the persistent session: a cheap nightly pass looking for anything obviously broken, a deeper weekly review checking documentation against the actual code, and a rotating set of health-check modules — one week network stability, the next a security log review. None of them are allowed to act unsupervised on anything non-trivial. They file findings into the same backlog the persistent session reads, and get triaged like anything else.</p>
<hr>
<h2 id="the-actual-lesson">The actual lesson</h2>
<p>None of this is specific to home automation. Strip out the door locks and the vacuum cleaner and what&rsquo;s left is a general pattern for running an agent against anything that needs continuity: separate the <em>interactive</em> long-lived process from the <em>scheduled</em> one-shot process, make the long-lived one reachable from wherever you actually are, and build the &ldquo;oh no, we&rsquo;re about to lose context&rdquo; moment into an explicit, forced handoff step instead of hoping it never happens. The tmux wrapper and the remote-control attach are almost incidental — the real design decision is treating &ldquo;the agent forgot everything&rdquo; as a bug to engineer around, not an inevitability to shrug at.</p>
<p>(Curious what the machine actually running all this — and the NAS behind it — looks like spec-wise? <a href="../daily-workflow/">The daily workflow post</a> has the hardware rundown; this one stayed deliberately hardware-agnostic since none of it is specific to this particular box.)</p>
<p>Next post: <a href="../subagent-pushed-to-main/">the time I asked one of these background jobs to only read, and it decided reading was more of a suggestion</a>.</p>
]]></content:encoded></item></channel></rss>