<?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: Pranesh Nikhar</title>
    <description>The latest articles on DEV Community by Pranesh Nikhar (@praneshnikhar).</description>
    <link>https://dev.to/praneshnikhar</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%2F4157339%2Fb72a24f1-1600-4e46-8e3d-56bcfeb9485b.jpg</url>
      <title>DEV Community: Pranesh Nikhar</title>
      <link>https://dev.to/praneshnikhar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/praneshnikhar"/>
    <language>en</language>
    <item>
      <title>Off-Peak: the AI agent whose best feature is doing nothing</title>
      <dc:creator>Pranesh Nikhar</dc:creator>
      <pubDate>Fri, 23 Oct 2026 18:01:29 +0000</pubDate>
      <link>https://dev.to/praneshnikhar/off-peak-the-ai-agent-whose-best-feature-is-doing-nothing-2jkp</link>
      <guid>https://dev.to/praneshnikhar/off-peak-the-ai-agent-whose-best-feature-is-doing-nothing-2jkp</guid>
      <description>&lt;p&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;. #hf26challenge&lt;/p&gt;

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

&lt;p&gt;I have a folder of ten documents I keep not summarizing. Meeting notes, an invoice, a sleep study, a paper I saved, receipts. The obvious fix is an agent that runs immediately. That agent is a bad roommate: it spins the fan, drains the battery, and interrupts me to tell me it is working.&lt;/p&gt;

&lt;p&gt;Off-Peak is the opposite. You hand it a pile and a deadline. It watches your laptop's idle time, battery, and thermal state, predicts how long the work will take, and waits until you leave. If you come back early, it pauses after the current item. If the lid closes, it resumes from the last finished item. In the morning, it leaves a report.&lt;/p&gt;

&lt;p&gt;It is a scheduler, not a chatbot. The model does the work; the scheduler decides whether the work is worth doing now.&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%2F714tuinpijthkiw7j53h.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%2F714tuinpijthkiw7j53h.png" alt="Off-Peak dashboard" width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The recorded run used ten documents and a compressed day. The human is away at lunch and in the evening. Off-Peak deferred 21 times, worked only in those away windows, and finished all ten before the simulated 10:30 PM deadline.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Baseline: run everything now&lt;/th&gt;
&lt;th&gt;Off-Peak&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Documents finished&lt;/td&gt;
&lt;td&gt;9/10 (one provider 503)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10/10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interruptions&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Energy while present&lt;/td&gt;
&lt;td&gt;1.01 Wh&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.00 Wh&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Energy while away&lt;/td&gt;
&lt;td&gt;0.00 Wh&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.09 Wh&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model time&lt;/td&gt;
&lt;td&gt;130 s&lt;/td&gt;
&lt;td&gt;398 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The baseline finished in 130 seconds and lost one document to a provider 503. Off-Peak took 398 seconds of model time across the day. It is slower. That is the point. It moves the work out of the time I am using the laptop. Nine context switches became zero.&lt;/p&gt;

&lt;p&gt;The honest caveat: the Gemma 4 31B endpoint returned intermittent 500/503s during the run. Three documents used Gemma; the rest fell back to Gemini 3.8 Flash. The router kept the queue alive, which is what a durable scheduler is for. The repo also has a fully local Ollama path, and the default model is open-weight Gemma 4 31B.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/praneshnikhar/off-peak" rel="noopener noreferrer"&gt;github.com/praneshnikhar/off-peak&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The core of the scheduler is small enough to read:&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;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_present&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;only_when_away&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deadline_override&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;latest_safe&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deadline pressure: the latest safe start has arrived&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;wait_action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;you are at the keyboard; the latest safe start is ...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other three gates are battery, thermal, and window length. If they all pass, the worker runs at most two items per tick, writes the output, checkpoints the item, and yields. Pause is checked before every item, so coming back to the desk is a soft stop, not a lost batch.&lt;/p&gt;

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

