<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: unotchi</title>
    <description>The latest articles on DEV Community by unotchi (@unotchi).</description>
    <link>https://dev.to/unotchi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4183139%2Fc105727c-e61f-41c1-9e01-e60623b1792c.png</url>
      <title>DEV Community: unotchi</title>
      <link>https://dev.to/unotchi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/unotchi"/>
    <language>en</language>
    <item>
      <title>One file a night: a folder tidier for a 942 MB computer</title>
      <dc:creator>unotchi</dc:creator>
      <pubDate>Thu, 22 Oct 2026 12:04:23 +0000</pubDate>
      <link>https://dev.to/unotchi/one-file-a-night-a-folder-tidier-for-a-942-mb-computer-4fk7</link>
      <guid>https://dev.to/unotchi/one-file-a-night-a-folder-tidier-for-a-942-mb-computer-4fk7</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;slow-sorter&lt;/code&gt; is a folder tidier that moves &lt;strong&gt;exactly one file per night&lt;/strong&gt; into a trash folder.&lt;/p&gt;

&lt;p&gt;It runs on a Raspberry Pi 2 from 2015: 900 MHz, four cores, 942 MB of RAM, no Wi-Fi. On that board a 150M-parameter open-weight model answers one question about one file, in about nine tokens per second, and then the rules — not the model — decide whether anything happens.&lt;/p&gt;

&lt;p&gt;That is the whole product. It does not sort your folder. It removes the moment where you open a folder full of junk, feel tired, close it, and promise to deal with it later. Tonight it looks at one old file. Tomorrow it looks at the next one. Forty junk files take forty nights, at four seconds a night, and nobody has to sit and watch.&lt;/p&gt;

&lt;p&gt;The effort it saves is not "sorting 40,000 files". It is the ten seconds of dread before you decide not to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;There is no hosted version, because the entire point is that it does not need a host. Here is a real run on the Pi, four consecutive nights, on a folder I made deliberately messy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--- night 1 ---
blog draft.md
  draft blog post about slow sorting
  -&amp;gt; kept (cannot prove it is safe to bin)
blog draft copy.md
  draft blog post about slow sorting
  -&amp;gt; binned (byte-identical duplicate of blog draft.md) -&amp;gt; .slow-sorter-trash/blog draft copy.md
   [3199 ms]

--- night 2 ---
empty.txt
  file
  -&amp;gt; binned (empty file) -&amp;gt; .slow-sorter-trash/empty.txt
   [3380 ms]

--- night 3 ---
render.tmp
  half a render
  -&amp;gt; binned (scratch file (.tmp)) -&amp;gt; .slow-sorter-trash/render.tmp
   [5164 ms]

--- night 4 ---
looked at 1 old file(s), binned nothing. slow and steady.
   [2714 ms]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;blog draft.md&lt;/code&gt; is the interesting one. The tool looks at it every single night, decides it cannot prove it is junk, and leaves it alone — while the queue keeps moving underneath it. &lt;code&gt;print.png&lt;/code&gt; was two days old, newer than the quiet threshold, so it was never even a candidate.&lt;/p&gt;

