<?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: Jackson Ly</title>
    <description>The latest articles on DEV Community by Jackson Ly (@jacksonxly).</description>
    <link>https://dev.to/jacksonxly</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%2F4007401%2F57bfa95e-a3e3-42db-8089-c13d9d6d3a54.jpg</url>
      <title>DEV Community: Jackson Ly</title>
      <link>https://dev.to/jacksonxly</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jacksonxly"/>
    <language>en</language>
    <item>
      <title>Our Rust file watcher ate 23.6 GB of RAM, and our ignore rules never had a chance</title>
      <dc:creator>Jackson Ly</dc:creator>
      <pubDate>Mon, 13 Jul 2026 13:40:17 +0000</pubDate>
      <link>https://dev.to/jacksonxly/our-rust-file-watcher-ate-236-gb-of-ram-and-our-ignore-rules-never-had-a-chance-49pn</link>
      <guid>https://dev.to/jacksonxly/our-rust-file-watcher-ate-236-gb-of-ram-and-our-ignore-rules-never-had-a-chance-49pn</guid>
      <description>&lt;p&gt;We build a local-first indexer for macOS. It watches the user's home folder so that when a file changes, we reindex just that file. Watching one project directory is a solved problem. Watching a real human's &lt;code&gt;$HOME&lt;/code&gt; is a different sport, and it broke us in a way that took a heap profiler to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;The app sat at roughly 25 GB of resident memory. Not a slow leak either. It climbed there shortly after launch and stayed. One core also pinned itself for about ten minutes at every boot.&lt;/p&gt;

&lt;p&gt;The obvious suspects were wrong. It was not the embedding model, and it was not our index. We had a careful ignore list (caches, &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;Library&lt;/code&gt;, build output), so surely we were not touching millions of files.&lt;/p&gt;

&lt;p&gt;We were not. But something else was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding
&lt;/h2&gt;

&lt;p&gt;We use the excellent &lt;code&gt;notify&lt;/code&gt; crate, specifically &lt;code&gt;notify-debouncer-full&lt;/code&gt;, which is what you reach for when you want raw filesystem events collapsed into something sane. The setup you copy from the README uses &lt;code&gt;RecommendedCache&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;RecommendedCache&lt;/code&gt; resolves to &lt;code&gt;FileIdMap&lt;/code&gt;. From the docs, the debouncer "can optionally keep track of the file system IDs all files and stitches rename events together". That is a genuinely useful feature: on macOS FSEvents and on Windows, a rename shows up as two unrelated events, and pairing them requires knowing that the file at the old path and the file at the new path are the same inode.&lt;/p&gt;

&lt;p&gt;To do that, it has to know the file ID of every file under the watch root. So it walks the entire watch root and caches a &lt;code&gt;(PathBuf, FileId)&lt;/code&gt; for every entry it finds.&lt;/p&gt;

&lt;p&gt;Here is the part that mattered: &lt;strong&gt;that walk does not know about your ignore rules.&lt;/strong&gt; Our ignore list filters &lt;em&gt;events&lt;/em&gt;. The cache is built underneath that, from the watch root down. We told the debouncer to watch &lt;code&gt;$HOME&lt;/code&gt;, so it faithfully walked all of &lt;code&gt;$HOME&lt;/code&gt;, including everything we thought we had excluded, following symlinks as it went.&lt;/p&gt;

&lt;p&gt;Under &lt;code&gt;malloc_history&lt;/code&gt;, the damage was specific:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;23.6 GB of live heap in the cache&lt;/li&gt;
&lt;li&gt;4.16 GB of that in the hashbrown table itself&lt;/li&gt;
&lt;li&gt;19.4 GB spread across 54.2 million path allocations&lt;/li&gt;
&lt;li&gt;roughly 6.4 million files stat-ed at boot, which is the pinned core and the ten minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;54 million allocations to remember the inode of files we had explicitly said we did not care about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, and what it costs
&lt;/h2&gt;

&lt;p&gt;We switched the cache to &lt;code&gt;NoCache&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is not free, and it is worth being precise about the tradeoff rather than pretending we outsmarted the library. &lt;code&gt;FileIdMap&lt;/code&gt; exists for a reason: with &lt;code&gt;NoCache&lt;/code&gt;, the debouncer can no longer stitch rename events together. A rename stops arriving as "this moved from A to B" and degrades into an unrelated delete at A and a create at B.&lt;/p&gt;

&lt;p&gt;We could absorb that, for two reasons that are specific to our design:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Our indexer is idempotent. Indexing the same file twice converges to the same row, so a spurious create is cheap and harmless.&lt;/li&gt;
&lt;li&gt;We already run a rescan and reconcile pass that removes index entries whose files no longer exist. Deletions get collected there regardless of whether the watcher paired them correctly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So a rename becomes delete-plus-create, and the system settles into the right state on its own. If your indexer is not idempotent, or you have no reconcile pass, this trade is not available to you and you should fix the scope instead.&lt;/p&gt;