&lt;p&gt;Stack: Python 3.10, SQLite, &lt;code&gt;psutil&lt;/code&gt;, the Gemini API for Gemma 4 31B, Ollama as the local path, a stdlib HTTP dashboard, and 13 tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Queue and checkpointing.&lt;/strong&gt; Every file is an item with a status. The output is written before the item is marked done. On startup, any &lt;code&gt;running&lt;/code&gt; job becomes &lt;code&gt;paused&lt;/code&gt;; the worker resumes from the first &lt;code&gt;pending&lt;/code&gt; item. Sleep, a crash, or a closed lid costs at most the item in flight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Telemetry.&lt;/strong&gt; macOS idle time comes from &lt;code&gt;ioreg&lt;/code&gt;, battery and thermal state from &lt;code&gt;pmset&lt;/code&gt;, CPU and load from &lt;code&gt;psutil&lt;/code&gt;. Linux uses &lt;code&gt;xprintidle&lt;/code&gt; and &lt;code&gt;/sys&lt;/code&gt;. The scheduler only sees a &lt;code&gt;SystemState&lt;/code&gt; dataclass, so the same policy runs against real telemetry or the compressed day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prediction.&lt;/strong&gt; The default predictor is a transparent heuristic calibrated on the agent's own runs: &lt;code&gt;items * overhead + chars/1000 * seconds_per_kchar&lt;/code&gt;. The repo also includes a TabPFN adapter that trains on the local run log and reports holdout MAE against a median baseline. The local TabPFN weights need a one-time license token; without it, the calibrated heuristic is used. I chose not to fake a learned predictor for the demo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Model routing.&lt;/strong&gt; Gemma 4 31B is the default. The router filters thought parts from the response, parses strict JSON, retries rate limits, and falls back to Gemini 3.8 Flash on provider 500/503. A circuit breaker skips the primary for ten minutes after a failure, so the rest of the queue does not pay the timeout again. Every run records the model that actually handled it. The fallback is visible in the report, not hidden.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dashboard.&lt;/strong&gt; One HTML file, one JSON endpoint, no build step, no external fonts or scripts. It reads the same SQLite file as the CLI. The timeline is the audit log: green worked, grey waited, amber paused for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What broke.&lt;/strong&gt; The first version ran the whole pile immediately, which is exactly the behavior I was trying to avoid. The second version paused on user presence but did not have a latest-safe-start calculation, so a job could wait forever. The deadline math is what makes laziness safe. The Gemma endpoint was the third surprise: open-weight models are not automatically more reliable than closed ones. The answer is not to hide the failure; it is to route around it and log it.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The decision layer has to be local.&lt;/strong&gt; A hosted agent cannot see my battery, my thermal state, or the fact that I just walked back to the desk. Off-Peak's scheduler is a small policy over local telemetry. That is the part that makes the project work. The model is replaceable; the context is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patience is only free with open weights.&lt;/strong&gt; A metered API makes "run 500 small jobs overnight" an economic decision. A downloaded model makes it a scheduling decision. The marginal cost of deferring is zero, so the agent can afford to be picky about when it runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model is swappable.&lt;/strong&gt; Gemma 4 31B through the Gemini API for throughput, Gemma through Ollama for a fully offline run, or another model behind the same &lt;code&gt;summarize&lt;/code&gt; interface. The policy does not change. When the Gemma endpoint failed, the fallback was a configuration, not a rewrite.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My files stay mine.&lt;/strong&gt; Off-Peak never uploads files. In API mode, the text of a document is sent as a prompt to the model endpoint; the queue, outputs, telemetry, and history stay in SQLite on the laptop. In Ollama mode, nothing leaves the machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The failure is inspectable.&lt;/strong&gt; When the endpoint returned 500/503, the fallback and the checkpoints were visible in the report. I could see which model handled each document. There was no opaque service deciding to drop the queue.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;9 interruptions avoided.&lt;/li&gt;
&lt;li&gt;0 minutes watching a progress bar.&lt;/li&gt;
&lt;li&gt;3.09 Wh of estimated model energy moved off my active hours.&lt;/li&gt;
&lt;li&gt;10/10 documents finished before the deadline.&lt;/li&gt;
&lt;li&gt;The most productive thing my laptop did this week was while I was not there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The point is not that the agent is fast. It is that the agent is &lt;strong&gt;patient&lt;/strong&gt;. Slowness is not a bug; it is a scheduling decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;The recorded day is compressed, and the energy numbers are estimates from model time and a fixed laptop wattage. Ten documents is a demo, not a benchmark. The Gemma endpoint was intermittent, so the model mix is reported honestly. Calendar integration is the obvious next step for the idle horizon.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma.&lt;/strong&gt; Gemma 4 31B is the default model for the work. The recorded run used it for the documents it could, and the local Ollama path uses Gemma for a fully offline run. The scheduler is model-agnostic, but Gemma is what makes the no-API-key path real. I am also entering the overall prize; the TabPFN adapter is in the repo, but I am not claiming that category because I could not run the gated weights in this environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What shaped this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/john_afariogun_e2351c78af/the-hidden-physics-of-ai-on-your-own-machine-inference-engineering-local-first-4f9p"&gt;The Hidden Physics of AI on Your Own Machine: Inference Engineering, Local-First&lt;/a&gt; by &lt;a href="https://dev.to/john_afariogun_e2351c78af"&gt;John Afariogun&lt;/a&gt; — local inference is a physical problem, not just a privacy checkbox.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/galagent/30-days-of-a-local-ai-agent-on-my-own-machine-what-actually-broke-1nji"&gt;30 Days of a Local AI Agent on My Own Machine: What Actually Broke&lt;/a&gt; by &lt;a href="https://dev.to/galagent"&gt;galagent&lt;/a&gt; — the failures that matter are restarts and resource contention, which is why checkpointing is central here.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/akki907/temporal-workflow-orchestration-building-reliable-agentic-ai-systems-3bpm"&gt;Temporal + AI Agents: The Missing Piece for Production-Ready Agentic Systems&lt;/a&gt; by &lt;a href="https://dev.to/akki907"&gt;akki907&lt;/a&gt; — the durable-execution lesson, applied here with SQLite checkpoints rather than a Temporal cluster.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/praneshnikhar/off-peak" rel="noopener noreferrer"&gt;github.com/praneshnikhar/off-peak&lt;/a&gt;&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/praneshnikhar/off-peak
&lt;span class="nb"&gt;cd &lt;/span&gt;off-peak
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;".[dev]"&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env  &lt;span class="c"&gt;# add GEMINI_API_KEY&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; offpeak demo &lt;span class="nt"&gt;--backend&lt;/span&gt; gemini &lt;span class="nt"&gt;--model&lt;/span&gt; gemma-4-31b-it
python &lt;span class="nt"&gt;-m&lt;/span&gt; offpeak serve &lt;span class="nt"&gt;--port&lt;/span&gt; 8765
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fully local:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama serve
ollama pull gemma3:1b
python &lt;span class="nt"&gt;-m&lt;/span&gt; offpeak demo &lt;span class="nt"&gt;--backend&lt;/span&gt; ollama &lt;span class="nt"&gt;--model&lt;/span&gt; gemma3:1b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the agent is going to do the work, it should at least have the manners to wait until I am gone.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Tender Radar: finding government tenders for small contractors, entirely on their own laptop</title>
      <dc:creator>Pranesh Nikhar</dc:creator>
      <pubDate>Sat, 17 Oct 2026 14:45:15 +0000</pubDate>
      <link>https://dev.to/praneshnikhar/tender-radar-finding-government-tenders-for-small-contractors-entirely-on-their-own-laptop-466k</link>
      <guid>https://dev.to/praneshnikhar/tender-radar-finding-government-tenders-for-small-contractors-entirely-on-their-own-laptop-466k</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;&lt;strong&gt;Tender Radar&lt;/strong&gt; is a local-first government tender radar for small contractors.&lt;/p&gt;

&lt;p&gt;A small electrical contractor bids on public tenders. Notices land on GeM, state portals and department boards, and the office has to answer three questions fast: which notices matter, is the firm actually eligible (turnover, similar work, licences), and how do we assemble the compliance paperwork without missing a clause. Miss one certificate and the bid is rejected on technical grounds; the preparation cost is gone. Many MSME contractors pay commission agents for this filtering, or simply miss tenders they would have won.&lt;/p&gt;

&lt;p&gt;Tender Radar answers all three on the contractor's own machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Extracts&lt;/strong&gt; the eligibility clauses from each notice (turnover, EMD, experience, licences, certifications)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matches&lt;/strong&gt; every clause against the firm's own documents (past bids, completion certificates, licence and ISO scans, audited financials), with a quote behind every claim&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flags&lt;/strong&gt; risky terms in plain language (an EMD at 9% of value, a deadline three days out, a job ten times anything the firm has built)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Estimates&lt;/strong&gt; win probability from the firm's historical bids with TabPFN&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drafts&lt;/strong&gt; the covering letter, compliance statement and capability summary behind a citation guard that deletes any sentence its own documents cannot support&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Who would pay: MSME contractors who pay agents per bid, or lose tenders they never saw. One won tender a year covers a year of tooling.&lt;/p&gt;

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