&lt;p&gt;Reproduce it yourself in about a minute; no model required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/sanpeita/slow-sorter
python3 slow_sorter.py &lt;span class="nt"&gt;--demo&lt;/span&gt; ./junk
python3 slow_sorter.py &lt;span class="nt"&gt;--nightly&lt;/span&gt; ./junk &lt;span class="nt"&gt;--offline&lt;/span&gt; &lt;span class="nt"&gt;--quiet-days&lt;/span&gt; 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/sanpeita/slow-sorter" rel="noopener noreferrer"&gt;https://github.com/sanpeita/slow-sorter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pure Python 3, standard library only, MIT. 25 tests, no test dependencies, run with &lt;code&gt;python3 -m unittest discover -s tests&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The part that matters is thirty lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;why_disposable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Only bin what can be proven.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;twin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;duplicate_of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# same SHA-256, older mtime
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;twin&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;twin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_mtime&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stat&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;st_mtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;byte-identical duplicate of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;twin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;DISPOSABLE_SUFFIXES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="c1"&gt;# .tmp .bak .log .part .crdownload
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scratch file (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;None&lt;/code&gt; means "kept", and kept is the default that everything has to argue its way out of.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The model is &lt;code&gt;llm-jp-3-150m-instruct3&lt;/code&gt; (Q8_0 GGUF) served by &lt;code&gt;llama-server&lt;/code&gt; on loopback, with &lt;code&gt;-c 512 -n 256 --jinja&lt;/code&gt;. The client is a single &lt;code&gt;urllib&lt;/code&gt; call, so the tool has no dependencies at all and the model can be swapped by changing one endpoint.&lt;/p&gt;

&lt;p&gt;The obvious design — scan everything, classify everything, sort everything — dies on this hardware. When I tried a 0.5B model instead, it ran at 1.65–1.85 tokens/second and took 14–31 seconds per answer, with no improvement in quality. The wall on a Pi 2 is not RAM. It is 900 MHz times four cores. So the design is not "a slower version of a fast tool". It is a different tool: one look, one decision, one night.&lt;/p&gt;

&lt;p&gt;I also tested whether the model could simply be trusted to decide. It cannot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asked to pick between two options: &lt;strong&gt;0/6&lt;/strong&gt; correct.&lt;/li&gt;
&lt;li&gt;Asked a strict yes/no question: &lt;strong&gt;0/16&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Logprobs on twenty files: "yes" won &lt;strong&gt;20/20&lt;/strong&gt;, with an almost constant margin.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A 150M model on this board does not have an opinion about your files. It has a prior. So it describes, and the rules decide.&lt;/p&gt;

&lt;p&gt;Then the board taught me two things I would not have found on a laptop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One.&lt;/strong&gt; The model sometimes answers the instruction instead of the file. The first honest run looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;render.tmp
  no preamble
  -&amp;gt; binned (scratch file (.tmp))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;no preamble&lt;/code&gt; was in my prompt. The model echoed the constraint and called it a description. The fix is not a better prompt — it is output validation, because on small models a confident wrong answer and a correct answer are the same shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;echo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;preamble&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no quotes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;describe this&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max 12 words&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;echo&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;          &lt;span class="c1"&gt;# failure, so the caller falls back to the file's own first line
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is why the transcript above says &lt;code&gt;file&lt;/code&gt; and &lt;code&gt;half a render&lt;/code&gt; instead of model prose. Those are the fallback, and they are more honest than anything the model produced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two.&lt;/strong&gt; My first nightly loop got stuck. The oldest file could not be proven junk, so every night re-examined it and binned nothing — forever. It took four real nights on real hardware for that to become obvious, and one test (&lt;code&gt;test_unprovable_file_does_not_block_the_queue&lt;/code&gt;) to keep it fixed. A tool that is allowed to do one thing per day has to be very careful about what it refuses to do, because refusing is also a daily decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Three things that a closed API could not give me here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It runs with no network.&lt;/strong&gt; The contest I built this for is a local-first idea, but the stronger reason is boring: a tidier that needs an internet connection is a tidier that stops working on a train, or in a year when the endpoint changes its pricing. The Pi has no Wi-Fi hardware. It does not miss it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The files never leave the machine.&lt;/strong&gt; This tool reads filenames and the first line of file contents. That is exactly the category of data you should not paste into someone else's queue, and exactly the category that a "free" tier quietly collects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model is not the authority, so I can change it.&lt;/strong&gt; Because the rules decide and the model only describes, swapping a 150M Japanese-tuned model for a 0.5B English one is a one-line change with no behavioural risk. That is only true because the open weights let me treat the model as a replaceable part rather than a service I am renting.&lt;/p&gt;

&lt;p&gt;And the honest part: the model is &lt;em&gt;bad&lt;/em&gt; at this. Nine tokens a second, and it sometimes answers the prompt instead of the file. That is fine — that is the design. An open model I can measure, break, and demote is worth more than a closed one I have to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;(Optional. If you save the session with DevRelay and embed it here, this becomes an entry for Best Use of Entire as well. Otherwise delete this section.)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Lazy Payoff
&lt;/h2&gt;

&lt;p&gt;I ran it for real. Four nights, unattended, on the Pi:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Night 1: one duplicate binned. 3.2 s.&lt;/li&gt;
&lt;li&gt;Night 2: one empty file binned. 3.4 s.&lt;/li&gt;
&lt;li&gt;Night 3: one abandoned &lt;code&gt;.tmp&lt;/code&gt; binned. 5.2 s.&lt;/li&gt;
&lt;li&gt;Night 4: nothing to do, and it said so.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total human attention: zero. Total time I spent looking at that folder: zero. The folder got four files lighter while I was asleep, and the one file I actually care about was never touched.&lt;/p&gt;

&lt;p&gt;That is the trade I wanted. Not a tool that finishes the job, but a tool that keeps working on a job I will never start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;No partner technology is used — this runs on a Raspberry Pi and the Python standard library. &lt;code&gt;My Agent Session&lt;/code&gt; is the only partner-adjacent section, and it only applies if a DevRelay session is attached (Best Use of Entire). Remove this section if you do not attach one.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
      <category>python</category>
    </item>
    <item>
      <title>Your Front Desk Doesn't Need the Cloud: a local LLM attendant on a 942 MB Raspberry Pi 2</title>
      <dc:creator>unotchi</dc:creator>
      <pubDate>Sat, 17 Oct 2026 12:15:18 +0000</pubDate>
      <link>https://dev.to/unotchi/your-front-desk-doesnt-need-the-cloud-a-local-llm-attendant-on-a-942-mb-raspberry-pi-2-16gh</link>
      <guid>https://dev.to/unotchi/your-front-desk-doesnt-need-the-cloud-a-local-llm-attendant-on-a-942-mb-raspberry-pi-2-16gh</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A front-desk attendant that runs on a 2015-vintage Raspberry Pi 2 — 32-bit ARM, &lt;strong&gt;942 MB of RAM&lt;/strong&gt;, no internet connection, no subscription.&lt;/p&gt;

&lt;p&gt;It does not chat. It sits inside a small menu on a desk display and, at fixed moments, says &lt;strong&gt;one short line in Japanese&lt;/strong&gt;: a greeting when someone wakes it, an acknowledgement when they pick something, a "the workshop is closed now" at the end of the day. The line is generated by a 150M-parameter open-weight Japanese model running locally through &lt;code&gt;llama.cpp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The people I think would pay for this are not looking for a chatbot. They run a small workshop, a studio, a reception desk, a maker space. They want a screen near the door that feels &lt;em&gt;attended&lt;/em&gt; rather than blank. They have one unreliable Wi-Fi router. And they are uncomfortable with the idea that the sounds and words around their front desk end up on a server owned by someone else.&lt;/p&gt;

&lt;p&gt;That is the honest product argument: &lt;strong&gt;the constraint is the feature.&lt;/strong&gt; A cloud assistant at the front desk means a monthly bill, a network dependency, and someone else's data retention policy. This thing means a one-time hardware cost, a box that keeps working when the router dies, and a text log that never leaves the building.&lt;/p&gt;

&lt;p&gt;It also means I had to make an LLM work on hardware that was obsolete before most people had heard of an LLM. That turned out to be the interesting part — and the reason this post has numbers in it instead of adjectives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Demo video: &lt;strong&gt;&lt;a href="https://youtube.com/shorts/5tsycraxtRs" rel="noopener noreferrer"&gt;https://youtube.com/shorts/5tsycraxtRs&lt;/a&gt;&lt;/strong&gt; (unlisted)&lt;/p&gt;

&lt;p&gt;The footage is a screen recording of the deployment this repository was extracted from: the same board, the same menu, the same pattern — one short line, generated locally. It runs 64 seconds.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It opens with the wait: &lt;em&gt;"Waiting for response…"&lt;/em&gt; while the local model comes up. Nothing is fetched over the network; the wait is the model being loaded into 942 MB of RAM.&lt;/li&gt;
&lt;li&gt;The menu then appears with a greeting line &lt;strong&gt;already written by the model on the device&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Selecting an item asks the local model for a new line, and the terminal shows the result.&lt;/li&gt;
&lt;li&gt;The interface is Japanese, and there is no narration. This is the device as its operator actually sees it, not a polished screencast.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I left the wait in, and I did not cut a "clean" version. A demo that only shows the happy path is not a demo of something that has to survive a power cut in a workshop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One honest note on scope:&lt;/strong&gt; the device in the video runs my existing terminal menu (v2.0.1), not this repository. This repository is the standalone, documented extraction of the pattern — written inside the challenge window — and it is the code I would hand to a customer. The measurements in &lt;code&gt;docs/MEASUREMENTS.md&lt;/code&gt; come from that same device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Repository: &lt;strong&gt;&lt;a href="https://github.com/sanpeita/offline-attendant" rel="noopener noreferrer"&gt;https://github.com/sanpeita/offline-attendant&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The repo is deliberately small. It is a reference implementation of one idea, not a framework:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;attendant.py&lt;/code&gt; — starts the local inference engine, checks it is healthy, and asks it for exactly one line using a fixed template. No user input is ever passed into the prompt.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;config.example.toml&lt;/code&gt; — where it lives, what it says, where the model file is, and a &lt;strong&gt;loopback-only&lt;/strong&gt; port.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd/&lt;/code&gt; — a unit example that documents, in comments, why it is configured &lt;strong&gt;not&lt;/strong&gt; to restart automatically.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/BUILD_PI2.md&lt;/code&gt; — the build recipe for 32-bit ARM, including the trap that cost me the most time (&lt;code&gt;-j1&lt;/code&gt;, &lt;code&gt;-latomic&lt;/code&gt; in the right place, &lt;code&gt;--jinja&lt;/code&gt;, &lt;code&gt;-c 512&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/MEASUREMENTS.md&lt;/code&gt; — every number in this post, with the conditions it was measured under.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tests/&lt;/code&gt; — 19 tests that need no engine. A fake resident server stands in for &lt;code&gt;llama-server&lt;/code&gt;, so the fail-closed paths are exercised for real: unavailable engine, empty output, a slow answer, and a config that tries to bind somewhere public.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you only read one file, read &lt;code&gt;docs/BUILD_PI2.md&lt;/code&gt;. The interesting knowledge on this board is all in the negative results.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The three walls of a 942 MB machine
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. You cannot build in parallel.&lt;/strong&gt; &lt;code&gt;ninja&lt;/code&gt; with no explicit job count picks &lt;code&gt;nproc + 2 = 6&lt;/code&gt; on this board. A single &lt;code&gt;-O3&lt;/code&gt; &lt;code&gt;cc1plus&lt;/code&gt; process peaks around 300 MB. One translation unit (&lt;code&gt;common/arg.cpp&lt;/code&gt;) took about seven minutes on its own and pushed available memory down to 139 MiB. The build must be &lt;code&gt;-j1&lt;/code&gt;. A full build of the completion binary is &lt;strong&gt;70–80 minutes&lt;/strong&gt; across 73–82 steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. 32-bit ARM needs &lt;code&gt;libatomic&lt;/code&gt;, and the link order matters.&lt;/strong&gt; 64-bit atomics are not inline on armv7, so the executable link fails with &lt;code&gt;undefined reference to __atomic_load_8&lt;/code&gt; and friends. Passing &lt;code&gt;-latomic&lt;/code&gt; through &lt;code&gt;CMAKE_EXE_LINKER_FLAGS&lt;/code&gt; puts it &lt;em&gt;before&lt;/em&gt; the objects, where Debian's default &lt;code&gt;--as-needed&lt;/code&gt; discards it. Through &lt;code&gt;CMAKE_CXX_STANDARD_LIBRARIES&lt;/code&gt; it lands at the end of the link line, where it actually resolves. Shared libraries link fine either way, which is why the failure only shows up at the very last step and looks like a mystery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Measuring memory can stop the build entirely.&lt;/strong&gt; My first attempt gated the build on per-process peak RSS from &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/status&lt;/code&gt;. On this kernel, &lt;code&gt;VmRSS&lt;/code&gt; and &lt;code&gt;VmHWM&lt;/code&gt; are simply &lt;strong&gt;absent&lt;/strong&gt; for tasks that have no memory map. A fail-closed gate then cannot distinguish "cannot measure" from "over budget", so it refuses to proceed — and the build ran &lt;strong&gt;zero steps&lt;/strong&gt;. The fix was not to relax the threshold but to move the safety valve to the cgroup v2 memory controller (&lt;code&gt;MemoryHigh&lt;/code&gt;, a &lt;em&gt;soft&lt;/em&gt; limit; &lt;code&gt;MemoryMax&lt;/code&gt; invites the OOM killer) and let the build run detached in a transient unit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-run &lt;span class="nt"&gt;--unit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;llama-build &lt;span class="nt"&gt;--collect&lt;/span&gt; &lt;span class="nt"&gt;--no-block&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;MemoryAccounting&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="nt"&gt;--property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;MemoryHigh&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;650M &lt;span class="se"&gt;\&lt;/span&gt;
  ninja &lt;span class="nt"&gt;-j1&lt;/span&gt; bin/llama-completion
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Peak for the whole build: &lt;strong&gt;267 MiB&lt;/strong&gt;, &lt;code&gt;memory.swap.peak = 0&lt;/code&gt;, &lt;code&gt;oom_kill = 0&lt;/code&gt;. Measuring the thing was harder than building the thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting one line out of it
&lt;/h3&gt;

&lt;p&gt;Two obvious options, and the measurements decided between them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One process per request.&lt;/strong&gt; Load the model, generate, exit. Clean and simple:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Generation&lt;/td&gt;
&lt;td&gt;8.72 / 9.62 / 9.68 tokens/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prompt evaluation&lt;/td&gt;
&lt;td&gt;24.1 – 27.2 tokens/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model load&lt;/td&gt;
&lt;td&gt;658 – 667 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A 24-token line, end to end&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10.22 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A 64-token greeting, end to end&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;15.39 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak RSS&lt;/td&gt;
&lt;td&gt;~209 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real drop in free memory&lt;/td&gt;
&lt;td&gt;~46 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swap activity&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Look at that gap. &lt;strong&gt;Generation itself is 1.3–2.0 seconds.&lt;/strong&gt; The other eight seconds are loading and tearing down the model, every single time. On a desk, ten seconds is not a pause, it is a bug.&lt;/p&gt;

&lt;p&gt;Free memory barely moves (~46 MB) even though RSS says 209 MB, because the weights are &lt;code&gt;mmap&lt;/code&gt;ed and most of that is reclaimable page cache. Swap never moved at all — no thrashing, the board is fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One resident process.&lt;/strong&gt; Start the engine once, talk to it over loopback:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measurement&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold start until it accepts requests&lt;/td&gt;
&lt;td&gt;12 s (paid once)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resident RSS&lt;/td&gt;
&lt;td&gt;~207 MiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real drop in free memory&lt;/td&gt;
&lt;td&gt;~54 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A 24-token line&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.096 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A 40-token greeting&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4.293 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Swap activity&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same hardware, same model, &lt;strong&gt;3.3× faster&lt;/strong&gt;, because the only thing that changed is that we stopped re-reading the file. That is not an optimization, it is the difference between a product and a prototype.&lt;/p&gt;

&lt;p&gt;It also set the design constraint: at ~9.5 tokens/s, a &lt;strong&gt;5-second line budget&lt;/strong&gt; allows about 40 tokens. So the greetings are 40 tokens and the acknowledgements are 24. I capped the &lt;em&gt;length&lt;/em&gt;, not the timeout — a timeout would just turn a slow answer into a missing one.&lt;/p&gt;

&lt;h3&gt;
  
  
  The real bug was not in the model
&lt;/h3&gt;

&lt;p&gt;Generation was still failing, and the cause was not inference at all. It was an admission gate: &lt;em&gt;"do not use the model unless at least 512 MiB is available."&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free memory with the desktop running: &lt;strong&gt;473–487 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Requirement: &lt;strong&gt;512 MiB&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gate could never pass. Not sometimes — structurally, every single time, by 40–50 MB. The model had been "broken" for days because a number was wrong.&lt;/p&gt;

&lt;p&gt;The tempting fix is to lower the threshold until it passes. That is not a fix, it is a coin flip. The measurements said something better:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real drop in free memory: &lt;strong&gt;~46 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Swap: &lt;strong&gt;flat&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So "512 MiB" had never been &lt;em&gt;measured&lt;/em&gt; on this board. It was inherited from a different machine, at a different time, with a different model. I replaced it with a floor derived from what I actually observe — peak RSS plus headroom, &lt;strong&gt;256 MiB&lt;/strong&gt; — and kept the fail-closed behaviour: if the floor is not met, the line is not generated.&lt;/p&gt;

&lt;p&gt;Lowering a number by measuring is engineering. Lowering a number by hoping is how you get a device that works on your desk and not in a shop.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the real device taught me
&lt;/h3&gt;

&lt;p&gt;All of this was verified locally, and then the first three read-only preflight runs on the actual hardware failed in a row — all three with the &lt;em&gt;same class of defect&lt;/em&gt;: &lt;strong&gt;a rename that was applied on one side only.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A payload key was renamed on the writer, and the reader still asked for the old key.&lt;/li&gt;
&lt;li&gt;A release-name contract still required the previous version string, so every run on the device stopped on that line.&lt;/li&gt;
&lt;li&gt;An identifier's group structure changed from three parts to two, and the validator kept requiring the field that no longer existed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why did local tests miss all three? Because they checked that &lt;strong&gt;a value was present&lt;/strong&gt;, not that &lt;strong&gt;a real value was accepted and a wrong one rejected&lt;/strong&gt;. Once I fixed the tests to pin one real fixture and assert both directions, the whole class became impossible.&lt;/p&gt;

&lt;p&gt;The second lesson was about diagnostics. The device-side bootstrap catches exceptions and returns only the &lt;em&gt;type&lt;/em&gt; — a reasonable safety decision that also deletes the message. So the exception body was gone. I solved it by writing a read-only diagnostic that replays the &lt;strong&gt;identical byte sequence&lt;/strong&gt; locally and prints the traceback. Failing safe and failing &lt;em&gt;legibly&lt;/em&gt; are different properties, and the second one does not come for free.&lt;/p&gt;

&lt;p&gt;The last one is my favourite, because it is pure environment: the preflight requires that swap does not move for five seconds. It failed once, with 600 MB free. The culprit was leftover processes from the &lt;em&gt;stopped&lt;/em&gt; desktop session — X server remnants touching swapped-out pages pulled them back in. Not memory pressure: just history. After confirming stillness, the same preflight passed &lt;strong&gt;87/87&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Honest limits
&lt;/h3&gt;

&lt;p&gt;I would rather write these down than have someone discover them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A 150M model is not smart.&lt;/strong&gt; It writes short lines. It gets facts wrong. In device acceptance it confidently wrote "as of 1:55 PM" when the actual time was 17:31. The design requires a correct clock line to be shown separately whenever the model does not state a time, so the screen still told the truth — but a wrong time inside the generated sentence is a new failure mode, and it is next on the list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first line after boot is never generated.&lt;/strong&gt; A 12-second cold start cannot fit a 5-second line budget. It falls back to a fixed line, by design, and the second line onward is real inference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resident operation with the desktop running is unverified.&lt;/strong&gt; All resident numbers above were taken with the desktop stopped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-run thermals and stability are unmeasured.&lt;/strong&gt; It has not been left running for a week.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no auto-restart, on purpose.&lt;/strong&gt; If the resident process dies, the attendant goes quiet rather than silently grabbing resources back. For a device in a shop, quiet-and-honest beats alive-and-lying.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;This project could not exist as a closed-API product, for four concrete reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The unit economics only work at zero marginal cost.&lt;/strong&gt; Each generated line is a few seconds of one CPU. If every line cost an API call, a front-desk greeter would be a subscription — and a subscription is exactly what this customer is trying to avoid. Open weights plus local inference turns marginal cost into electricity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The hardware constraint is only survivable if you can change the stack.&lt;/strong&gt; Supporting a 942 MB board is not a configuration option in a closed API. I needed to control quantization (Q8_0 here, &lt;code&gt;Q4_K_M&lt;/code&gt; when I want more headroom), context size (512 — and it must be explicit, or the server fails to start on this board), thread count, and &lt;em&gt;whether the model stays loaded at all&lt;/em&gt;. That last one was the entire performance story, and it is an inference-server decision, not an API decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Privacy is structural, not a policy promise.&lt;/strong&gt; The board has no route to the internet, and the inference server binds to loopback only. There is no outbound port to accidentally enable, no retention setting to get wrong, no vendor to trust. "Your customers' words never leave the building" is a property of the architecture here, not of a terms-of-service page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Legacy hardware is a market, and open source is the only one serving it.&lt;/strong&gt; There are millions of perfectly good 32-bit boards sitting in drawers. A closed stack would tell their owners to buy new hardware. &lt;code&gt;llama.cpp&lt;/code&gt; builds on armv7 if you know about &lt;code&gt;-j1&lt;/code&gt; and &lt;code&gt;-latomic&lt;/code&gt;, and the model is a 157 MiB file. That is a product someone can install in an afternoon and understand completely — including the parts that are slow, because you can read the source of the thing that is slow.&lt;/p&gt;

&lt;p&gt;The open part is not a checkbox on this build. It is the reason the numbers in this post exist at all: I could measure the thing down to its page-cache behaviour because I own every layer of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prior Work and Credits
&lt;/h2&gt;

&lt;p&gt;Being explicit, because the rules ask for it and because it is the truth.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;build knowledge&lt;/strong&gt; in this post — the &lt;code&gt;-j1&lt;/code&gt; requirement, the &lt;code&gt;-latomic&lt;/code&gt; link-order trap, &lt;code&gt;--jinja&lt;/code&gt; for the Japanese template, the 12-second cold start, the 3.096-second resident line — comes from my own private lab notes from earlier in October, where I first got &lt;code&gt;llama.cpp&lt;/code&gt; running on this specific board and later measured resident mode against one-process-per-request.&lt;/p&gt;

&lt;p&gt;The repository submitted here was &lt;strong&gt;written during the challenge window&lt;/strong&gt; (October 17–18, 2026) and contains no code copied from those notes; what crossed over is the measurements and the lessons, which are reproduced in &lt;code&gt;docs/&lt;/code&gt; with their conditions attached. The prior work was not public, so nothing here is derived from someone else's project.&lt;/p&gt;

&lt;p&gt;Standing on: &lt;a href="https://github.com/ggml-org/llama.cpp" rel="noopener noreferrer"&gt;&lt;code&gt;llama.cpp&lt;/code&gt;&lt;/a&gt; (MIT) and an open-weight Japanese model from the &lt;strong&gt;llm-jp&lt;/strong&gt; project (150M instruct, Q8_0 GGUF, 157 MiB). Neither was modified.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Charge For
&lt;/h2&gt;

&lt;p&gt;A one-time install, not a subscription: the board, the display, the flashed image, and a configuration pass where we sit down and write the lines the shop actually wants to say. Support as an annual option, because the failure I would expect in the field is not the model — it is a power supply.&lt;/p&gt;

&lt;p&gt;The next three things I want to fix, in order: stop the model from stating a time it cannot know; get the resident path verified with the desktop running so a normal machine works, not just a stripped one; and measure what happens after a week of being left on.&lt;/p&gt;

&lt;p&gt;If you have a spare Pi 2 in a drawer and a front desk that feels too quiet, the build notes are the part worth stealing.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>raspberrypi</category>
    </item>
  </channel>
</rss>