&lt;p&gt;Result: 24.8 GB down to 1.45 GB.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second bug we found on the way
&lt;/h2&gt;

&lt;p&gt;While measuring, we caught something else. When FSEvents drops events (it does this under load, by design, and tells you so), the debouncer emits a rescan. Our code fanned that out as one rescan per &lt;em&gt;configured&lt;/em&gt; root.&lt;/p&gt;

&lt;p&gt;Our roots were nested. We watched &lt;code&gt;$HOME&lt;/code&gt;, and we also watched &lt;code&gt;Documents&lt;/code&gt;, &lt;code&gt;Downloads&lt;/code&gt;, and &lt;code&gt;Desktop&lt;/code&gt; because they were configured separately. So a single dropped event triggered four concurrent, roughly &lt;code&gt;$HOME&lt;/code&gt;-scale re-walks, and every file under the nested roots got embedded twice. You could see it plainly in the logs: two &lt;code&gt;EmbeddingsGenerated&lt;/code&gt; lines per document.&lt;/p&gt;

&lt;p&gt;The fix was to de-nest the roots into independent subtrees before emitting any rescan, so overlapping configuration collapses into one walk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The lesson worth stealing
&lt;/h2&gt;

&lt;p&gt;Library defaults are calibrated for the common case, and the common case is a project folder with a few thousand files. Nothing in the API warns you, because nothing is wrong with the API. &lt;code&gt;FileIdMap&lt;/code&gt; is correct. It is correct at 5,000 files and it is a memory bomb at 6.4 million, and the difference is entirely in what you point it at.&lt;/p&gt;

&lt;p&gt;So, two things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Know what your watcher caches before you widen the scope.&lt;/strong&gt; Filtering events is not the same as filtering what the library indexes internally. Ours sat below our filter, where we could not see it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure the heap, do not reason about it.&lt;/strong&gt; We would never have found 54.2 million path allocations by reading code. &lt;code&gt;malloc_history&lt;/code&gt; found it in one pass. Every hour we spent theorizing about the embedding model was an hour we did not spend attaching a profiler.&lt;/p&gt;

&lt;p&gt;If you are watching a user's whole home directory, assume every default in your stack was written by someone who was not.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>macos</category>
      <category>performance</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Point any app at a local LLM on your Mac (OpenAI-compatible endpoints)</title>
      <dc:creator>Jackson Ly</dc:creator>
      <pubDate>Fri, 10 Jul 2026 00:17:10 +0000</pubDate>
      <link>https://dev.to/jacksonxly/point-any-app-at-a-local-llm-on-your-mac-openai-compatible-endpoints-4o8l</link>
      <guid>https://dev.to/jacksonxly/point-any-app-at-a-local-llm-on-your-mac-openai-compatible-endpoints-4o8l</guid>
      <description>&lt;p&gt;Most apps that grew an "AI" feature in the last two years talk to one of a handful of cloud APIs, and almost all of them speak the same dialect: the OpenAI Chat Completions format. That one detail is the reason you can pull the cloud out and run the whole thing locally on a Mac without the app ever noticing.&lt;/p&gt;

&lt;p&gt;Here is the trick, why it works, and the gotchas that bite.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one interface everything agrees on
&lt;/h2&gt;

&lt;p&gt;OpenAI's &lt;code&gt;/v1/chat/completions&lt;/code&gt; endpoint became the de facto standard. So when an app lets you "use your own key" or "set a custom base URL," it is almost always going to POST to &lt;code&gt;{base_url}/chat/completions&lt;/code&gt; with a JSON body of messages and read back the same shape. It does not care what is on the other end, only that the response matches.&lt;/p&gt;

&lt;p&gt;Local runners leaned into this. Both popular Mac ones expose exactly that endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ollama&lt;/strong&gt; serves an OpenAI-compatible API at &lt;code&gt;http://localhost:11434/v1&lt;/code&gt; (its native API lives on &lt;code&gt;/api&lt;/code&gt;, but the &lt;code&gt;/v1&lt;/code&gt; path speaks the OpenAI dialect).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LM Studio&lt;/strong&gt; has a built-in server you switch on from the Developer tab, serving on &lt;code&gt;http://localhost:1234/v1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So "make this app local" usually reduces to: point its base URL at one of those, put any non-empty string where it wants an API key, and pick a model you have pulled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 60-second version
&lt;/h2&gt;

&lt;p&gt;Ollama:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;ollama        &lt;span class="c"&gt;# or the .dmg from ollama.com&lt;/span&gt;
ollama serve &amp;amp;             &lt;span class="c"&gt;# server on :11434&lt;/span&gt;
ollama pull llama3.1:8b    &lt;span class="c"&gt;# pull a model once&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm it speaks OpenAI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:11434/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "llama3.1:8b",
    "messages": [{"role": "user", "content": "say hi in 3 words"}]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that returns a &lt;code&gt;choices[0].message.content&lt;/code&gt;, any OpenAI-compatible client can use it. In the app, set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Base URL: &lt;code&gt;http://localhost:11434/v1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;API key: &lt;code&gt;ollama&lt;/code&gt; (or literally anything; it is ignored)&lt;/li&gt;