&lt;p&gt;The board, ranked by fit and win probability:&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%2Fvl32z3u1m4yuw0k5k4hb.jpg" 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%2Fvl32z3u1m4yuw0k5k4hb.jpg" alt="Tender Radar board" width="800" height="730"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One tender, with the evidence quote and document citation behind every requirement:&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%2F1pg40wrsb6tknwoofdks.jpg" 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%2F1pg40wrsb6tknwoofdks.jpg" alt="Tender detail with evidence" width="666" height="1400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The print-ready bid pack it produces:&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%2Friq5f2kepuyzppwfjssk.jpg" 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%2Friq5f2kepuyzppwfjssk.jpg" alt="Bid pack" width="106" height="1200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything above was computed locally, on purpose: the whole point is that it runs on the machine that holds the sensitive documents. Five commands take you from clone to board (full list in the README): install, generate the demo corpus, load it, scan 8 tenders, open the dashboard. The corpus is a synthetic but realistic electrical MSME (Jaipur, Class-A licence, about INR 3.7 crore average turnover), clearly labelled; live portal ingestion is scaffolded behind a key.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/praneshnikhar/tender-radar" rel="noopener noreferrer"&gt;github.com/praneshnikhar/tender-radar&lt;/a&gt;, built during the challenge window. &lt;code&gt;scripts/eval_summary.py&lt;/code&gt; reproduces every number below.&lt;/p&gt;

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

&lt;p&gt;Extraction, verdicts and drafting all run on &lt;strong&gt;Gemma 4&lt;/strong&gt; (gemma4:latest, 4.6B Q4) via Ollama, fully local. Amounts are parsed deterministically in code, because money must never depend on a model's mood. Matching is hybrid retrieval (a built-in TF-IDF embedder plus BM25) followed by one Gemma verdict pass per tender. Win probability is &lt;strong&gt;TabPFN v3.5 with thinking mode&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The citation guard is the part I am proud of, and it exists because a 4B model is not a frontier model. I caught it marking a clause "met" while its own rationale argued the firm fell short. So every "met" carries a quote, verified before display: exact match, or a fuzzy match where numbers are compared numerically ("INR 84.5 lakh" passes against "INR 84.50 lakh"; "two crore" fails against "2 crore"). A deterministic amount cross-check runs independently of the model.&lt;/p&gt;

&lt;p&gt;Measured on an M3 laptop with 8 GB RAM, everything local: 51 clauses extracted across 8 tenders, every one with a parsed amount; verdicts 24 met, 5 partial, 19 missing, 3 unknown; the quote guard rejected 3 model citations (downgraded, reason recorded) and the cross-check caught 4 numeric conflicts; drafting removed 8 unsupported sentences across 16 sections; 21 LLM calls, about 83k tokens, US$0.00; 22 minutes for 8 tenders.&lt;/p&gt;

&lt;p&gt;Win model ablation on the same time-ordered split (147 submitted bids): sklearn baseline AUC 0.731 / Brier 0.260 vs &lt;strong&gt;TabPFN v3.5 + thinking: AUC 0.752 / Brier 0.209&lt;/strong&gt;. A should-I-bid number lives or dies by calibration.&lt;/p&gt;

