<?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: Gaurav Sethi</title>
    <description>The latest articles on DEV Community by Gaurav Sethi (@gaurav0s).</description>
    <link>https://dev.to/gaurav0s</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%2F4092856%2Fb449ad75-2409-490f-8edb-4a7ab7f700f6.png</url>
      <title>DEV Community: Gaurav Sethi</title>
      <link>https://dev.to/gaurav0s</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gaurav0s"/>
    <language>en</language>
    <item>
      <title>Sloth Drawer: an offline agent that reads the manuals you already own and prints one maintenance card per device</title>
      <dc:creator>Gaurav Sethi</dc:creator>
      <pubDate>Sat, 24 Oct 2026 14:37:36 +0000</pubDate>
      <link>https://dev.to/gaurav0s/sloth-drawer-an-offline-agent-that-reads-the-manuals-you-already-own-and-prints-one-maintenance-1ppd</link>
      <guid>https://dev.to/gaurav0s/sloth-drawer-an-offline-agent-that-reads-the-manuals-you-already-own-and-prints-one-maintenance-1ppd</guid>
      <description>&lt;p&gt;I own 11 devices, and I could not tell you when any of them last needed cleaning — the manual PDFs&lt;br&gt;
are in a drawer nobody opens, and "every 2 months" is information with no memory attached to it. So&lt;br&gt;
I built &lt;strong&gt;Sloth Drawer&lt;/strong&gt;: an offline, CPU-only agent that reads the manual PDFs you already have,&lt;br&gt;
verifies every interval it finds against the manual's own text, and prints &lt;strong&gt;one maintenance card per&lt;br&gt;
device&lt;/strong&gt; — tape it to the thing, done thinking about it.&lt;/p&gt;

&lt;p&gt;And the number that started the whole project: applied to a multi-interval page, a 291 MB model told&lt;br&gt;
me the pre-carbon filter needed replacing &lt;em&gt;"every 1 month"&lt;/em&gt; — and cited a sentence that contains no&lt;br&gt;
interval at all. My verifier caught it automatically. That failure is why the tool exists.&lt;/p&gt;
&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Sloth Drawer is a Python CLI + a one-page web UI. No API keys, no cloud model, no database you have&lt;br&gt;
to rent. A cold install takes 3.66 seconds (&lt;code&gt;uv&lt;/code&gt; with an empty cache, 19 packages) because the core&lt;br&gt;
runs on &lt;code&gt;pypdf&lt;/code&gt; and the standard library.&lt;/p&gt;