&lt;li&gt;Model: &lt;code&gt;llama3.1:8b&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LM Studio is the same idea with a GUI: load a model, toggle the server on, and use base URL &lt;code&gt;http://localhost:1234/v1&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pointing real tools at it
&lt;/h2&gt;

&lt;p&gt;The pattern shows up everywhere once you look for it. The official OpenAI SDKs are the clearest example: change one field.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OpenAI&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OpenAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:11434/v1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ollama&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llama3.1:8b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summarize this in one line: ...&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same code, cloud or local. Only the &lt;code&gt;base_url&lt;/code&gt; changed. In JavaScript it is the &lt;code&gt;baseURL&lt;/code&gt; option; in a lot of CLI and editor tools it is an &lt;code&gt;OPENAI_BASE_URL&lt;/code&gt; environment variable. Some end-user apps expose it too: DEVONthink 4, for instance, lets its Chat point at Ollama or LM Studio directly, so search and summarize run over your own documents with nothing leaving the machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest gotchas
&lt;/h2&gt;

&lt;p&gt;It is not always drop-in. The places it breaks, roughly in order of how often they catch people:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Context window.&lt;/strong&gt; A local model often defaults to a small context (Ollama defaults to 2048 tokens unless you raise &lt;code&gt;num_ctx&lt;/code&gt;). If an app sends a big document and the model only reads the last 2k tokens, it looks like the model "ignored" your content. Raise the context in the model config, or send smaller chunks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing features.&lt;/strong&gt; Some apps assume function calling, JSON mode, or vision. Not every local model or runner supports all of them, and the failure is often silent. Check the runner's compatibility notes for the specific feature you depend on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming quirks.&lt;/strong&gt; Most local servers stream fine, but a few clients expect particular SSE framing. If a response hangs, turn streaming off in the client as a test to isolate it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model quality.&lt;/strong&gt; A 7-8B model is not GPT-class. For classification, summarizing, tagging, and search over your own material it is often plenty; for long chains of reasoning it will disappoint. Match the job to the model rather than expecting parity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAM.&lt;/strong&gt; Everything is bounded by unified memory on a Mac. 16GB comfortably runs a capable 7-8B model; below that, stay in the 3B range and keep the context modest.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why bother
&lt;/h2&gt;

&lt;p&gt;Two reasons that actually matter. It is free after the download, so there is no per-token meter running while you iterate on a prompt fifty times. And nothing leaves the machine, which for anything sensitive (client data, personal notes, a private codebase) is the difference between "I can use AI on this" and "I am not allowed to."&lt;/p&gt;

&lt;p&gt;The best part is that because it is one interface, you do not have to commit. Keep a cloud base URL for the genuinely hard reasoning and a local one for the bulk, private, high-volume work, and switch between them by changing a single string.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with AI assistance and edited by a human. Endpoint details reflect public docs as of July 2026 and move quickly, so check each project's current docs for exact paths and defaults.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>mac</category>
      <category>productivity</category>
    </item>
    <item>
      <title>MLX vs GGUF on Apple Silicon: which local LLM format should you actually use?</title>
      <dc:creator>Jackson Ly</dc:creator>
      <pubDate>Tue, 07 Jul 2026 01:16:53 +0000</pubDate>
      <link>https://dev.to/jacksonxly/mlx-vs-gguf-on-apple-silicon-which-local-llm-format-should-you-actually-use-53gj</link>
      <guid>https://dev.to/jacksonxly/mlx-vs-gguf-on-apple-silicon-which-local-llm-format-should-you-actually-use-53gj</guid>
      <description>&lt;p&gt;If you run local models on a Mac, you eventually hit the same fork in the road: the same model is available as a GGUF file and as an MLX version, and something has to tell you which one to download. The short answer is that MLX is faster on Apple Silicon and GGUF goes everywhere. The useful answer is knowing when that trade actually matters, because for a lot of setups it does not.&lt;/p&gt;

&lt;p&gt;Here is the practical version, with the reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the two formats actually are
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GGUF&lt;/strong&gt; is a single self-contained file. Weights, tokenizer, metadata, and quantization parameters are all bundled into one portable blob that llama.cpp (and everything built on it) can load anywhere: Mac, Linux, Windows, CPU, CUDA, Metal. That portability is the whole point of the format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MLX&lt;/strong&gt; is Apple's array framework, not a file. An MLX model is a directory of safetensors files plus a config that the MLX runtime reads directly. It is built to run on Apple Silicon and quantize natively against the unified memory pool. It does not leave Apple Silicon, full stop.&lt;/p&gt;

&lt;p&gt;So this is not really "two file formats." It is one portable format and one Apple-native runtime that happen to ship the same model weights.&lt;/p&gt;

&lt;h2&gt;
  
  
  The performance gap is real but bounded
&lt;/h2&gt;