&lt;p&gt;Reading that shaped the guard: &lt;a href="https://dev.to/enhanciar_ai_96a4ba4877e3/how-we-make-an-llm-cite-every-answer-down-to-fileline-and-check-the-citation-is-telling-the-truth-hmj"&gt;Your AI cited a real file. It still lied to you.&lt;/a&gt; and &lt;a href="https://dev.to/sidswirl/making-rag-admit-when-its-guessing-source-grounded-hallucination-checks-g22"&gt;Making RAG admit when it is guessing&lt;/a&gt;.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The data is competitively sensitive.&lt;/strong&gt; A contractor's audited financials, past bids and pricing are the most sensitive documents the firm owns. Every document-reading and document-writing step is a local open-weight model. The only network call in the system is the TabPFN win model, and it sends features (value, distance, competitor count), never documents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero marginal cost changes behaviour.&lt;/strong&gt; The alternatives cost money per bid (agents) or per seat (SaaS). Local inference makes scanning one more notice free, so the firm checks every notice instead of the ones it can afford to check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small-data problems need small-data models, and a fallback.&lt;/strong&gt; Win probability is modelled over about 220 historical bids; TabPFN is built for that regime, unlike closed AutoML that wants thousands of rows. The pipeline also degrades gracefully to sklearn, so it has zero mandatory external dependencies.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The honest trade-off: a 4B local model is slower and rougher at prose than a frontier API on a per-tender basis. The guard makes that safe, and the privacy and cost properties make it worth it. A closed API would have been easier to build and impossible to ship to this customer.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&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;Tender Radar: building a local-first government tender radar with Gemma 4, TabPFN, and a citation guard&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&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;Side Hustle challenge: build with open-source AI at its core. Government tender radar for small contractors. What it does: scrapes tender portals such as GeM, matches tenders to a small firm's past work and certifications, builds an eligibility checklist, and drafts the compliance sections. Open stack: a multi-agent pipeline with a vector index of the firm's documents, running locally. Who pays: MSME contractors who currently pay agents commissions or miss tenders entirely. Why open wins: their financials and past bids are competitively sensitive. Locked constraints: week 3 only for now, budget 0, electrical contractor persona. Create a new folder and start making everything, first plan.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &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;Plan first, then build. Key decisions from environment reconnaissance:&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-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;sysctl CPU + RAM; docker --version; ollama list&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 agent-session-terminal"&gt;&lt;code&gt;$ sysctl CPU + RAM; docker --version; ollama list&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;Apple M3, 8 GB RAM. Docker daemon hung (no response). Ollama installed with gemma4:latest (4.6B Q4, tools+thinking, 3.3GB) already pulled. No embedding model present. Local Postgres 17 on :5432 but no pgvector extension.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Decisions: SQLite as default store (fully local, ACID, brute-force cosine search is instant at demo scale) with an optional Postgres+pgvector path documented; open-weight LLM via Ollama; built-in hashed TF-IDF embedder as fallback so nothing needs downloading; TabPFN for the win-probability model with sklearn fallback. Architecture: notices -&amp;gt; extract -&amp;gt; match -&amp;gt; score -&amp;gt; draft -&amp;gt; bid pack. Prize targeting: Gemma (core LLM), TabPFN (win model), honest about everything else.&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;Scaffolded the project: pyproject, db/schema.sql, and the core modules.&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/tender_radar/ingest/fixtures.py&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 python"&gt;&lt;code&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tender_radar&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;ingest&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;fixtures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&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 python"&gt;&lt;code&gt;&lt;span class="n"&gt;FixtureGenerator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;synthetic&lt;/span&gt; &lt;span class="n"&gt;but&lt;/span&gt; &lt;span class="n"&gt;realistic&lt;/span&gt; &lt;span class="n"&gt;electrical&lt;/span&gt; &lt;span class="nc"&gt;MSME &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Sharma&lt;/span&gt; &lt;span class="n"&gt;Electricals&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;Co&lt;/span&gt;&lt;span class="p"&gt;.,&lt;/span&gt; &lt;span class="n"&gt;Jaipur&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Class&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="n"&gt;license&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="mf"&gt;3.7&lt;/span&gt; &lt;span class="n"&gt;crore&lt;/span&gt; &lt;span class="n"&gt;avg&lt;/span&gt; &lt;span class="n"&gt;turnover&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt; &lt;span class="n"&gt;firm&lt;/span&gt; &lt;span class="nf"&gt;documents &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;audited&lt;/span&gt; &lt;span class="n"&gt;financials&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;licence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ISO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt; &lt;span class="n"&gt;certificates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capability&lt;/span&gt; &lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt; &lt;span class="n"&gt;tender&lt;/span&gt; &lt;span class="n"&gt;notices&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;authentic&lt;/span&gt; &lt;span class="n"&gt;Rajasthan&lt;/span&gt; &lt;span class="n"&gt;procurement&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;220&lt;/span&gt; &lt;span class="n"&gt;historical&lt;/span&gt; &lt;span class="n"&gt;bid&lt;/span&gt; &lt;span class="n"&gt;outcomes&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-write"&gt;write&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;src/tender_radar/match/__init__.py&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 python"&gt;&lt;code&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;tender_radar&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&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 python"&gt;&lt;code&gt;&lt;span class="n"&gt;Hybrid&lt;/span&gt; &lt;span class="n"&gt;retrieval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="n"&gt;similarity&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;BM25&lt;/span&gt; &lt;span class="n"&gt;over&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;then&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="n"&gt;LLM&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="k"&gt;pass&lt;/span&gt; &lt;span class="n"&gt;per&lt;/span&gt; &lt;span class="n"&gt;tender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;quote_verified&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;exact&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="n"&gt;passes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;fuzzy&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;aware&lt;/span&gt; &lt;span class="n"&gt;numeric&lt;/span&gt; &lt;span class="n"&gt;comparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt; &lt;span class="nf"&gt;numeric_cross_check&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;deterministic&lt;/span&gt; &lt;span class="n"&gt;backstop&lt;/span&gt; &lt;span class="n"&gt;comparing&lt;/span&gt; &lt;span class="n"&gt;INR&lt;/span&gt; &lt;span class="n"&gt;amounts&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;clause&lt;/span&gt; &lt;span class="n"&gt;vs&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;cited&lt;/span&gt; &lt;span class="n"&gt;document&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;dont pull any models, choose any model which i already have, but i will be using api key&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;Understood - using only gemma4:latest locally for LLM work, API key for TabPFN, and the built-in embedder. Smoke-tested Gemma 4 on clause extraction: it returned clean structured JSON with all five requirement types correctly segmented; a full generated tender notice read like a real state portal document.&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;Two real bugs found and fixed during the build worth recording:&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-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;python -m pytest test/ (after debugging sqlite error)&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 agent-session-terminal"&gt;&lt;code&gt;$ python -m pytest test/ (after debugging sqlite error)&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;(1) 'table evidence has 11 columns but 10 values were supplied' - traced by counting the literal '?' placeholders in the SQL string; an edit had updated the Python tuple to 11 values but left 10 placeholders. Fixed the placeholder count, not the tuple. (2) Quote guard rejected 'INR 84.5 lakh' against a document saying 'INR 84.50 lakh' because tokenization split '84.50' into [84, 50]. Rewrote the check: currency amounts are parsed with units (crore/lakh -&amp;gt; same scale) and compared numerically; bare numbers still require verbatim token match. Verified: 'two crore' fails against '2 crore'; 'INR 6.2 crore' fails against 'INR 62 lakh'; equivalent reformattings pass. 8 tests pass.&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-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;Full measured run on 8 tenders, all local on the M3/8GB: 51 clauses extracted, every one with a parsed amount. Verdicts: 24 met / 5 partial / 19 missing / 3 unknown. The guard rejected 3 model citations (downgraded to unknown with reason) and the deterministic amount cross-check caught 4 numeric conflicts, including one where the model marked a clause 'met' while its own rationale argued the firm fell short. Draft guard removed 8 unsupported sentences across 16 sections. 21 LLM calls, ~83k tokens total, $0.00. Took screenshots of the board, detail page, and print-ready bid pack.&lt;/p&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;tabpfn_sk_&lt;strong&gt;REDACTED&lt;/strong&gt; - this is the tabpfn key&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;Installed tabpfn-client and found the auth subtlety: the library reads TABPFN_TOKEN, not TABPFN_API_KEY, so the config now accepts both and calls set_access_token explicitly. A/B tested checkpoints on the same time-ordered 75/25 split over 147 submitted bids:&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-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;compare default vs v3.5_default vs v3.5_default+thinking on the same split&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 agent-session-terminal"&gt;&lt;code&gt;$ compare default vs v3.5_default vs v3.5_default+thinking on the same split&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;default checkpoint: AUC 0.6713, Brier 0.2137. v3.5_default: AUC 0.6713, Brier 0.2137. v3.5_default + thinking(medium): AUC 0.7378, Brier 0.2084.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Thinking mode became the default. Final eval: sklearn baseline AUC 0.731 / Brier 0.260 vs TabPFN v3.5+thinking AUC 0.752 / Brier 0.209 - better on both, and calibration is what a 'should I bid?' number needs.&lt;/p&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;use the devrelay tool to make a submission for week 2 (Side Hustle), as this is a week 2 project&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;Submission workflow: pushed the repo to GitHub (praneshnikhar/tender-radar, built within the challenge window), generated web-sized screenshots for the post, extracted the exact DEV submission template from the challenge page, saved this session (scrubbed, no keys or local paths), and published the submission post with the required devchallenge and hf26challenge tags, entering Best Use of Gemma and Best Use of TabPFN.&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;
        11 of 13 messages
          · some content was hidden by the author
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&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;: Gemma 4 does all clause extraction, verdicts and drafting, running locally via Ollama.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of TabPFN&lt;/strong&gt;: TabPFN v3.5 with thinking mode is the win-probability model, with a same-split ablation against an sklearn baseline.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Built in a day for Hacktoberfest Week 2. The firm's data never leaves the laptop; the bills never arrive.&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>WanderLog: an offline field journal that answers "where did I see that kingfisher last time?"</title>
      <dc:creator>Pranesh Nikhar</dc:creator>
      <pubDate>Thu, 08 Oct 2026 17:15:26 +0000</pubDate>
      <link>https://dev.to/praneshnikhar/wanderlog-an-offline-field-journal-that-answers-where-did-i-see-that-kingfisher-last-time-fg7</link>
      <guid>https://dev.to/praneshnikhar/wanderlog-an-offline-field-journal-that-answers-where-did-i-see-that-kingfisher-last-time-fg7</guid>
      <description>&lt;p&gt;I have a habit of seeing things outside and then losing them to my own memory. The peacock that called from the fog. The kingfisher that dove twice off the jetty and came up with a fish. The lizard big enough to hiss like a bicycle pump.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;WanderLog&lt;/strong&gt; — a local-first field journal that turns my coding agent into a naturalist's notebook. It's a Model Context Protocol (MCP) server. I go outside; when I get back I record a thirty-second voice memo of what I saw, and one command turns it into structured sightings and a walk. Everything else — remembering, finding, connecting — happens on my own machine, with open-source AI, with no network.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; TypeScript · MCP (Model Context Protocol) · SQLite · ONNX Runtime · &lt;a href="https://huggingface.co/docs/transformers.js" rel="noopener noreferrer"&gt;Transformers.js&lt;/a&gt; · open-weight embeddings (&lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;) · local &lt;code&gt;gemma3:4b&lt;/code&gt; via Ollama · ElevenLabs Scribe (optional) · Sentry &lt;code&gt;gen_ai&lt;/code&gt; tracing&lt;br&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/praneshnikhar/wanderlog" rel="noopener noreferrer"&gt;github.com/praneshnikhar/wanderlog&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;WanderLog is a field journal for the person who notices things outside and forgets them: the birder without a checklist, the hiker who wants to remember which trail had fog on the lake, the person who sees a peacock on the commute and wants to know whether it's the same one from last month.&lt;/p&gt;