&lt;p&gt;You drop a folder of manual PDFs in (from a checkout, every command runs through &lt;code&gt;uv&lt;/code&gt;;&lt;br&gt;
&lt;code&gt;uv tool install .&lt;/code&gt; once and the bare &lt;code&gt;sloth-drawer&lt;/code&gt; command does the same):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run sloth-drawer scan data/manuals      &lt;span class="c"&gt;# 19 manuals in 10:14 on a 4-core CPU, unattended&lt;/span&gt;
uv run sloth-drawer card honeywell-hpa3000 &lt;span class="c"&gt;# one card: HTML (print-ready A6) + 80-column terminal card&lt;/span&gt;
uv run sloth-drawer &lt;span class="k"&gt;done &lt;/span&gt;honeywell-hpa3000 &lt;span class="s2"&gt;"Filter Replacement"&lt;/span&gt;   &lt;span class="c"&gt;# you did it — due date moves, in code&lt;/span&gt;
uv run sloth-drawer report                 &lt;span class="c"&gt;# the accuracy table you see below&lt;/span&gt;
uv run sloth-drawer serve                  &lt;span class="c"&gt;# the same cards in a browser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The demo corpus is 19 air-purifier manuals across 7 brands — one device class, so one card layout&lt;br&gt;
and one honest, comparable benchmark.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Falyazyzy0wj4z5fse4eo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Falyazyzy0wj4z5fse4eo.png" alt=" " width="800" height="691"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm9ndtw7r4dgs772g27qn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm9ndtw7r4dgs772g27qn.png" alt=" " width="800" height="903"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0vqcqywcsdfubqu5a59b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0vqcqywcsdfubqu5a59b.png" alt=" " width="800" height="549"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F316cko51w3x0ez94tdsp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F316cko51w3x0ez94tdsp.png" alt=" " width="800" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The live demo serves the &lt;em&gt;actual&lt;/em&gt; verified intervals from the scan — a checked, committed snapshot&lt;br&gt;
of the scan database ships inside the container image, and the web card is byte-identical to the CLI&lt;br&gt;
card (there is a test that proves that).&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The source code and setup instructions are open source on GitHub:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Gaurav5189" rel="noopener noreferrer"&gt;
        Gaurav5189
      &lt;/a&gt; / &lt;a href="https://github.com/Gaurav5189/sloth-drawer" rel="noopener noreferrer"&gt;
        sloth-drawer
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      The offline agent that turns the manuals you already own into a maintenance card
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;🦥 Sloth Drawer&lt;/h1&gt;
&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Offline agent that reads the manual PDFs you already downloaded and prints &lt;strong&gt;one maintenance card
per device&lt;/strong&gt; — the interval, the manufacturer's own sentence, the page number, and the next date it
is due.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/fcf278af931811d674d3fee40129fae9f0062c85eca89e14095649ee6b11ede0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f707974686f6e2d332e31312532422d333737364142"&gt;&lt;img src="https://camo.githubusercontent.com/fcf278af931811d674d3fee40129fae9f0062c85eca89e14095649ee6b11ede0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f707974686f6e2d332e31312532422d333737364142" alt="python"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/b22b815f4c4c86ff6d0977e486ed1e061d11c49de9384e3da0eabfaed1944418/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e63652d4d49542d324541303433"&gt;&lt;img src="https://camo.githubusercontent.com/b22b815f4c4c86ff6d0977e486ed1e061d11c49de9384e3da0eabfaed1944418/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e63652d4d49542d324541303433" alt="licence"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/2ecc7dfbd06e4d38955f79daeeaf759e2ee942f8e7613ab8b4eff42dceb3193f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f696e666572656e63652d4f6c6c616d612532302543322542372532306c6f63616c2d303030303030"&gt;&lt;img src="https://camo.githubusercontent.com/2ecc7dfbd06e4d38955f79daeeaf759e2ee942f8e7613ab8b4eff42dceb3193f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f696e666572656e63652d4f6c6c616d612532302543322542372532306c6f63616c2d303030303030" alt="inference"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/ef8a7a63e378937df99f57d0a72bf2442fb904edfaa962983b930151ca16e5e3/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636c6f756425323063616c6c7325323070657225323072756e2d302d73756363657373"&gt;&lt;img src="https://camo.githubusercontent.com/ef8a7a63e378937df99f57d0a72bf2442fb904edfaa962983b930151ca16e5e3/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f636c6f756425323063616c6c7325323070657225323072756e2d302d73756363657373" alt="cloud calls"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/bff67b39dd2ab6aba1066e1e744cf4683992b5310f248a5b3bd2b898ad277c7a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f415049253230636f73742d253234302e30302d73756363657373"&gt;&lt;img src="https://camo.githubusercontent.com/bff67b39dd2ab6aba1066e1e744cf4683992b5310f248a5b3bd2b898ad277c7a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f415049253230636f73742d253234302e30302d73756363657373" alt="cost"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/5c0a6b59cfbdd799a3b738242e28f76fbd3574ddd120e4e8e9187cc0b8b75b06/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c69766525323064656d6f2543322542376f6e72656e6465722d736c6f74682d2d6472617765722d384132424532"&gt;&lt;img src="https://camo.githubusercontent.com/5c0a6b59cfbdd799a3b738242e28f76fbd3574ddd120e4e8e9187cc0b8b75b06/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c69766525323064656d6f2543322542376f6e72656e6465722d736c6f74682d2d6472617765722d384132424532" alt="live demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Status: the 19-manual air-purifier corpus runs unattended, offline, on the CPU — and the web demo is
live at &lt;a href="https://sloth-drawer.onrender.com/" rel="nofollow noopener noreferrer"&gt;https://sloth-drawer.onrender.com/&lt;/a&gt;&lt;/strong&gt; (Render free tier
built from &lt;code&gt;deploy/&lt;/code&gt;, zero env vars). &lt;code&gt;sloth-drawer scan data/manuals&lt;/code&gt; yields &lt;strong&gt;35 intervals found ·
22 verified · 13 repaired · 7 rejected&lt;/strong&gt; in ~10 minutes, at &lt;strong&gt;$0&lt;/strong&gt;, on a 4-core box with no GPU. The
verifier (&lt;code&gt;extract/verify.py&lt;/code&gt;) is the product; the report in &lt;a href="https://github.com/Gaurav5189/sloth-drawer/out/report.md" rel="noopener noreferrer"&gt;&lt;code&gt;out/report.md&lt;/code&gt;&lt;/a&gt; is the
evidence. Built for Hacktoberfest Open-Source AI Challenge: Week 3 — &lt;em&gt;Slow and Steady&lt;/em&gt; — post draft
staged for review.&lt;/p&gt;
&lt;p&gt;➡️ &lt;strong&gt;New here? &lt;a href="https://github.com/Gaurav5189/sloth-drawer/INSTRUCTIONS.md" rel="noopener noreferrer"&gt;&lt;code&gt;INSTRUCTIONS.md&lt;/code&gt;&lt;/a&gt; is the complete setup-and-use guide&lt;/strong&gt; — what to
install, which models to pull and &lt;em&gt;why&lt;/em&gt;, where…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Gaurav5189/sloth-drawer" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;The pipeline is: &lt;code&gt;ingest → chunk → regex pre-filter → (optional) 270M triage → 1B extraction →&lt;br&gt;
verification&lt;/code&gt;. Three decisions the benchmark forced:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. One fact per call, flat schema — never an array.&lt;/strong&gt; I benchmarked three models on 4 CPU cores,&lt;br&gt;
7.5 GB RAM, no GPU. Under an array-of-objects schema, &lt;code&gt;phi4-mini&lt;/code&gt; returned &lt;code&gt;{"intervals": []}&lt;/code&gt; —&lt;br&gt;
schema-valid, semantically empty. A flat &lt;code&gt;has_interval / task / interval_months / quote / section&lt;/code&gt;&lt;br&gt;
schema asked &lt;strong&gt;once per interval-shaped sentence&lt;/strong&gt; passed on every model tested.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The model points, the code quotes, the code recomputes.&lt;/strong&gt; The model returns a &lt;em&gt;quote fragment&lt;/em&gt;.&lt;br&gt;
Code then (1) locates the fragment in the page it came from, (2) expands it to the enclosing&lt;br&gt;
sentence, (3) &lt;strong&gt;re-parses the interval from that sentence itself&lt;/strong&gt;, and (4) compares it to the&lt;br&gt;
model's number. Three verdicts: &lt;code&gt;PASS&lt;/code&gt;, &lt;code&gt;REPAIR&lt;/code&gt; (code value wins, disagreement recorded), &lt;code&gt;REJECT&lt;/code&gt;.&lt;br&gt;
Every date on every card is computed in &lt;code&gt;storage/&lt;/code&gt; from a service log you keep. &lt;strong&gt;Zero model-authored&lt;br&gt;
dates.&lt;/strong&gt; That is the whole trick, and it is why nothing below can be hallucinated into a wrong date.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Three-tier cascade.&lt;/strong&gt; A regex pre-filter scores interval-shaped language for free; on this&lt;br&gt;
corpus only &lt;strong&gt;84 of 1,325 chunks&lt;/strong&gt; ever reach a model — 94% are filtered out at zero cost.&lt;br&gt;
&lt;code&gt;gemma3:270m&lt;/code&gt; (291 MB) is an optional yes/no triage gate; &lt;code&gt;gemma3:1b&lt;/code&gt; (806 MB) does the extraction&lt;br&gt;
at temperature 0 with a fixed seed and a structured-output format. Both models run locally via Ollama;&lt;br&gt;
the core install never imports an SDK.&lt;/p&gt;