&lt;p&gt;On the same Mac, at the same quantization level, MLX runs roughly 15 to 40 percent faster than GGUF and uses about 10 percent less memory. The speedup comes from MLX being compiled for Apple Silicon and operating directly on the unified memory pool, where the CPU and GPU share one block of RAM and every gigabyte is usable as model memory.&lt;/p&gt;

&lt;p&gt;There is one quality nuance worth knowing. At 4-bit, GGUF's &lt;code&gt;Q4_K_M&lt;/code&gt; uses mixed precision inside each layer, so it can hold quality slightly better than a naive 4-bit MLX quant. It is a small difference, but if you are quantizing aggressively on a memory-tight machine, it is the kind of thing that shows up in outputs.&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;GGUF&lt;/th&gt;
&lt;th&gt;MLX&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it is&lt;/td&gt;
&lt;td&gt;Portable single file&lt;/td&gt;
&lt;td&gt;Apple framework, safetensors dir&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runs on&lt;/td&gt;
&lt;td&gt;Mac, Linux, Windows, CPU, CUDA, Metal&lt;/td&gt;
&lt;td&gt;Apple Silicon only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed on M-series&lt;/td&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;~15 to 40% faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;~10% less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4-bit quality&lt;/td&gt;
&lt;td&gt;Slightly better (Q4_K_M mixed precision)&lt;/td&gt;
&lt;td&gt;Fine, marginally behind at 4-bit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Portability, ecosystem, cross-platform&lt;/td&gt;
&lt;td&gt;Raw throughput on Apple Silicon&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Tool support is the part people get wrong
&lt;/h2&gt;

&lt;p&gt;The format you should pick is partly decided by the tool you already use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;LM Studio&lt;/strong&gt; has had an MLX backend since late 2024 and also runs GGUF through its bundled llama.cpp. So on LM Studio you genuinely get to choose per model, and picking the MLX build on an M-series chip is usually free speed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ollama&lt;/strong&gt; ran GGUF through llama.cpp's Metal path for a long time. Its 0.19 preview added an optional MLX backend that reports roughly double the old speed, but it targets Macs with 32GB or more of unified memory. On a 16GB machine you stay on the GGUF/Metal path, so the MLX question does not even come up for you yet.&lt;/p&gt;

&lt;p&gt;That 32GB line matters more than the raw benchmark. A lot of "MLX is 2x faster" claims quietly assume a machine most people do not have.&lt;/p&gt;

&lt;h2&gt;
  
  
  The portability catch that bites later
&lt;/h2&gt;

&lt;p&gt;MLX being Apple-only is fine until it is not. If you are building something that might need a CUDA fallback, a Linux server, or a Windows user, and you shipped MLX as your only build, you are re-quantizing under pressure the day that requirement lands. GGUF would have just run.&lt;/p&gt;