&lt;p&gt;It's built around one rule: &lt;strong&gt;the screen should be the shortest part of the experience.&lt;/strong&gt; There's no dashboard to maintain, no logbook to transcribe, no "curated profile of your nature observations." There's a walk, a thirty-second voice memo, and a question you can answer a month later.&lt;/p&gt;

&lt;p&gt;On the trail, you just talk to your phone like you're telling a friend what you saw. At home, one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;npm run log-voice &lt;span class="nt"&gt;--&lt;/span&gt; ~/walk.m4a
&lt;span class="go"&gt;transcribing walk.m4a with ElevenLabs...
transcript (scribe_v2, eng):
  "Saw a white-breasted kingfisher at the lake jetty. It dove twice and got a fish.
   Also spotted a rain lily on the path side, fresh after last night's rain.
   Walked the Central Park loop, three point two kilometers in fifty minutes. Fog on the lake."
&lt;/span&gt;&lt;span class="gp"&gt;logged walk #&lt;/span&gt;1
&lt;span class="go"&gt;a walk on Central Park loop, 3.2 km, 50 minutes, fog on the lake. Walked on 2026-10-08.
&lt;/span&gt;&lt;span class="gp"&gt;logged sighting #&lt;/span&gt;1
&lt;span class="go"&gt;bird white-breasted kingfisher, at lake jetty, dove twice and got a fish. Spotted on 2026-10-08.
&lt;/span&gt;&lt;span class="gp"&gt;logged sighting #&lt;/span&gt;2
&lt;span class="go"&gt;plant rain lily, at path side, fresh after last night's rain. Spotted on 2026-10-08.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No typing, no forms. The memo became a walk, two sightings linked to that walk, and three searchable journal entries — and the spoken "three point two kilometers" became a real number.&lt;/p&gt;

&lt;p&gt;Then the journal answers questions about itself, from the same local model:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ WANDERLOG_OLLAMA_MODEL=gemma3:4b npm run demo

  "where did I see the kingfisher last time?"

  1. [2026-10-03] sighting #10 — white-breasted kingfisher at Maota Lake, Amber
     score=0.465 semantic=0.500 keyword=0.186

  Answer from local gemma3:4b:

  According to entry [1] (2026-10-03), the white-breasted kingfisher was last
  sighted at Maota Lake, Amber. The note specifically states "bird
  white-breasted kingfisher, at Maota Lake, Amber".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It doesn't search for the word "kingfisher" — it searches for what the entry &lt;em&gt;means&lt;/em&gt;. "Where did I see it last time" and "bird with the blue flash near the lake at golden hour" both land on the same entries, because the journal is understood by the same open-weight model that understands you. And the answer above was written by a 4B model on the same laptop: it cited the entry number, parsed the date, and got the place right.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The full loop on a real walk — memo recorded on the trail, logged at home, question answered — is being field-tested on Friday, October 9. The video and an honest field report will land right here after the walk:&lt;/p&gt;



&lt;p&gt;Everything in the terminal sessions above is real output from the actual project, not mock-ups: the voice run is a real transcription of a real audio file, and the search output comes from the seeded journal.&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/praneshnikhar" rel="noopener noreferrer"&gt;
        praneshnikhar
      &lt;/a&gt; / &lt;a href="https://github.com/praneshnikhar/wanderlog" rel="noopener noreferrer"&gt;
        wanderlog
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &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;WanderLog&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A &lt;strong&gt;local-first field journal&lt;/strong&gt; for people who spend time outside.&lt;/p&gt;