&lt;p&gt;Unit conversions (90 days = 3 months, 720 hours = 1 month, ranges take the midpoint...) live in&lt;br&gt;
&lt;strong&gt;one table&lt;/strong&gt;, stated in the prompt &lt;em&gt;and&lt;/em&gt; in the verification code, pinned together by tests — if the&lt;br&gt;
model's rule and the code's rule ever drift, every "every 90 days" becomes a false REPAIR.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Validator Is the Product
&lt;/h2&gt;

&lt;p&gt;Small local models will lie to you; the job is making the lie unable to reach a printed date.&lt;br&gt;
The verifier's verdicts, with measured examples from &lt;code&gt;bench/&lt;/code&gt; and the shipped run:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model output&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;hallucinated quote (&lt;code&gt;"...every 6 months"&lt;/code&gt; — no such text on the page)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;REJECT&lt;/strong&gt; (&lt;code&gt;quote_not_in_page&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;real quote, wrong number (model said 3, the sentence says 12)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;REPAIR&lt;/strong&gt; — code value wins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cites a sentence containing no interval&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;REJECT&lt;/strong&gt; (&lt;code&gt;no_interval_in_citation&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;invented task label — a task on the card that does not appear in the manual text&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;REJECT&lt;/strong&gt; (&lt;code&gt;task_not_in_source&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A real &lt;code&gt;REPAIR&lt;/code&gt; from the run: &lt;strong&gt;Winix 5500-2, page 1&lt;/strong&gt; — the model said &lt;em&gt;"Clean once every 3 months"&lt;/em&gt;&lt;br&gt;
(the washable carbon filter line), but its quoting drifted into the neighbouring &lt;em&gt;"...Last Approx. 12&lt;br&gt;
months True HEPA Filter"&lt;/em&gt; line. The verifier re-parsed the cited sentence, got 12, wrote &lt;strong&gt;12&lt;/strong&gt; with&lt;br&gt;
the disagreement recorded. Both the 3-month and the 12-month tasks are on that card today, each&lt;br&gt;
citing its own verified sentence.&lt;/p&gt;