&lt;p&gt;This is the real decision axis, and it is not about speed. It is about how long your setup has to live and where it has to run. A weekend project that only ever runs on your own MacBook has no reason not to take the MLX speedup. Infrastructure that will outlive any single runtime should default to GGUF, or ship both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision, compressed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Personal use on an M-series Mac with 32GB or more:&lt;/strong&gt; take MLX. It is faster and lighter and you lose nothing you will miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;16GB Mac, or you just want the simplest path:&lt;/strong&gt; GGUF. You are likely on the GGUF path anyway, and the portability is free insurance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform, a server, or anything that might need CUDA later:&lt;/strong&gt; GGUF, or build both. Do not paint yourself into an Apple-only corner for a 20 percent speedup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You are not sure:&lt;/strong&gt; GGUF. It runs everywhere, and you can always add the MLX build later when you know you want it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The honest summary is that both are good, and on a modern Mac you can usually try each in a couple of minutes with the same tool. Speed is the easy axis to measure and the wrong one to optimize first. Portability is the one you only notice when it is gone.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with AI assistance and edited by a human. Format details reflect public information as of July 2026 and move quickly (Ollama's MLX backend in particular is young), so check each tool's current release notes before you rely on a specific number.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>apple</category>
    </item>
    <item>
      <title>What belongs in an agent's context window (and what to evict)</title>
      <dc:creator>Jackson Ly</dc:creator>
      <pubDate>Mon, 06 Jul 2026 11:37:07 +0000</pubDate>
      <link>https://dev.to/jacksonxly/what-belongs-in-an-agents-context-window-and-what-to-evict-16ca</link>
      <guid>https://dev.to/jacksonxly/what-belongs-in-an-agents-context-window-and-what-to-evict-16ca</guid>
      <description>&lt;p&gt;Most "agent memory" writeups are about getting things in: which vector store, how to chunk, how to embed. The harder question in practice is the opposite one. Your context window is a fixed budget. Every token you spend on a stale tool output is a token you did not spend on the thing that actually decides the next step. So the real engineering problem is not retrieval, it is eviction: what earns a place in context, and what gets dropped or compressed.&lt;/p&gt;

&lt;p&gt;Here is the model that has held up for me across a few agent builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context is a working set, not a memory
&lt;/h2&gt;

&lt;p&gt;Borrow the term from operating systems. The working set is the small slice of memory a process actually touches right now. Everything else lives on disk and gets paged in on demand. An agent's context window is a working set, and treating it like long-term storage is the mistake that makes agents slow, expensive, and confused.&lt;/p&gt;

&lt;p&gt;Durable memory (the vector store, the notes, the logs) is your disk. It can be huge. The context window is RAM: small, hot, and contested. The job of "agent memory" is mostly the pager: deciding what to bring into the working set for this step and what to let fall back to disk.&lt;/p&gt;

&lt;p&gt;Once you see it this way, most context bloat is a pager bug. You retrieved 40 chunks because they matched, kept all 40 in context for the rest of the session, and now every subsequent step pays attention over 40 things that stopped being relevant 10 steps ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two axes, not one
&lt;/h2&gt;

&lt;p&gt;The thing that made my eviction decisions cleaner was realizing they run on two independent axes, and people collapse them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fidelity: must stay exact vs can be approximated.&lt;/strong&gt; Some content breaks if you compress it. A commitment the agent made ("I will not touch prod"), a tool's exact error string, an ID, the user's literal instruction. Others degrade gracefully: a long document you only need the gist of, an old reasoning trace, the transcript of a resolved subtask. Summarize the second kind aggressively. Never summarize the first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recency vs salience.&lt;/strong&gt; Recency is easy and mostly wrong on its own. The token that matters is not the most recent one, it is the most salient one for the current step: the retrieved fact this decision hinges on, the constraint that must not be violated, the goal. Recent context is one bucket of salient content, not the definition of it.&lt;/p&gt;

&lt;p&gt;Cross the two axes and you get a priority you can actually implement. High-salience-must-stay-exact is sacred: keep it verbatim, evict last. Low-salience-can-approximate is the first thing you compress or drop. The dangerous quadrant is high-fidelity-but-low-salience: exact content you are paying full price to keep, that is not helping this step. That is where most of the waste lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete eviction policy
&lt;/h2&gt;

&lt;p&gt;You do not need anything fancy to start. A policy that beats "keep everything until the window overflows":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pin the invariants.&lt;/strong&gt; Goal, hard constraints, the user's literal ask. These never get evicted or summarized. Small, always present.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the last N turns raw.&lt;/strong&gt; Recency is a cheap proxy for salience for conversational coherence. N is small, like 3 to 6.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demote retrieved evidence after it is used.&lt;/strong&gt; A chunk that informed a step gets summarized to its one-line claim plus a pointer back to durable memory. If a later step needs the detail, page it back in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compress resolved subtasks to their outcome.&lt;/strong&gt; The full transcript of "figured out the config" becomes "config is X, verified." The reasoning got you there, but it is not load-bearing anymore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evict on salience, not age.&lt;/strong&gt; When you need room, drop the lowest-salience item, not the oldest. An old invariant outranks a fresh but irrelevant tool dump.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pointer-back part is what makes this safe. You are not deleting information, you are moving it from RAM to disk with a way to fault it back. Summarization without a pointer is just lossy deletion, and that is how agents "forget" things they clearly should have known.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode to watch
&lt;/h2&gt;

&lt;p&gt;The subtle bug is trusting your own summaries. When you compress retrieved evidence into a one-line claim, that claim is now a memory the agent will act on, and it was written by an LLM that may have smoothed over the caveat that mattered. Keep the pointer to the source, and when a step is high-stakes, page the raw evidence back in instead of trusting the summary. Fidelity and salience decide what to keep; stakes decide when to distrust your own compression.&lt;/p&gt;

&lt;p&gt;None of this needs a new framework. It needs you to stop treating the context window as storage and start treating it as a scheduler: a small, contested resource where the real skill is deciding what to page out.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with AI assistance and edited by a human. I think about this because I build a local-first personal memory layer, where the "disk" is your own captured work and the pager runs on your machine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>programming</category>
      <category>llm</category>
    </item>
    <item>
      <title>What poisoning a RAG store taught us about agent memory</title>
      <dc:creator>Jackson Ly</dc:creator>
      <pubDate>Mon, 06 Jul 2026 04:37:45 +0000</pubDate>
      <link>https://dev.to/jacksonxly/what-poisoning-a-rag-store-taught-us-about-agent-memory-3cl5</link>
      <guid>https://dev.to/jacksonxly/what-poisoning-a-rag-store-taught-us-about-agent-memory-3cl5</guid>
      <description>&lt;p&gt;Most of the "AI memory" conversation is about getting things in: chunking, embeddings, which vector database, how to compress a long chat into something you can recall later. Almost none of it is about what happens when one of those memories is wrong on purpose. That is the part we cannot stop thinking about while building a local-first personal memory layer, because an assistant that remembers you is also an assistant that can be talked into remembering a lie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not try to detect a poisoned memory. Gate what an unproven memory is allowed to &lt;em&gt;do&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;"Two independent sources" is the retrieval spelling of an old, unsolved problem: permissionless membership. Independence is not free.&lt;/li&gt;
&lt;li&gt;Standing earned from outcomes beats standing bought with identity, but the agent cannot grade its own homework.&lt;/li&gt;
&lt;li&gt;A per-action cap does not stop a slow drip. You need a cumulative budget plus a reversible human gate on the irreversible tail.&lt;/li&gt;
&lt;li&gt;Everything rests on one axis nobody bounds: provenance that survives transformation. That is the floor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the writeup of a public thread on r/LangChain that turned into one of the best technical exchanges we have had in a while. It started with a builder (the maintainer of an open-source memory layer, mnemo / agora-mnemo) doing something more people should do: he tried to poison his own RAG store and reported honestly on what held and what did not. Over about a week he built each idea, measured it, tried to break it, and shipped the parts that survived. Credit for the experiments and the shipped code is his. Our part was mostly the framing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do retrieval-time poison defenses fail?
&lt;/h2&gt;

&lt;p&gt;The naive defense against a poisoned document is to detect the poison: compute perplexity, look for outliers, flag the weird chunk. This is the wall that keeps failing, and it fails for a boring reason. It lives in embedding space, so it breaks the moment you swap the encoder. His results showed exactly that. A defense tuned on one embedder fell over on the next.&lt;/p&gt;

&lt;p&gt;The reframe that travels further: you did not build a poison detector, you rediscovered that &lt;strong&gt;retrieval and authority are two different privileges&lt;/strong&gt;, and only the second one has to be earned. In most memory stacks they are the same thing. If a chunk comes back in the top-k, it gets to shape the answer. Separate them. Let untrusted content &lt;em&gt;inform&lt;/em&gt; the model, and never let it &lt;em&gt;authorize&lt;/em&gt; an action on its own. That is the same split that holds up best against prompt injection (the dual-LLM, capabilities pattern in Google DeepMind's CaMeL work): the privileged planner never touches raw untrusted text, and untrusted values only reach a sensitive tool call through a typed capability a policy has to allow.&lt;/p&gt;

&lt;p&gt;Retrieval is access control's easy half. The hard half is information flow: a value admitted for a cheap read can still poison the &lt;em&gt;context&lt;/em&gt; a trusted action later consumes. The fix is not a better authority tier, it is a low-water mark. An action inherits the minimum integrity of everything in its context, so the instant a single-source, unproven memory enters, the action's effective integrity drops and it fails closed. If one poisoned read does not drag the whole action down, your labels are not propagating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why are "independent sources" not actually free?
&lt;/h2&gt;

&lt;p&gt;Once you gate on authority, the tempting rule is "trust a claim if at least two independent sources agree." The word doing all the work is &lt;em&gt;independent&lt;/em&gt;. If an attacker can mint sources cheaply (new domains, new document ids, new ingest times), corroboration collapses into a Sybil attack: forge three records, forge the independence, graduate your own poison. He measured it. Two freshly registered domains supplying their own corroboration sail through the count gate.&lt;/p&gt;

&lt;p&gt;This is not a RAG quirk. "Two independent sources" is the retrieval spelling of &lt;strong&gt;permissionless membership&lt;/strong&gt;, and it has been open for decades. Byzantine agreement is safe up to a fraction of faulty replicas &lt;em&gt;only if the replicas are actually independent&lt;/em&gt;, and that independence was always assumed out of band. The only ways anyone has ever gotten it without a trusted registrar are to make identity scarce: proof of work, stake, or a real-world attestation. So the design question stops being "is this chunk poisoned" (encoder-dependent, keeps breaking) and becomes "what makes a source costly to mint" (a membership-cost question people have studied for twenty years).&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually survived the experiments?
&lt;/h2&gt;

&lt;p&gt;He built four ways to make a corroborating source costly (an allowlist or registrar, signed attestation, proof of work, and staked-with-decay standing) and measured each against a forged multi-source poison. The honest result sorts cleanly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cost alone only prices the attack, it does not wall it.&lt;/strong&gt; Proof of work and staking are symmetric taxes: the honest writer pays the same, and by a classic result (Cheng and Friedman, 2005) no symmetric reputation function can be Sybil-proof.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What helps is asymmetric and rooted outside the system.&lt;/strong&gt; Standing earned from &lt;em&gt;outcomes&lt;/em&gt; (did acting on this memory turn out well) is the only one of the four with an asymmetric root. A wrong poison cannot earn a good outcome.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;But the agent cannot grade its own homework.&lt;/strong&gt; If the outcome signal is endogenous (an attack like MINJA gets the agent to write its own corroboration), the whole thing collapses. The only unforgeable outcome is a real consequence the attacker actually pays, and you can only afford to observe that on actions you were willing to be wrong about. So outcome-learning and action-gating are the same boundary: you earn standing on the reversible set, and high-stakes actions never learn, they only get gated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is a density catch worth stating plainly: only re-used memory earns standing, so the cold long tail never accrues it and lives under the cap permanently. Standing-gating is a hot-path privilege, not a global one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attack that quietly defeats a per-action cap
&lt;/h2&gt;

&lt;p&gt;The residual we thought we had was "one patient sleeper, single shot, under a blast-radius cap." That was wrong, and it is the correction we would most want you to take away. A per-item cap is silent on a &lt;em&gt;run&lt;/em&gt; of individually reversible writes that sum past the reversibility threshold. That is salami-slicing, and he measured a version compounding into an irreversible outcome. The only scalar that bounds it is a &lt;strong&gt;cumulative per-source budget&lt;/strong&gt;, not a per-action one. The only lever that bites a patient attacker (who just amortizes a small cap over a long horizon) is &lt;strong&gt;retroactive forfeiture&lt;/strong&gt;: accrued standing becomes a bond you burn on a caught defection, so effective cost scales with one over the probability of getting caught, not with the cap.&lt;/p&gt;

&lt;p&gt;Ship those and you get the honest shape of the whole problem: you do not remove the residual, you price it, bound it, and put a reversible human gate on the irreversible tail. Auto-forfeiture stays off by default, because on an attacker-influenceable oracle an automatic penalty is a framing weapon (feed bad outcomes attributed to a rival, get the rival auto-slashed). The human is not there to be the truth oracle. They are there to keep forfeiture rare and reversible, which is the only property that defangs the framing attack.&lt;/p&gt;

&lt;h2&gt;
  
  
  The floor everything stands on
&lt;/h2&gt;

&lt;p&gt;Strip the layers down and they all quietly ride on one axis nobody had bounded: &lt;strong&gt;attribution&lt;/strong&gt;. The detector, the budget, and the gate all assume you can correctly assign an action to its source and label whether it is irreversible. A single relabel does not degrade the other three, it voids all of them at once and silently, because they are all keyed on the same source id. Attribution is not a fourth control. It is the floor the other three stand on.&lt;/p&gt;

&lt;p&gt;You can make attribution &lt;em&gt;tamper-evident&lt;/em&gt; with textbook cryptography (hash-chained receipts, in the Haber and Stornetta lineage), so a post-hoc relabel stops being silent. What you cannot get from inside a single system is attribution you can &lt;em&gt;trust at write time&lt;/em&gt;: if the attacker controls the labeling channel, the label's truth is exactly the thing they control. That only moves if you bind the label to something the attacker does not hold at write time (origin-signed provenance, or independent corroboration of the world), at which point "can I trust the label" becomes "can I trust the root," which is the identity question again. The tower terminates on exactly one thing you choose to trust from outside, and everything above it is mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this lands for a personal, local-first brain
&lt;/h2&gt;

&lt;p&gt;We are building recal as a local-first personal memory layer: something that watches how you actually work and can act on it, on your own machine, behind an approve-gate you control. Personal memory raises the stakes on every point above. The "sources" are your own captured activity and notes, the "irreversible actions" are things done on your behalf, and there is no cloud moderation team to catch a bad memory for you. The takeaway we are designing around is the one this thread earned: do not detect bad memories, gate what an unproven memory is allowed to do, keep the destructive tail behind a reversible human gate, and treat provenance-that-survives-transformation as the substrate the whole thing rests on. "Solved" was always the wrong word. Turning an encoder-resetting arms race into a bounded, priced, auditable risk is the shippable win.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Does a knowledge graph or better vector database fix RAG poisoning?&lt;/strong&gt;&lt;br&gt;
No. Both are retrieval improvements, and the failure is that retrieval is treated as authority. The fix is to separate the two: retrieve widely, but let only corroborated, provenance-tracked memory drive an action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is detecting AI-generated or poisoned text a viable defense?&lt;/strong&gt;&lt;br&gt;
On its own, no. Detection lives in embedding space and resets every time the encoder changes. Gating what a memory is allowed to do generalizes across encoders in a way detection does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the single most important primitive to build first?&lt;/strong&gt;&lt;br&gt;
Provenance that survives transformation (chunking, summarization, re-embedding). Every other control (budgets, standing, slashing) is keyed on knowing which source an action came from, so attribution is the floor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does this require a blockchain?&lt;/strong&gt;&lt;br&gt;
No. The useful cryptography here is boring and old: hash-chained, tamper-evident receipts. The hard, still-open part is not tamper-evidence, it is trusting a label at write time, which needs an external signing root, not a consensus network.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with AI assistance and edited by a human. This is the synthesis of a real public thread on r/LangChain; the experiments and shipped code are the mnemo / agora-mnemo maintainer's, and the framing was developed collaboratively in the open. Technical claims (CaMeL, Cheng and Friedman 2005, MINJA, Sybil / Douceur 2002, Haber and Stornetta) were checked against their primary sources.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>security</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Why I built my Mac assistant to run 100% on-device (and what local-first actually cost me)</title>
      <dc:creator>Jackson Ly</dc:creator>
      <pubDate>Mon, 29 Jun 2026 06:44:55 +0000</pubDate>
      <link>https://dev.to/jacksonxly/why-i-built-my-mac-assistant-to-run-100-on-device-and-what-local-first-actually-cost-me-1imc</link>
      <guid>https://dev.to/jacksonxly/why-i-built-my-mac-assistant-to-run-100-on-device-and-what-local-first-actually-cost-me-1imc</guid>
      <description>&lt;p&gt;I'm building a proactive personal assistant for the Mac called recal. It watches how I work, learns my patterns, and starts doing the repetitive busywork for me. The one constraint I refused to bend on: it runs entirely on-device. No cloud, no account, no server. 0 bytes of my activity leave the machine.&lt;/p&gt;

&lt;p&gt;This is the honest engineering version of that decision: why I made it, what it bought me, and what it genuinely cost. I'm the founder, building in public, and the product is pre-launch, so this is about the architecture, not a sales pitch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy by architecture, not by policy
&lt;/h2&gt;

&lt;p&gt;Every AI productivity tool I tried wanted the same thing: my screen, my files, my activity history, living on someone else's server. For a tool whose entire job is to watch how I work all day, "trust our privacy policy" was never going to be enough. A policy is a promise. Architecture is a guarantee.&lt;/p&gt;

&lt;p&gt;If the data never leaves the device, there is no server to breach, no account to leak, no policy to quietly change next quarter. You can pull the ethernet cable and it still works. That is a fundamentally different security model than "we encrypt it in transit."&lt;/p&gt;

&lt;p&gt;The catch is that you have to actually build for it, and "local-first" stops being a marketing word the moment you hit the real engineering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture is the scariest part, so it is default-deny
&lt;/h2&gt;

&lt;p&gt;A tool that observes your work is one bad decision away from being spyware. The Microsoft Recall backlash made that vivid for everyone. So the capture layer is built default-deny: it does not record unless a specific, allowed signal says it is safe to.&lt;/p&gt;

&lt;p&gt;The model I kept coming back to is Helen Nissenbaum's contextual integrity: information is not simply private or public, it is appropriate or inappropriate to a context. A password field, a banking tab, an incognito window are contexts where capture is never appropriate, no matter how useful the data would be. So sensitivity is a first-class tag on every observed event, decided at capture time, and anything uncertain gets dropped rather than stored. The expensive, paranoid default is the correct one here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local retrieval without a cloud index
&lt;/h2&gt;

&lt;p&gt;The fun objection to local-first AI is "but the good models live in data centers." True, and I am not pretending a Mac runs a frontier model. But most of what a personal assistant needs is not a giant model, it is your own context, retrievable fast.&lt;/p&gt;

&lt;p&gt;That part runs entirely on-device: content gets embedded with a small ONNX model running locally, and retrieval is a mix of embeddings, recency, and plain filters. For personal re-finding ("what was I looking at last Tuesday before the meeting"), that combination beats a cloud round-trip, and it never ships your life to an index you do not control. The thing that surprised me: you need far less model than the industry implies, as long as you are retrieving over your own data instead of trying to reason about the whole world.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I care about most: it proposes, I approve
&lt;/h2&gt;

&lt;p&gt;"It did something on its own" is terrifying the first time it is wrong. So recal never acts on its own. It does the work in the background, then surfaces a finished result for one tap: approve, or do not.&lt;/p&gt;

&lt;p&gt;That approve-gate is not a UX afterthought, it is the trust contract. It keeps a human as the one deciding while the machine does the doing. It also makes a wrong proposal cheap (you reject it and move on) instead of catastrophic (it already emailed the wrong person). Building the gate first, before the autonomy, was the single best product decision I made.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cost
&lt;/h2&gt;

&lt;p&gt;Being honest, because that is the point of building in public.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No giant cloud model to lean on. Anything that genuinely needs frontier-scale reasoning has to be designed around, deferred, or made opt-in, and you feel that constraint daily.&lt;/li&gt;
&lt;li&gt;Capture you can trust is slow to build. Default-deny means you are constantly saying no to data you would love to have, and writing the rules that decide "appropriate" is most of the work.&lt;/li&gt;
&lt;li&gt;You carry the whole stack. There is no server to hotfix. The observation engine, the index, and the agent all ship to the user's machine in Swift and Rust, and a bug is a bug on their hardware, not a deploy you roll back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Would I do it again? Yes, without hesitation. The cost is real, but what I get back is a tool I would actually trust to watch my whole working life, which is the only kind worth building.&lt;/p&gt;

&lt;p&gt;If the on-device, local-first direction resonates, I am building recal in public and there is a waitlist at &lt;a href="https://recal.so" rel="noopener noreferrer"&gt;recal.so&lt;/a&gt;. But mostly I wanted to write the architecture down honestly, because I think more personal software should be built this way, and I would genuinely like to hear how others are drawing the same lines.&lt;/p&gt;

</description>
      <category>buildinpublic</category>
      <category>showdev</category>
      <category>privacy</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