&lt;p&gt;WanderLog is a Model Context Protocol (MCP) server that turns your coding
agent into a naturalist's notebook. Log the birds, plants, and trails you see
on a walk; later ask in plain language &lt;em&gt;"where did I see the kingfisher last
time?"&lt;/em&gt; — and get an answer computed &lt;strong&gt;entirely on your machine&lt;/strong&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open-source AI at the core.&lt;/strong&gt; Semantic search runs on an open-weight
sentence-transformer (&lt;code&gt;Xenova/all-MiniLM-L6-v2&lt;/code&gt;, Apache-2.0) executing
in-process via &lt;a href="https://huggingface.co/docs/transformers.js" rel="nofollow noopener noreferrer"&gt;Transformers.js&lt;/a&gt;
and ONNX Runtime. Everything that &lt;em&gt;understands&lt;/em&gt; your journal — Q&amp;amp;A and
voice-note parsing — runs on &lt;strong&gt;Gemma&lt;/strong&gt; (&lt;code&gt;gemma3:4b&lt;/code&gt;) through
&lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;, on your machine. No API keys, no cloud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No internet required.&lt;/strong&gt; After the first run, the embedding model is cached
and every query is answered with zero network calls. Your sightings, notes
and locations never leave the SQLite file on your disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice-first.&lt;/strong&gt; A…&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/praneshnikhar/wanderlog" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;MIT licensed, one &lt;code&gt;npm test&lt;/code&gt; away from reproducible: a hermetic smoke test drives every tool over real MCP stdio, plus unit tests for the voice pipeline that stub both the ElevenLabs and Ollama calls.&lt;/p&gt;

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



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph trail [On the trail]
        A[Voice memo&amp;lt;br/&amp;gt;no typing]
    end
    subgraph home [At home, on-device]
        B[Transcribe&amp;lt;br/&amp;gt;ElevenLabs Scribe · optional]
        C[Parse&amp;lt;br/&amp;gt;gemma3:4b via Ollama]
        D[(SQLite journal&amp;lt;br/&amp;gt;sightings · walks · vectors)]
        E[Semantic search + answers&amp;lt;br/&amp;gt;MiniLM embeddings · gemma3:4b]
    end
    A --&amp;gt; B --&amp;gt; C --&amp;gt; D --&amp;gt; E
    A -.-&amp;gt;|--transcript: no network at all| C&lt;/code&gt;&lt;/pre&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt; — one SQLite file (&lt;code&gt;~/.wanderlog/journal.db&lt;/code&gt;): sightings, walks, and a small &lt;code&gt;embeddings&lt;/code&gt; table that stores each entry's vector as a blob. At field-journal scale (hundreds to low thousands of entries) a vector database would be ceremony, not engineering — cosine similarity over the table runs in milliseconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings&lt;/strong&gt; — &lt;code&gt;@huggingface/transformers&lt;/code&gt; loads an open-weight ONNX sentence-transformer (&lt;code&gt;all-MiniLM-L6-v2&lt;/code&gt;, Apache-2.0) and runs it &lt;em&gt;in-process&lt;/em&gt; with ONNX Runtime. First run downloads ~23 MB once; after that, every query is answered with zero network calls. If the model can't load, search degrades gracefully to keyword overlap — the journal still works at the edge of the trail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice&lt;/strong&gt; — the new piece. A memo is transcribed (ElevenLabs Scribe by default, &lt;strong&gt;optional&lt;/strong&gt;), then a local open-weight model parses the transcript into JSON — walk, sightings, places, notes — validated against a zod schema. If the model invents a category the schema doesn't know, the entry is kept and bucketed as &lt;code&gt;other&lt;/code&gt; rather than dropped. Pass &lt;code&gt;--transcript&lt;/code&gt; and the entire pipeline runs offline; no bytes leave the machine. One honest wrinkle: Scribe runs zero-retention by default (&lt;code&gt;enable_logging=false&lt;/code&gt;), which is an enterprise feature on ElevenLabs' side — on smaller plans the CLI says so up front and offers &lt;code&gt;WANDERLOG_STT_LOGGING=1&lt;/code&gt; (explicit retention) or the fully local path instead of silently downgrading your privacy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Q&amp;amp;A&lt;/strong&gt; — &lt;code&gt;answer_question&lt;/code&gt; retrieves the top evidence locally, then asks &lt;strong&gt;your&lt;/strong&gt; local Ollama model (I tested &lt;code&gt;llama3.2:3b&lt;/code&gt; and Google's open-weight &lt;code&gt;gemma3:4b&lt;/code&gt;) to answer strictly from those entries, citing entry numbers. No Ollama? It returns the ranked evidence, which for a field journal is usually answer enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ranking&lt;/strong&gt; — hybrid score: embedding similarity (75%) merged with keyword overlap (25%), with a freshness tiebreak so a "when did I last see X" question prefers the &lt;em&gt;recent&lt;/em&gt; entry over an equally-relevant older one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracing&lt;/strong&gt; — every journal operation is instrumented with Sentry's &lt;code&gt;gen_ai&lt;/code&gt; semantic attributes: tool calls are &lt;code&gt;gen_ai.execute_tool&lt;/code&gt; spans, transcription is &lt;code&gt;gen_ai.transcription&lt;/code&gt;, parsing and Q&amp;amp;A are &lt;code&gt;gen_ai.chat&lt;/code&gt; with token counts, and search and embeddings are spans too. Set &lt;code&gt;SENTRY_DSN&lt;/code&gt; and you can watch the agent think.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Measured on my MacBook (no GPU fan spin-up, no cloud): cold model load &lt;strong&gt;244 ms&lt;/strong&gt;, one embedding &lt;strong&gt;3 ms&lt;/strong&gt;, full semantic search over a 16-entry journal &lt;strong&gt;9 ms&lt;/strong&gt;. The only thing slower than that is me, on the trail.&lt;/p&gt;

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

&lt;p&gt;The closed version of this product would be an app with a cloud account. And the cloud version of a &lt;em&gt;field journal&lt;/em&gt; is exactly wrong, for four reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Location data stays private.&lt;/strong&gt; Every sighting has a place attached — sometimes precise coordinates. eBird and iNaturalist are amazing, but they ask me to send every observation (and where I was standing) to their servers. WanderLog keeps the notebook in a file I own. Not a server I don't control — a file I can &lt;code&gt;cat&lt;/code&gt;, back up, or delete with my own hands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model is swappable because it's open.&lt;/strong&gt; &lt;code&gt;WANDERLOG_EMBED_MODEL&lt;/code&gt; and &lt;code&gt;WANDERLOG_OLLAMA_MODEL&lt;/code&gt; are environment variables. Don't like MiniLM? Point it at another open-weight embedder and the journal starts understanding you differently — no vendor change, no data migration, no "new API version." That's what open weights buy you: &lt;strong&gt;your data is interpreted by models &lt;em&gt;you&lt;/em&gt; choose, not by a company's pricing page.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing and runs anywhere.&lt;/strong&gt; A laptop, a Raspberry Pi, a 2020 MacBook — the whole inference stack is ONNX Runtime plus a 23 MB model, and the optional Q&amp;amp;A and parsing layer runs on &lt;code&gt;gemma3:4b&lt;/code&gt;, a 4B open-weight model that fits comfortably in RAM. There is no per-token bill, no rate limit, no "free tier."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can change how the agent behaves.&lt;/strong&gt; It's an MCP server; the journal-keeper prompt, the tools, and the scoring are all source in a repo you can fork. Fine-tuning your agent &lt;em&gt;is&lt;/em&gt; editing the prompt — the whole behavior contract is inspectable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The one cloud call in the whole system — transcription — is opt-in in both directions: you can use it, skip it (&lt;code&gt;--transcript&lt;/code&gt;), or swap it, and everything downstream of the transcript is open weights on your disk. The open approach won precisely where it matters: &lt;strong&gt;everywhere the network ends.&lt;/strong&gt; The trail is the last place on earth that has birds but no bandwidth, and WanderLog was designed for exactly that boundary — the model is already on the disk, the data is already on the disk, and the two of them never need to meet a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tools
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log_sighting&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Record a species, category, place, weather, field notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log_walk&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Record an outing: trail, distance, duration, notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;log_voice_note&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Voice memo (or raw transcript) → transcription + structured entries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;search_journal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hybrid semantic + keyword search, computed on-device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;answer_question&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Local-Gemma answer with citations; evidence fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;journal_overview&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Totals, category counts, last outing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Plus MCP resources (&lt;code&gt;journal://stats&lt;/code&gt;, &lt;code&gt;journal://day/{date}&lt;/code&gt;, &lt;code&gt;journal://category/{category}&lt;/code&gt;, &lt;code&gt;journal://walk/{id}&lt;/code&gt;), so any client can browse the notebook like files.&lt;/p&gt;

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

&lt;p&gt;The build — including verifying the voice pipeline with a real memo and catching the zero-retention tier limit the hard way — is saved on DEV as an agent session:&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;WanderLog: verifying the voice pipeline end-to-end and catching a zero-retention 403&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;My Mac lagged and I had to force restart — I think you were in the middle of verifying the voice extension?&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;Picking up where we left off: re-running the typecheck and the full test suite (voice unit tests + MCP smoke test), then the real end-to-end run with gemma3:4b.&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-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;npm run typecheck &amp;amp;&amp;amp; npm test&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 agent-session-terminal"&gt;&lt;code&gt;$ npm run typecheck &amp;amp;&amp;amp; npm test&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;tsc --noEmit clean. Voice tests passed (JSON parse fallback, zero-retention multipart to ElevenLabs, transcript structuring into walk + sightings). Smoke test: 15 checks passed, including log_voice_note over real MCP stdio.&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-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;All green. Next: the on-device path through scripts/log-voice.ts with gemma3:4b, writing to a temp DB so the real journal is untouched.&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-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;WANDERLOG_DB=/tmp/wanderlog-e2e.db WANDERLOG_OLLAMA_MODEL=gemma3:4b npm run l...&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 agent-session-terminal"&gt;&lt;code&gt;$ WANDERLOG_DB=/tmp/wanderlog-e2e.db WANDERLOG_OLLAMA_MODEL=gemma3:4b npm run log-voice -- --transcript "saw a white-breasted kingfisher at the lake jetty, it dove twice and got a fish. also a rain lily on the path side. walked the central park loop, 3.2 kilometers in 50 minutes, fog on the lake."&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;logged walk #1 — a walk on central park loop, 3.2 km, 50 minutes, fog on the lake. logged sighting #1 — bird white-breasted kingfisher, at lake jetty, dove twice and got a fish. logged sighting #2 — plant rain lily, at path side. (parsed on-device with gemma3:4b)&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;Here's an ElevenLabs API key so you can test a real voice memo. (Key omitted from this transcript for security.)&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;Generating a real voice memo with macOS text-to-speech, then running the full audio → transcript → structured entries pipeline.&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-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;say -v Samantha -o /tmp/memo.aiff "saw a white-breasted kingfisher at the lak...&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 agent-session-terminal"&gt;&lt;code&gt;$ say -v Samantha -o /tmp/memo.aiff "saw a white-breasted kingfisher at the lake jetty... walked the central park loop, 3.2 kilometers in 50 minutes" &amp;amp;&amp;amp; afconvert -f m4af -d aac /tmp/memo.aiff /tmp/memo.m4a &amp;amp;&amp;amp; ELEVENLABS_API_KEY=*** WANDERLOG_OLLAMA_MODEL=gemma3:4b npm run log-voice -- /tmp/memo.m4a&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;transcribing /tmp/memo.m4a with ElevenLabs...
log-voice failed: ElevenLabs transcription failed: HTTP 403 {"detail":{"type":"authorization_error","code":"forbidden","message":"Only users from the enterprise or trial tier can use ZRM mode.","status":"not_allowed"}}&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-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;That's a real finding, not a fluke: the code asks for zero-retention (enable_logging=false) by default, and ElevenLabs gates that to enterprise/trial plans. Confirming the rest of the pipeline works when logging is explicitly allowed, and that the user is told the tradeoff instead of it happening silently:&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-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;ELEVENLABS_API_KEY=*** WANDERLOG_STT_LOGGING=1 WANDERLOG_OLLAMA_MODEL=gemma3:...&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 agent-session-terminal"&gt;&lt;code&gt;$ ELEVENLABS_API_KEY=*** WANDERLOG_STT_LOGGING=1 WANDERLOG_OLLAMA_MODEL=gemma3:4b npm run log-voice -- /tmp/memo.m4a&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;transcript (scribe_v2, eng): "Saw a white-breasted kingfisher at the lake jetty. It dove twice and got a fish. Also spotted a rain lily on the path side. Fresh after last night's rain, walked the Central Park loop. Three point two kilometers in fifty minutes. Fog on the lake"
logged walk #1 + 2 sightings (bird + plant), linked to the walk. (parsed on-device with gemma3:4b)&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-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;Now fixing the failure mode: a 403 from the zero-retention gate must become an actionable error — never a raw HTTP dump, and never a silent downgrade of the privacy default.&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/voice.ts — if !keepLogging &amp;amp;&amp;amp; res.status === 403 &amp;amp;&amp;amp; /ZRM|zero[- ]retentio...&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;voice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;keepLogging&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="sr"&gt;/ZRM|zero&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;- &lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;retention/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ElevenLabs plan does not support zero-retention mode — set WANDERLOG_STT_LOGGING=1 to let ElevenLabs keep the audio for this transcription, or pass an existing transcript instead.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&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;Edit&lt;/span&gt; &lt;span class="nx"&gt;applied&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-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;Adding a unit test that simulates the ZRM 403 and asserts the helpful message.&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;test/voice.test.ts — add zrmDenied mock returning 403 with the enterprise-tie...&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;test&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="nx"&gt;add&lt;/span&gt; &lt;span class="nx"&gt;zrmDenied&lt;/span&gt; &lt;span class="nx"&gt;mock&lt;/span&gt; &lt;span class="nx"&gt;returning&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="kd"&gt;with&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;enterprise&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;tier&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rejects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;transcribeAudio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioPath&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sr"&gt;/WANDERLOG_STT_LOGGING=1/&lt;/span&gt;&lt;span class="p"&gt;)&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;Edit&lt;/span&gt; &lt;span class="nx"&gt;applied&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-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;Re-running everything, plus a live check against the real API to see exactly what a user now gets.&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-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;npm run typecheck &amp;amp;&amp;amp; npm test &amp;amp;&amp;amp; (live run without WANDERLOG_STT_LOGGING)&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 agent-session-terminal"&gt;&lt;code&gt;$ npm run typecheck &amp;amp;&amp;amp; npm test &amp;amp;&amp;amp; (live run without WANDERLOG_STT_LOGGING)&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;typecheck clean. Voice tests: 5 sections, incl. "transcribeAudio explains the zero-retention plan limit instead of dumping HTTP 403". Smoke: 15 checks. Live run: "log-voice failed: ElevenLabs plan does not support zero-retention mode — set WANDERLOG_STT_LOGGING=1 to let ElevenLabs keep the audio for this transcription, or pass an existing transcript instead."&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;Commit it, and don't worry about rotating the key.&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-bash"&gt;bash&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;git add -A &amp;amp;&amp;amp; git commit -m "add voice-note pipeline: ElevenLabs transcriptio...&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 agent-session-terminal"&gt;&lt;code&gt;$ git add -A &amp;amp;&amp;amp; git commit -m "add voice-note pipeline: ElevenLabs transcription, on-device gemma3 parsing, Sentry tracing" &amp;amp;&amp;amp; git push&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;[main 9cd5580] add voice-note pipeline: ElevenLabs transcription, on-device gemma3 parsing, Sentry tracing — 14 files changed, 1618 insertions(+), 156 deletions(-). fec0cbc..9cd5580 main -&amp;gt; main&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;Pasted the DEV submission template for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass.&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;Pulled the challenge rules, confirmed the project's first commit (Oct 6) is inside the challenge window, then restructured the existing DEV draft to the template: voice-first story, gen_ai tracing, honest zero-retention wrinkle, and the three prize categories it genuinely qualifies for.&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-getchallenges"&gt;get_challenges&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;Hacktoberfest Week 1 — Touch Grass&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;Hacktoberfest Week 1 — Touch Grass&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;Submissions due Oct 11, 11:59 PM PDT. Judged on: writing quality (heaviest), relevance to theme, creativity, technical execution, optional partner use. Required tag #hf26challenge. Featured: Best Use of Gemma. Partner: ElevenLabs, Sentry Agent Tracing, and others.&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-updatearticle"&gt;update_article&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;article 4804421 — full restructure to the submission template with the voice-...&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;article 4804421 — full restructure to the submission template with the voice-memo flow, Sentry tracing section, prize categories, and field-test TODOs&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;Draft updated on DEV.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&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;
        13 of 13 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;WanderLog genuinely uses three of this week's partner technologies, so I'm entering all three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — &lt;code&gt;gemma3:4b&lt;/code&gt; via Ollama is the intelligence of the project: it parses spoken notes into structured field entries and answers journal questions with citations, strictly from retrieved evidence. It runs on the same laptop that holds the journal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of ElevenLabs&lt;/strong&gt; — Scribe is the transcription leg of the voice flow, and the only cloud step. Zero-retention by default, optional by design (&lt;code&gt;--transcript&lt;/code&gt; skips it entirely), and honest when the plan can't honor zero-retention instead of quietly logging your audio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Sentry Agent Tracing&lt;/strong&gt; — every operation the agent performs emits &lt;code&gt;gen_ai.*&lt;/code&gt; traces to Sentry when &lt;code&gt;SENTRY_DSN&lt;/code&gt; is set: tool calls, transcription, chat with token counts, embeddings, and search. It's how I watched a 4B model on a laptop behave like an agent.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;I'm taking WanderLog to Central Park, Jaipur this week for a real field test — logging actual sightings from a voice memo, and asking the journal questions at the end. This post will be updated with the field report and the video.&lt;/p&gt;

&lt;p&gt;Built in three days, entirely inside the Hacktoberfest Week 1 window, MIT licensed, with a hermetic test suite that exercises every tool over real MCP stdio.&lt;/p&gt;

&lt;p&gt;Open source made this project &lt;em&gt;possible&lt;/em&gt; — the model is open, the runtime is open, and the notebook is yours. Go outside. Take notes. Ask it questions later.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built for &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest 2026 Open-Source AI Challenge: Week 1&lt;/a&gt; — Touch Grass. Built on open-source AI: Transformers.js + open-weight embeddings, Gemma 3 via Ollama, MCP, SQLite — all open, all local, all yours.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>hf26challenge</category>
      <category>devchallenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a voice language tutor that lives on my friend's laptop</title>
      <dc:creator>Pranesh Nikhar</dc:creator>
      <pubDate>Sat, 03 Oct 2026 21:11:51 +0000</pubDate>
      <link>https://dev.to/praneshnikhar/i-built-a-voice-language-tutor-that-lives-on-my-friends-laptop-5g2m</link>
      <guid>https://dev.to/praneshnikhar/i-built-a-voice-language-tutor-that-lives-on-my-friends-laptop-5g2m</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;A friend of mine is learning Spanish. They're not bad at it — they're &lt;em&gt;scared&lt;/em&gt; of it. Practicing with a real person means being wrong out loud, in front of another human, and that freezes them. Language apps feel like flashcards with a timer, not a conversation. And a tutor costs money they don't have.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Habla Conmigo&lt;/strong&gt; — a patient voice tutor that lives on their own laptop. You talk to it like a person: tap the mic, say whatever you want in Spanish (or half-Spanish, half-English, or completely stuck English), and it replies &lt;strong&gt;out loud&lt;/strong&gt;, gently correcting mistakes and keeping the conversation going.&lt;/p&gt;

&lt;p&gt;It solves the exact problem my friend had: a practice partner with zero judgment, infinite patience, and a memory that follows the conversation across turns.&lt;/p&gt;

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

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/rMEPxz6QT_8" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Live (deployed, UI + API — the model itself runs locally, by design): &lt;a href="https://habla-conmigo-sxfq.onrender.com/" rel="noopener noreferrer"&gt;habla-conmigo.onrender.com&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/praneshnikhar/hacktoberfest" rel="noopener noreferrer"&gt;github.com/praneshnikhar/hacktoberfest&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Three layers, one conversation loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Browser mic → ffmpeg&lt;/strong&gt; — the frontend records a webm blob; the server converts it to mp3 with ffmpeg.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ElevenLabs Scribe (STT)&lt;/strong&gt; — transcribes the audio, auto-detecting language so a stuck student can fall back to English mid-sentence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-weight model via Ollama (the tutor brain)&lt;/strong&gt; — the reply comes from a model running locally on the laptop (Llama 3.2 / Gemma 3), with a system prompt that makes it a &lt;em&gt;tutor&lt;/em&gt;: reply in the target language, keep it to 1–3 sentences, correct one mistake at a time, always end with a follow-up question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ElevenLabs TTS&lt;/strong&gt; — speaks the reply back in a warm multilingual voice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Conversation memory (last 8 turns) lives in the server, so the tutor follows topics across turns instead of starting fresh every sentence.&lt;/p&gt;

&lt;p&gt;The whole thing is ~700 lines of plain Node.js + Express + vanilla JS. No framework, no database, no cloud AI.&lt;/p&gt;

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

&lt;p&gt;This is the part I care about most, because the open pieces are &lt;em&gt;why the app exists at all&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A nervous learner's words never leave their machine.&lt;/strong&gt; The core of this app — the part that understands and replies — is an open-weight model running locally. A closed API would ship every halting, half-grammatical sentence of a shy person to a third-party server, and bill per message. Here it's free and private, and it even works offline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in, swap any model in one line.&lt;/strong&gt; Because the brain is Ollama, changing the teacher is a one-line &lt;code&gt;.env&lt;/code&gt; change: Llama → Gemma → Qwen → whatever ships next. Try doing that with a closed model. This also means the tutor can be fine-tuned later for my friend's specific mistakes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open where it matters, closed only where it adds value.&lt;/strong&gt; The one closed service (ElevenLabs) does one narrow thing — voice — and even that degrades gracefully: remove the API key and the app still works as a text tutor. The product doesn't collapse when the proprietary part is taken away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; The model is free, the runtime is free, the voice tier has a free plan. "Practicing a language" shouldn't have a subscription.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The counterfactual is what convinced me: with a closed model, I would have needed a paid API, an account per user, a data policy, and a conversation my friend couldn't afford to have.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;Built end-to-end with the opencode agent harness (open-source CLI coding agent) driving the implementation, debugging, and this write-up.&lt;/em&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of ElevenLabs&lt;/strong&gt; — an open-source agent given a voice: Scribe STT + multilingual TTS power the full conversational loop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt; — deployed via the included &lt;code&gt;render.yaml&lt;/code&gt; blueprint&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Tags: #devchallenge #weekendchallenge #hf26challenge&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