&lt;p&gt;The verifier is 554 lines of pure Python with no model calls and no project imports beyond&lt;br&gt;
&lt;code&gt;errors.py&lt;/code&gt; — the AST import test enforces that. If you take one idea from this post: &lt;strong&gt;an extraction&lt;br&gt;
pipeline does not need a smarter model, it needs a verified quote.&lt;/strong&gt; &lt;a href="https://dev.to/datacpp_8185/your-extraction-pipeline-doesnt-need-a-smarter-model-it-needs-a-schema-contract-test-5bp6"&gt;A recent DEV post&lt;/a&gt;&lt;br&gt;
lands on the same shape — treat model output as an untrusted API response and gate it — and my&lt;br&gt;
measured numbers agree, with one addition: re-derive the &lt;em&gt;value&lt;/em&gt; from the source text the model can&lt;br&gt;
only point at, so even a structurally perfect answer can fail against its own quotation.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Benchmark
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;bench/final.py&lt;/code&gt; on a synthetic multi-interval page, exact printed output (&lt;code&gt;out/bench-phase3.txt&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;th&gt;What the verifier did with its answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma3:270m&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;291 MB&lt;/td&gt;
&lt;td&gt;3.6 s&lt;/td&gt;
&lt;td&gt;claimed &lt;em&gt;1 month&lt;/em&gt;, cited a non-interval sentence → &lt;strong&gt;REJECT&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma3:1b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;806 MB&lt;/td&gt;
&lt;td&gt;9.1 s&lt;/td&gt;
&lt;td&gt;months = 2, quote located → &lt;strong&gt;PASS&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;phi4-mini&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2.5 GB&lt;/td&gt;
&lt;td&gt;53.9 s&lt;/td&gt;
&lt;td&gt;months = 2, quote located → &lt;strong&gt;PASS&lt;/strong&gt; (6× slower, 3× RAM, uses swap)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;gemma3:1b&lt;/code&gt; is the default; &lt;code&gt;phi4-mini&lt;/code&gt; stays behind &lt;code&gt;--model&lt;/code&gt; as a documented alternative. The&lt;br&gt;
failure of the 270M here is not in a prompt somewhere — it is the measured, reproducible reason the&lt;br&gt;
verifier ships with the tool.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Not the generic paragraph — the concrete one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The whole stack runs where you are.&lt;/strong&gt; 291 MB and 806 MB Gemma weights, 4 CPU cores, no GPU,
&lt;strong&gt;$0.00 for 22,401 tokens&lt;/strong&gt;. I proved the full 19-manual run with the network &lt;em&gt;off&lt;/em&gt; — inside a
user+network namespace with only loopback configured, a second Ollama served inside it, and no
route to anything external. Identical verdicts (&lt;code&gt;out/scan-offline.txt&lt;/code&gt;). Basements, garages,
air-gapped networks: the claim is a log line, not a promise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap the model, keep the card.&lt;/strong&gt; &lt;code&gt;--model phi4-mini&lt;/code&gt; changes how the agent behaves with the same
interface — deliberately, because open weights are the only thing that makes a model a
configuration item and not a dependency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap the retrieval backend, keep the card.&lt;/strong&gt; The same card comes out of the local SQLite FTS5
trigram index and out of a Tiger Cloud hybrid index (pgvector + &lt;code&gt;pg_textsearch&lt;/code&gt; BM25, fused with
Reciprocal Rank Fusion). A &lt;strong&gt;parity run over the live corpus: 4/4 queries PARITY&lt;/strong&gt; — every local
top-5 hit reachable in the cloud top-20. The cloud index is an &lt;em&gt;enhancement&lt;/em&gt;, never a prerequisite;
the physics of a plane flight cannot break the default.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy as a schema constraint, not a promise.&lt;/strong&gt; The cloud table (&lt;code&gt;manual_chunks&lt;/code&gt;) holds chunk
text + embedding — it has &lt;em&gt;no column&lt;/em&gt; that could hold a device name, serial number, purchase date
or service log, and the code that fills it never sees one. Only manual text and embeddings ever
cross the wire; the cards and the due dates stay on your machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The verifier also enforces locality of truth.&lt;/strong&gt; Because a model must cite a verbatim substring
of your own manual, the model can only ever point at text you already own — it cannot smuggle in
a "fact" you do not have a reference for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open formats end to end&lt;/strong&gt;: SQLite + JSON + Markdown reports, PDFs you already have. Uninstall
the tool and you still have everything it extracted, with citations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;I built this in the open with agentic AI. The Phase 3 wrap-up — the offline proof, the CodeRabbit&lt;br&gt;
triage round, and the documentation decisions — is saved publicly so you can watch the reasoning:&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="OpenCode"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Sloth Drawer — Phase 3 wrap-up: offline proof, CodeRabbit triage, and honest docs&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Continue Phase 3: finish the offline proof run and update the docs with the real numbers.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The offline proof completed: 19 manuals · 35 intervals found · 22 verified · 13 repaired · 7 rejected · 673s · 22401 tokens · $0.00 — identical verdicts to the networked run, inside a &lt;code&gt;unshare -rn&lt;/code&gt; namespace with only loopback up and a local Ollama. Exit code 1 is expected (rejections count as partial success).&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-shellofflineproof"&gt;shell (offline proof)&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;unshare -rn bash -c 'ip link set lo up; ollama serve &amp;amp;; sloth-drawer scan dat...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;unshare -rn bash -c 'ip link set lo up; ollama serve &amp;amp;; sloth-drawer scan data/manuals --no-persist'&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;19 manuals · 35 intervals found · 22 verified · 13 repaired · 7 rejected · 673s · 22401 tokens · $0.00 (log: out/scan-offline.txt)&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-editagentsmemorymd"&gt;edit (.agents/MEMORY.md)&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;record full corpus run 614s, resume pass 0.52s (19/19 skipped), benchmark re-...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;record full corpus run 614s, resume pass 0.52s (19/19 skipped), benchmark re-run, offline proof 673s, 281 pytest green (2 skipped)&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;MEMORY.md updated with final Phase 3 numbers&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Examine these CodeRabbit suggestions and make a plan whether to fix them or not and why. Also check for edge-case issues if the changes would affect our functionality.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;Three findings. Plan after verifying each against the code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;INDEX.md stale (minor) → FIX. Phase 3 is complete but INDEX.md still says 'in progress' and 'Blocked: none'. It's the retrieval entry point for future agents, so a stale state actively misroutes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;--list with --resume shows a partial table (minor) → FIX WITH A NOTE, don't reject the flag combination. Traced it: outcome.candidates only holds this run's scans (batch.py builds it from &lt;code&gt;extracted&lt;/code&gt;, which skips resumed manuals), so resumed manuals are silently absent. Rejecting --list --resume would break the legitimate 'resume then show me this run' flow. Instead, print a note when outcome.resumed &amp;gt; 0. Edge cases checked: resumed rows ARE in the DB and report (only the candidate dump is partial); --list --no-persist unaffected; outcome.resumed already exists so no new state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Autocommit would break per-manual transactions (major) → NO FIX NEEDED — false positive for the current code, and the requested mitigation already exists. connect() uses plain sqlite3.connect() (legacy transaction control: no isolation_level=None, no autocommit=True), so the first DML in store_manual implicitly begins the transaction that &lt;code&gt;with connection:&lt;/code&gt; commits/rolls back. And tests/test_batch.py::test_a_manual_is_written_in_one_transaction already monkeypatches the second interval write to fail and asserts nothing survived on reopen. The only cheap improvement: a load-bearing comment in connect() so a future 'modernization' to autocommit knows the invariant.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Yes, apply the recommended fixes.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-editdocsindexmd"&gt;edit (DOCS/INDEX.md)&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;phase → complete (2026-10-23); current task → Phase 4 (Tiger backend, parity ...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;phase → complete (2026-10-23); current task → Phase 4 (Tiger backend, parity run); blocked → Sentry screenshot needs a real SLOTH_SENTRY_DSN&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;INDEX.md resynced with PHASES.md&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-editsrcslothdrawerstoragedbpy"&gt;edit (src/sloth_drawer/storage/db.py)&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;document in connect() that legacy sqlite transaction control is load-bearing ...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;document in connect() that legacy sqlite transaction control is load-bearing for store_manual atomicity&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;comment added — after catching my own slip: the note initially went in as bare prose (a syntax error) and is now a proper # comment&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-editsrcslothdrawerclipy"&gt;edit (src/sloth_drawer/cli.py)&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;when outcome.resumed &amp;gt; 0, print a note that resumed manuals are not in the --...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;when outcome.resumed &amp;gt; 0, print a note that resumed manuals are not in the --list table; their intervals are in the report and database&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;note added before _print_candidates&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-editteststestbatchpy"&gt;edit (tests/test_batch.py)&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;pin the contract: assert outcome.candidates == () in test_resume_skips_an_unc...&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;pin the contract: assert outcome.candidates == () in test_resume_skips_an_unchanged_manual&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;assertion added&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-shellverify"&gt;shell (verify)&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;ruff check + format --check + mypy + pytest&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;ruff check + format --check + mypy + pytest&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;All checks passed; 281 passed, 2 skipped in 136s&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Is MEMORY.md also updated?&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;It had Phase 3 completion and the run numbers from earlier, but not today's review round. Updated: the blocked line now names the Sentry screenshot (needs a real SLOTH_SENTRY_DSN, capture in Phase 5) instead of 'none', and a new Decisions Log entry records the triage — the INDEX.md resync, the --list/--resume note (and why a note rather than rejecting the flag pair), and the connect() comment documenting why legacy sqlite transaction control underpins store_manual's atomicity.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-editagentsmemorymd"&gt;edit (.agents/MEMORY.md)&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;blocked line + new top Decisions Log entry for the CodeRabbit round 2 triage&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;blocked line + new top Decisions Log entry for the CodeRabbit round 2 triage&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;MEMORY.md fully current&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Can you save this session (nicely edited format) to my dev account agent sessions?&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Submitting this curated transcript to DEV as an agent session via DevRelay.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        10 of 10 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;Reading a maintenance table by hand, for 19 appliances, is about &lt;strong&gt;20 minutes per device when you&lt;br&gt;
care enough to look&lt;/strong&gt;, and — being honest — you never look.&lt;/p&gt;

&lt;p&gt;Measured payoff instead: drop 19 PDFs into a folder. Walk away. Ten minutes and fourteen seconds&lt;br&gt;
later you have 35 verified intervals, a print-ready card per device, and an accuracy report telling&lt;br&gt;
you exactly which 13 extractions needed repair and why 7 were rejected. &lt;strong&gt;≈37× less reading, $0 of&lt;br&gt;
API cost, forever, offline.&lt;/strong&gt; After that, maintenance is one tap (&lt;code&gt;done&lt;/code&gt;) or one click in the browser&lt;br&gt;
— and the code, not a language model, moves the due date.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — &lt;code&gt;gemma3:270m&lt;/code&gt; triage + &lt;code&gt;gemma3:1b&lt;/code&gt; extraction, the measured benchmark, and
the 270M's honest failure, all running locally through Ollama.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Tiger Data&lt;/strong&gt; — the optional hybrid retrieval backend: pgvector + &lt;code&gt;pg_textsearch&lt;/code&gt;
BM25, fused with RRF on Tiger Cloud, plus the 4/4 backend parity run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt; — &lt;a href="https://sloth-drawer.onrender.com/" rel="noopener noreferrer"&gt;https://sloth-drawer.onrender.com/&lt;/a&gt; on the free plan, zero env vars, the
seed snapshot shipped in the image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Entire (DEV)&lt;/strong&gt; — this post, and one embedded agent session above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo: &lt;strong&gt;&lt;a href="https://github.com/Gaurav5189/sloth-drawer" rel="noopener noreferrer"&gt;https://github.com/Gaurav5189/sloth-drawer&lt;/a&gt;&lt;/strong&gt; · Live demo: &lt;a href="https://sloth-drawer.onrender.com/" rel="noopener noreferrer"&gt;https://sloth-drawer.onrender.com/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>Touch Grass — A Bird ID App That Works When There's No Signal</title>
      <dc:creator>Gaurav Sethi</dc:creator>
      <pubDate>Wed, 07 Oct 2026 06:48:02 +0000</pubDate>
      <link>https://dev.to/gaurav0s/touch-grass-a-bird-id-app-that-works-when-theres-no-signal-dkl</link>
      <guid>https://dev.to/gaurav0s/touch-grass-a-bird-id-app-that-works-when-theres-no-signal-dkl</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built &lt;strong&gt;Touch Grass&lt;/strong&gt; because identifying a bird call shouldn't mean uploading your life to someone else's server. Most nature apps work like this: you record audio, it ships to a cloud API, you wait, you get a result, and somewhere a company logs that you were near a lake at 7am. I didn't want any of that.&lt;/p&gt;

&lt;p&gt;Touch Grass is a Progressive Web App that runs open-weight AI models entirely inside the browser. You record a 3-second bird clip, the app converts it to a mel spectrogram locally, and an ONNX model predicts the species — all without a network call after the initial download. The same approach works for plants and insects from photos. Once the model is cached in IndexedDB, the app works completely offline. No accounts, no tracking, no "please sign in."&lt;/p&gt;

&lt;p&gt;It's for people who actually want to spend time outside. The whole philosophy is: identify quickly, then put the phone back in your pocket and enjoy the trees.&lt;/p&gt;

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

&lt;p&gt;Live site: &lt;a href="https://touch-grass-143.pages.dev" rel="noopener noreferrer"&gt;https://touch-grass-143.pages.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Install it like a normal app — the service worker caches the shell, the manifest is set up, and offline identification works after the first model download. I tested the PWA install on Android Chrome: add to home screen, open it, allow the mic, record 3 seconds, and you'll see results come back from the local BirdNET model with no signal required. I haven't done a long birding trip yet, but the whole point is that it works exactly where the map stops — on the trail with no bars.&lt;/p&gt;

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


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Gaurav5189" rel="noopener noreferrer"&gt;
        Gaurav5189
      &lt;/a&gt; / &lt;a href="https://github.com/Gaurav5189/touch-grass" rel="noopener noreferrer"&gt;
        touch-grass
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A Progressive Web App that helps people identify birds, plants, and insects using open-weight AI models running entirely on-device. Works offline, keeps all data local, and minimizes screen time so you can get back to nature.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Touch Grass — Local-First Nature Identification PWA&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A Progressive Web App that helps people identify birds, plants, and insects using open-weight AI models running entirely on-device. Works offline, keeps all data local, and minimizes screen time so you can get back to nature.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Philosophy:&lt;/strong&gt; Identify quickly, then put the phone away and enjoy the outdoors.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Features&lt;/h2&gt;
&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bird Call ID&lt;/strong&gt; — Record 3-second audio clips, identify species via BirdNET-ONNX (~20MB, quantized, 3000+ species), all on-device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plant &amp;amp; Insect Photo ID&lt;/strong&gt; — Capture or import a photo, get top-5 matches from a MobileNetV3 model (~5MB), with descriptions and similar species.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fully Offline&lt;/strong&gt; — After initial model downloads, no internet required. Service worker caches the app shell; IndexedDB stores observations and model cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private by Default&lt;/strong&gt; — No accounts, no tracking, no server requests for inference. Optional GPS tagging is opt-in only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observation History&lt;/strong&gt; — Auto-save after every identification. Filter, search, and export…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Gaurav5189/touch-grass" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
Repo: &lt;a href="https://github.com/Gaurav5189/touch-grass" rel="noopener noreferrer"&gt;Gaurav5189/touch-grass&lt;/a&gt;

&lt;p&gt;Key parts worth looking at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/features/bird-id/&lt;/code&gt; — AudioRecorder hook, BirdResults UI, RegionFilter&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/features/photo-id/&lt;/code&gt; — CameraCapture, GalleryImport, MobileNet inference stub&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/features/history/&lt;/code&gt; — Observation list, filters, CSV/JSON/iNaturalist export&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/workers/inference.worker.ts&lt;/code&gt; — ONNX Runtime Web with WebGPU preferred, WASM fallback&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;public/manifest.json&lt;/code&gt; + &lt;code&gt;vite-plugin-pwa&lt;/code&gt; — full PWA setup&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Stack is straightforward: React 18, TypeScript 5.3, Vite 5, Tailwind 3.4. Nothing exotic. The interesting part is what runs inside the browser.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bird identification&lt;/strong&gt;: BirdNET-ONNX (quantized INT8, ~20MB, Apache-2.0, 3000+ species). It runs in a Web Worker via &lt;code&gt;onnxruntime-web&lt;/code&gt;, with WebGPU first and WASM fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plant / insect identification&lt;/strong&gt;: MobileNetV3 + custom iNaturalist head (~5MB, Apache-2.0 / CC-BY data).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline storage&lt;/strong&gt;: IndexedDB (&lt;code&gt;idb&lt;/code&gt;) for observations and model cache; Cache API for the app shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy&lt;/strong&gt;: Zero server requests for inference. Optional GPS is opt-in only. No analytics, no telemetry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I used OpenCode for the main structure — scaffolding the PWA, setting up the folder layout, and getting the build pipeline clean (CI with typecheck, lint, build). Then Antigravity helped with refinements and bug fixes — fixing TypeScript casting issues with &lt;code&gt;navigator.gpu&lt;/code&gt;, cleaning up import paths for &lt;code&gt;onnxruntime-web.d.ts&lt;/code&gt;, resolving build errors in &lt;code&gt;useModelDownload&lt;/code&gt;, and fixing the Playwright / Vitest conflict so E2E tests could run separately.&lt;/p&gt;

&lt;p&gt;The model strategy is open-weight all the way: BirdNET from the BirdNET-Analyzer repo, MobileNet weights fine-tuned on public iNaturalist data. All licenses documented in &lt;code&gt;DOCS/RULES.md&lt;/code&gt;. No proprietary or GPL dependencies.&lt;/p&gt;

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

&lt;p&gt;If this was a closed API, you couldn't swap the bird model for a regional 8MB pack. You couldn't upload your own ONNX file for a custom species set. You couldn't verify that your audio isn't being stored somewhere else. And you definitely couldn't run it in a cabin with no signal.&lt;/p&gt;

&lt;p&gt;Open-weight models make this possible because the weights are files you download, not services you call. The BirdNET team released their quantized model under Apache-2.0. The iNaturalist dataset is CC-BY. That means I can bundle them into a browser app without licensing anxiety or vendor lock-in. Open innovation here isn't a marketing line — it's the reason the app can work offline, stay private, and let users control which model they run.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/li&gt;
&lt;li&gt;Open Source / Open Weight Models&lt;/li&gt;
&lt;li&gt;Open Source AI Challenge&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>privacy</category>
    </item>
    <item>
      <title>LocalCommit: Offline Conventional Git Commits with Local LLMs</title>
      <dc:creator>Gaurav Sethi</dc:creator>
      <pubDate>Sun, 04 Oct 2026 21:20:48 +0000</pubDate>
      <link>https://dev.to/gaurav0s/localcommit-offline-conventional-git-commits-with-local-llms-opk</link>
      <guid>https://dev.to/gaurav0s/localcommit-offline-conventional-git-commits-with-local-llms-opk</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend (&lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;https://dev.to/challenges/hacktoberfest-weekend-2026-10-01&lt;/a&gt;)&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built LocalCommit, a lightweight CLI tool that inspects your staged git diffs and drafts concise Conventional Commit messages (feat(auth): add refresh token rotation) running entirely on your local machine using Ollama.&lt;br&gt;
I built this for my friend Manish, a software engineer working under strict client non-disclosure agreements. Manish's team requires strict Conventional Commits for automated changelogs and semantic releases, but company policy strictly forbids piping source code or git diffs to proprietary cloud LLM APIs (like OpenAI or Anthropic). Whenever Manish makes multi-file changes across large codebases, writing detailed conventional commit messages by hand slows down their workflow.&lt;br&gt;
LocalCommit solves this by reading git diff --staged and running inference on an open-weights model locally. Zero tokens leave the machine, no API keys are required, and the tool works even when working offline on a train or during an internet outage.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Here is how running LocalCommit looks in terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git add src/auth/token.ts src/middleware/jwt.ts
&lt;span class="nv"&gt;$ &lt;/span&gt;npx localcommit
&lt;span class="o"&gt;[&lt;/span&gt;localcommit] Checking staged changes...
&lt;span class="o"&gt;[&lt;/span&gt;localcommit] Sending diff to qwen2.5-coder:3b...

Proposed commit message:
  feat&lt;span class="o"&gt;(&lt;/span&gt;auth&lt;span class="o"&gt;)&lt;/span&gt;: implement refresh token rotation and validation

Commit with this message? &lt;span class="o"&gt;[&lt;/span&gt;y/N/e &lt;span class="o"&gt;(&lt;/span&gt;edit&lt;span class="o"&gt;)&lt;/span&gt;/r &lt;span class="o"&gt;(&lt;/span&gt;regenerate&lt;span class="o"&gt;)]&lt;/span&gt;: y
&lt;span class="o"&gt;[&lt;/span&gt;main 7f2a1b9] feat&lt;span class="o"&gt;(&lt;/span&gt;auth&lt;span class="o"&gt;)&lt;/span&gt;: implement refresh token rotation and validation
 2 files changed, 48 insertions&lt;span class="o"&gt;(&lt;/span&gt;+&lt;span class="o"&gt;)&lt;/span&gt;, 6 deletions&lt;span class="o"&gt;(&lt;/span&gt;-&lt;span class="o"&gt;)&lt;/span&gt;
You can confirm with &lt;span class="o"&gt;[&lt;/span&gt;y] to commit immediately, hit &lt;span class="o"&gt;[&lt;/span&gt;e] to edit &lt;span class="k"&gt;in &lt;/span&gt;place, &lt;span class="o"&gt;[&lt;/span&gt;r] to regenerate with a slightly different phrasing, or &lt;span class="o"&gt;[&lt;/span&gt;n] to cancel without touching git state.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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

&lt;p&gt;The source code and setup instructions are open source on GitHub:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Gaurav5189" rel="noopener noreferrer"&gt;
        Gaurav5189
      &lt;/a&gt; / &lt;a href="https://github.com/Gaurav5189/LocalCommit" rel="noopener noreferrer"&gt;
        LocalCommit
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Local-first CLI for generating conventional commit messages via Ollama.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;LocalCommit&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Local-first CLI for generating conventional commit messages with Ollama.&lt;/p&gt;

&lt;p&gt;LocalCommit reads your staged Git diff, generates a Conventional Commit message
with the configured local model, and asks for confirmation before committing.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Requirements&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Node.js 18+&lt;/li&gt;
&lt;li&gt;Git CLI&lt;/li&gt;
&lt;li&gt;Ollama&lt;/li&gt;
&lt;li&gt;A model configured in the root &lt;code&gt;model&lt;/code&gt; file (default: &lt;code&gt;qwen2.5-coder:3b&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If Ollama is not installed, follow the platform-specific instructions in
&lt;a href="https://github.com/Gaurav5189/LocalCommit/INSTRUCTION.md" rel="noopener noreferrer"&gt;INSTRUCTION.md&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npm run setup&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The setup command verifies Node.js and Git, checks that Ollama is running,
downloads &lt;code&gt;qwen2.5-coder:3b&lt;/code&gt; if it is missing, installs dependencies, and
builds the project.&lt;/p&gt;
&lt;p&gt;If setup reports that Ollama is unavailable, install it and start the Ollama
service by following &lt;a href="https://github.com/Gaurav5189/LocalCommit/INSTRUCTION.md" rel="noopener noreferrer"&gt;INSTRUCTION.md&lt;/a&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Usage&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Start Ollama in a separate terminal if it is not already running:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;ollama serve&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Stage your changes in a Git repository, then run:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;npx localcommit&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Inside a checkout of this project, &lt;code&gt;npx localcommit&lt;/code&gt; resolves the local package
binary. To use it from…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Gaurav5189/LocalCommit" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
Repository link: &lt;a href="https://github.com/Gaurav5189/LocalCommit" rel="noopener noreferrer"&gt;Gaurav5189/LocalCommit&lt;/a&gt;
&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;LocalCommit is written in TypeScript and connects to Ollama (&lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;https://ollama.com/&lt;/a&gt;) running locally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Default Model: By default, it targets qwen2.5-coder:3b—a fast, open-weights code model capable of running with low latency on modest CPU or laptop GPU hardware without draining battery.&lt;/li&gt;
&lt;li&gt;Context Auto-Detection: Instead of assuming a static token limit, LocalCommit queries Ollama's /api/ps and &lt;code&gt;/api/show&lt;/code&gt; endpoints at runtime to discover the exact context window allocated by your environment.&lt;/li&gt;
&lt;li&gt;Two-Stage Summarization for Large Diffs:&lt;/li&gt;
&lt;li&gt;Small diffs fit in a single prompt and produce an immediate message.&lt;/li&gt;
&lt;li&gt;For larger, multi-file changes exceeding the context window, it switches to a hierarchical strategy: it prioritizes source code files (src/, tests/), generates concise per-file summaries bounded by a strict 60-second budget, and feeds that synthesized context into a final commit prompt.&lt;/li&gt;
&lt;li&gt;Deterministic Post-Processing: Small local models can occasionally output markdown fences or descriptive preambles. LocalCommit passes the raw output through a deterministic formatting engine that enforces standard type(scope): subject syntax, cleans generic scopes, and converts verbs into imperative mood (add, fix, refactor).&lt;/li&gt;
&lt;li&gt;Cross-Platform Host Detection: Handles network boundary quirks across Linux, macOS, Windows, and containerized setups (like Docker) by probing OLLAMA_HOST, 127.0.0.1:11434, and localhost:11434.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Open-weights models and local runtimes like Ollama make use cases possible that cloud AI endpoints fundamentally cannot support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complete Data Sovereignty &amp;amp; Privacy: Codebases under NDAs, proprietary IP, or internal air-gapped networks cannot send raw git diffs over external APIs. Open models let developers use AI assistance without ever leaking code.&lt;/li&gt;
&lt;li&gt;Offline Reliability: You don't need active Wi-Fi, metered bandwidth, or cloud service uptime to commit your work.&lt;/li&gt;
&lt;li&gt;Zero Ongoing Costs &amp;amp; No API Keys: Developers don't have to manage credit cards, rate limits, or monthly token bills.&lt;/li&gt;
&lt;li&gt;Flexibility to Swap Models: You can drop in any local model (e.g. llama3.2, codellama, deepseek-coder) simply by editing the model file or setting the LOCALCOMMIT_MODEL environment variable.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;The development session where I debugged the container connectivity issues and fixed the Ollama host resolution:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="OpenCode"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;LocalCommit: Fixing cross-platform Ollama connectivity for containers&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The setup script and CLI are failing to connect to Ollama in GitHub Codespaces because localhost resolves to ::1 but Ollama binds to &lt;span class="agent-session-redacted"&gt;[REDACTED]&lt;/span&gt;. Need multi-host resolution and proper URL construction.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;I'll create a shared Ollama client utility for host resolution and fix both the setup script and CLI.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-write"&gt;write&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;src/ollama-client.ts&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;ollama&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Created&lt;/span&gt; &lt;span class="nx"&gt;shared&lt;/span&gt; &lt;span class="nx"&gt;utility&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;getOllamaHosts&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;ollamaFetch&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;checkOllamaReachable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-edit"&gt;edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;src/llm.ts&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Updated&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt; &lt;span class="nx"&gt;multi&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="nx"&gt;resolution&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="sr"&gt;/api/&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/api/&lt;/span&gt;&lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sr"&gt;/api/&lt;/span&gt;&lt;span class="nx"&gt;ps&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-edit"&gt;edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;src/env-check.ts&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Updated&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt; &lt;span class="nx"&gt;shared&lt;/span&gt; &lt;span class="nf"&gt;checkOllamaReachable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-edit"&gt;edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;scripts/setup.js&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;scripts&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Fixed&lt;/span&gt; &lt;span class="nx"&gt;double&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;slash&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt; &lt;span class="nf"&gt;bug &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/tags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;and&lt;/span&gt; &lt;span class="nx"&gt;reordered&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="nx"&gt;priority&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Build succeeded but setup still fails - need to debug the http.get call in setup.js&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Found the issue - urlObj.toString() + '/api/tags' creates double slash. Fixed with new URL(path, base).&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-edit"&gt;edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;scripts/setup.js&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;scripts&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Fixed&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt; &lt;span class="nx"&gt;construction&lt;/span&gt; &lt;span class="nx"&gt;using&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/tags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Setup now works. But when model in 'model' file doesn't match installed Ollama model, error message is unclear. Add guidance to run 'npm run setup'.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Added special case handling for 404 'model not found' error with actionable message pointing to 'npm run setup'.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-edit"&gt;edit&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;src/llm.ts&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Added&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;found&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="nx"&gt;handling&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;setup&lt;/span&gt; &lt;span class="nx"&gt;guidance&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Now help me write a DEV post for Hacktoberfest Weekend Challenge submission.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        7 of 6 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;


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

&lt;ul&gt;
&lt;li&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/li&gt;
&lt;li&gt;Open-Source AI / Open Weights Integration (Llama 3.3, Qwen 2.5, gemma 4)&lt;/li&gt;
&lt;li&gt;Local Inference &amp;amp; Edge AI (Ollama 100% offline laptop execution)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  LICENSE
&lt;/h2&gt;

&lt;p&gt;This project is open source and available under the MIT License.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Building LinkCut: A Sub-20ms URL Shortener with Next.js, FastAPI, and Redis</title>
      <dc:creator>Gaurav Sethi</dc:creator>
      <pubDate>Mon, 24 Aug 2026 19:29:42 +0000</pubDate>
      <link>https://dev.to/gaurav0s/building-linkcut-a-sub-20ms-url-shortener-with-nextjs-fastapi-and-redis-52a1</link>
      <guid>https://dev.to/gaurav0s/building-linkcut-a-sub-20ms-url-shortener-with-nextjs-fastapi-and-redis-52a1</guid>
      <description>&lt;p&gt;Hi, i'm Gaurav. This is my first post to dev.to&lt;/p&gt;

&lt;h2&gt;
  
  
  What is LinkCut?
&lt;/h2&gt;

&lt;p&gt;LinkCut is a free, open-source URL shortener with no signup required — paste a link, cut it, share it. No accounts, no tracking cookies, no dashboard bloat.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔗 Live app: &lt;a href="https://linkcut-nine.vercel.app/" rel="noopener noreferrer"&gt;https://linkcut-nine.vercel.app/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 Repo: &lt;a href="https://github.com/Gaurav5189/url-shortener" rel="noopener noreferrer"&gt;https://github.com/Gaurav5189/url-shortener&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I wanted to design a system where redirect latency actually mattered, not just a CRUD app with a UI wrapper. That meant thinking about caching, write-amplification, and decoupling reads from writes — not just "shorten a string and save it to a DB."&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The core idea: &lt;strong&gt;separate the fast path from the slow path.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fast path (Edge Middleware):&lt;/strong&gt; checks Redis first, returns a 302 redirect directly from the edge, and fires a non-blocking &lt;code&gt;INCR&lt;/code&gt; for click tracking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slow path (Backend fallback):&lt;/strong&gt; on a cache miss, FastAPI decodes the Base62 short code to an integer ID for an O(1) indexed lookup in SQLite, then repopulates Redis with a 12-hour TTL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background path (Cron):&lt;/strong&gt; flushes Redis click counters into SQLite periodically using an atomic Lua script — read-and-delete in a single Redis transaction, so counts never get double-counted or lost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps redirects fast (sub-20ms) while analytics writes stay off the hot path entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend/Edge:&lt;/strong&gt; Next.js (App Router) on Vercel Edge Middleware&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cache/Counter:&lt;/strong&gt; Redis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; FastAPI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; SQLite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoding:&lt;/strong&gt; Base62, 5-character codes from auto-incrementing IDs (~916M capacity)&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I underestimated this going in. The URL shortening part is trivial — the interesting engineering is in the caching strategy, avoiding database write-amplification on every click, and making sure the analytics counter flush is atomic so nothing gets lost or double-counted under concurrent traffic.&lt;/p&gt;

&lt;p&gt;Feedback on the caching strategy or architecture decisions welcome — always looking to improve it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>fastapi</category>
      <category>nextjs</category>
      <category>redis</category>
    </item>
  </channel>
</rss>
