<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Macos on Man-You</title><link>https://man-you.ringum.net/tags/macos/</link><description>Recent content in Macos on Man-You</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Mon, 20 Apr 2026 10:00:00 +0200</lastBuildDate><atom:link href="https://man-you.ringum.net/tags/macos/index.xml" rel="self" type="application/rss+xml"/><item><title>Higgins: a neurosymbolic menubar buddy</title><link>https://man-you.ringum.net/posts/higgins/</link><pubDate>Mon, 20 Apr 2026 10:00:00 +0200</pubDate><guid>https://man-you.ringum.net/posts/higgins/</guid><description>&lt;p&gt;Higgins is a local assistant that lives in the menubar. A Qwen 2.5 7B model runs on-device via MLX, a SQLite triple store remembers things across sessions, and a small collection of tools — AppleScript runners, EventKit bridges, &lt;code&gt;gh&lt;/code&gt; CLI wrappers — lets him actually do work on the Mac. No cloud, no subscription, no data leaving the machine.&lt;/p&gt;
&lt;p&gt;The name is a nod to Magnum P.I.&amp;rsquo;s reserved English major-domo. The vibe was aspirational; most of the engineering effort went into making sure he doesn&amp;rsquo;t cheerfully hallucinate your dentist appointment into 2023.&lt;/p&gt;
&lt;h2 id="why-not-just-a-chat-wrapper"&gt;Why not just a chat wrapper&lt;/h2&gt;
&lt;p&gt;The starting pitch was neuro-symbolic: &amp;ldquo;LLMs are great at vibes and terrible at strict logic; symbolic systems are the opposite; put a logic engine inside a neural one.&amp;rdquo; Nice in a blog post, vague as a project.&lt;/p&gt;
&lt;p&gt;What it actually means in code: the neural side (Qwen) handles language, intent, paraphrasing. The symbolic side (SQLite facts table, typed tool schemas, EventKit queries) handles everything where being wrong is worse than being silent — dates, names, calendar events, PR numbers. The LLM never gets to compute a date from priors; it forwards your words to a parser. The LLM never claims to remember your dog&amp;rsquo;s name; the fact comes from a SQL row, or it doesn&amp;rsquo;t answer.&lt;/p&gt;
&lt;p&gt;That split is the whole project. Everything else is plumbing.&lt;/p&gt;
&lt;h2 id="the-qwen-brain"&gt;The Qwen brain&lt;/h2&gt;
&lt;p&gt;Qwen 2.5 7B Instruct, 4-bit quantized, ~4.5 GB on disk. Loaded via &lt;a href="https://github.com/ml-explore/mlx-swift-lm"&gt;mlx-swift-lm&lt;/a&gt; with progress reported through a &lt;code&gt;NSKeyValueObservation&lt;/code&gt; on the download&amp;rsquo;s &lt;code&gt;Progress&lt;/code&gt; object — since &lt;code&gt;swift-huggingface&lt;/code&gt; fires the handler once then mutates in place, not on every byte.&lt;/p&gt;
&lt;p&gt;Picking Qwen over Gemma was almost incidental. Gemma 3 4B worked but wouldn&amp;rsquo;t accept a &lt;code&gt;system&lt;/code&gt; role, forcing a hacky primer-pair trick. Gemma 4 26B-a4b is MoE, which mlx-swift-lm&amp;rsquo;s &lt;code&gt;Gemma4Model&lt;/code&gt; doesn&amp;rsquo;t implement yet. Qwen accepts system prompts cleanly and, crucially, was trained to emit native tool calls — which matters a lot more than benchmark scores.&lt;/p&gt;
&lt;p&gt;The old path was prompt-engineered: &lt;code&gt;TOOL_CALL toolname {...}&lt;/code&gt; in the output, parsed with regex. Fragile. Qwen wants to emit something like:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;lt;tool_call&amp;gt;
{&amp;#34;name&amp;#34;: &amp;#34;reminder_add&amp;#34;, &amp;#34;arguments&amp;#34;: {&amp;#34;text&amp;#34;: &amp;#34;…&amp;#34;, &amp;#34;datetime&amp;#34;: &amp;#34;…&amp;#34;}}
&amp;lt;/tool_call&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;mlx-swift-lm surfaces this as &lt;code&gt;.toolCall(ToolCall)&lt;/code&gt; events in the generation stream, if you pass structured tool specs into &lt;code&gt;UserInput(chat:, tools:)&lt;/code&gt;. Once wired up, the model stops inventing its own format and starts using the one it was RL&amp;rsquo;d on.&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;Generation loop excerpt&lt;/figcaption&gt;
&lt;div class="highlight" title="Generation loop excerpt"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;gen&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;MLXLMCommon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;gen&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;c&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;The text parser stuck around as a fallback for edge cases.&lt;/p&gt;
&lt;h2 id="the-symbolic-side"&gt;The symbolic side&lt;/h2&gt;
&lt;p&gt;A SQLite file at &lt;code&gt;~/Library/Application Support/Symbolic/memory.db&lt;/code&gt; with two tables:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;episodes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;user_msg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;assistant_reply&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;REAL&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;facts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;predicate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;REAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_seen&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;REAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;UNIQUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;Episodes are the raw log: every chat turn lands here. Facts are the distilled, queryable form — subject/predicate/object triples with a confidence score. The interesting work happens in the pipe between the two.&lt;/p&gt;
&lt;h2 id="sleep"&gt;Sleep&lt;/h2&gt;
&lt;p&gt;The consolidation metaphor is cribbed directly from human memory: episodic hippocampal traces don&amp;rsquo;t become semantic knowledge until you sleep. Higgins does the same. After 15 minutes of chat silence, or when you tap the 🌙 button, he runs a pass:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;NREM&lt;/strong&gt;: the model reads the day&amp;rsquo;s recent episodes and emits one JSON object per line — extracted stable facts about you, your world, your preferences. Ephemeral conversational filler (greetings, acknowledgments, weather chat) is skipped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Synaptic downscaling&lt;/strong&gt;: every fact&amp;rsquo;s confidence is multiplied by 0.95. Unused memories decay. Recalled ones get re-bumped above 1.0 and capped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Purge&lt;/strong&gt;: facts below 0.05 confidence are dropped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dream log&lt;/strong&gt;: a human-readable markdown summary lands in &lt;code&gt;~/Library/Application Support/Symbolic/dreams/YYYY-MM-DD.md&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;Sleeper entrypoint&lt;/figcaption&gt;
&lt;div class="highlight" title="Sleeper entrypoint"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;nightCycle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;episodeLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Int&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;async&lt;/span&gt; &lt;span class="kr"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Report&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;episodes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recentEpisodes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;episodeLimit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;facts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;extractFacts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;episodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;facts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addFact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;object&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decayFacts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multiplier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;purged&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;purgeWeakFacts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;There&amp;rsquo;s no &lt;code&gt;remember&lt;/code&gt; command to learn. Facts accumulate while you use the thing.&lt;/p&gt;
&lt;h2 id="per-turn-recall-closing-the-loop"&gt;Per-turn recall: closing the loop&lt;/h2&gt;
&lt;p&gt;Sleep would be useless if the facts just piled up. Before each generation, the user&amp;rsquo;s query is keyword-matched against the &lt;code&gt;facts&lt;/code&gt; table (top 5 hits, &lt;code&gt;LIKE&lt;/code&gt; on subject/predicate/object). Matching facts are injected into &lt;em&gt;that turn&amp;rsquo;s&lt;/em&gt; system context as a bracketed prefix:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;[Known facts from memory:
- dog / name / Rex (confidence 0.95)
- manz / employer / Woosmap (confidence 0.87)
]
what&amp;#39;s my dog&amp;#39;s name?&lt;/code&gt;&lt;/pre&gt;
&lt;/figure&gt;
&lt;p&gt;History stays clean — the decoration only exists for the single inference call. No tool call needed for the model to answer &amp;ldquo;Rex&amp;rdquo;. The symbolic substrate grounds the neural output on the way past.&lt;/p&gt;
&lt;p&gt;At low scale (hundreds of facts) &lt;code&gt;LIKE&lt;/code&gt; matching is good enough. Embedding-backed similarity search is the upgrade when it isn&amp;rsquo;t.&lt;/p&gt;
&lt;h2 id="tools"&gt;Tools&lt;/h2&gt;
&lt;p&gt;The tool layer is where the project actually earns its keep. Three surfaces:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AppleScript library.&lt;/strong&gt; Curated scripts live in &lt;code&gt;~/Library/Application Support/Symbolic/scripts/&lt;/code&gt;, each with frontmatter metadata (&lt;code&gt;-- name:&lt;/code&gt;, &lt;code&gt;-- description:&lt;/code&gt;, &lt;code&gt;-- args:&lt;/code&gt;). Seed scripts ship with the app; the user can edit or add more. The model calls them by name — &lt;code&gt;calendar_today&lt;/code&gt;, &lt;code&gt;note_quick&lt;/code&gt;, etc. — as if they were first-class tools.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;EventKit bridge.&lt;/strong&gt; AppleScript against Calendar.app is infamously slow — iterating every event on every calendar through the bridge takes 30+ seconds for a busy calendar. EventKit queries the same data in milliseconds. The catch: it needs &lt;code&gt;NSCalendarsFullAccessUsageDescription&lt;/code&gt; in a real &lt;code&gt;Info.plist&lt;/code&gt;. The SPM executable embeds one via a linker trick:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;Package.swift&lt;/figcaption&gt;
&lt;div class="highlight" title="Package.swift"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;linkerSettings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unsafeFlags&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;-Xlinker&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;-sectcreate&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;-Xlinker&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;__TEXT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;-Xlinker&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;__info_plist&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;-Xlinker&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;Resources/Info.plist&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;code&gt;-sectcreate __TEXT __info_plist&lt;/code&gt; injects the plist into a section TCC reads when deciding whether to prompt for permission. Works without wrapping the binary in a &lt;code&gt;.app&lt;/code&gt; bundle.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;gh&lt;/code&gt; CLI wrappers.&lt;/strong&gt; &lt;code&gt;gh_my_prs&lt;/code&gt;, &lt;code&gt;gh_review_queue&lt;/code&gt;, &lt;code&gt;gh_notifications&lt;/code&gt;, &lt;code&gt;gh_recent_merged&lt;/code&gt;. Subprocess calls to the user&amp;rsquo;s existing &lt;code&gt;gh&lt;/code&gt; session — no OAuth dance, no token storage. A &lt;code&gt;ProcessRunner&lt;/code&gt; actor handles timeouts and respects &lt;code&gt;Task.isCancelled&lt;/code&gt; so a stuck subprocess doesn&amp;rsquo;t freeze the UI when you hit the red stop button.&lt;/p&gt;
&lt;p&gt;Tool results render as folded cards with a wrench icon, tool name, and a one-line summary. Click to expand. Errors auto-expand. &lt;code&gt;#123 · PR title · owner/repo&lt;/code&gt; links are real anchors thanks to &lt;a href="https://github.com/gonzalezreal/swift-markdown-ui"&gt;MarkdownUI&lt;/a&gt; on the assistant-side rendering.&lt;/p&gt;
&lt;h2 id="dates-or-why-the-model-doesnt-do-arithmetic"&gt;Dates, or: why the model doesn&amp;rsquo;t do arithmetic&lt;/h2&gt;
&lt;p&gt;7B models are bad at date math. &amp;ldquo;Remind me tomorrow at 8:15&amp;rdquo; reliably produced ISO strings from 2023, 2024, anywhere but &lt;em&gt;actually tomorrow&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The fix is to not let the model do arithmetic at all. Any tool argument named &lt;code&gt;datetime&lt;/code&gt;, &lt;code&gt;when&lt;/code&gt;, &lt;code&gt;due_date&lt;/code&gt;, etc. gets intercepted in Swift before dispatch. A tiny table handles English and French relative words (&lt;code&gt;tomorrow&lt;/code&gt;, &lt;code&gt;today&lt;/code&gt;, &lt;code&gt;demain&lt;/code&gt;, &lt;code&gt;hier&lt;/code&gt;) — &lt;code&gt;NSDataDetector&lt;/code&gt; isn&amp;rsquo;t locale-parameterizable and picks up the system locale, which fails on English keywords under a French macOS. Anything the table doesn&amp;rsquo;t catch falls through to &lt;code&gt;NSDataDetector&lt;/code&gt;, which anchors relative phrasing against &lt;code&gt;Date()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Critically, the user&amp;rsquo;s raw message is the source of truth, not the model&amp;rsquo;s output. If you typed &amp;ldquo;tomorrow&amp;rdquo;, &amp;ldquo;tomorrow&amp;rdquo; is what gets parsed — even if the model hallucinates an ISO. The model&amp;rsquo;s job is intent extraction, not date reasoning.&lt;/p&gt;
&lt;h2 id="the-applescript-run-button"&gt;The AppleScript Run button&lt;/h2&gt;
&lt;p&gt;When the model writes code in an assistant reply, the code block renders with a language tag and a copy button. When the language is &lt;code&gt;applescript&lt;/code&gt;, a Run button appears:&lt;/p&gt;
&lt;figure class="code-block-figure"&gt;
&lt;figcaption&gt;CodeBlock with conditional run action&lt;/figcaption&gt;
&lt;div class="highlight" title="CodeBlock with conditional run action"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-swift" data-lang="swift"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;MarkdownUI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Markdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;turn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;markdownBlockStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;codeBlock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CodeBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;code&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;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;language&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;language&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;onRun&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;runAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&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;language&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&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;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;p&gt;The action shells out to &lt;code&gt;osascript -e &amp;lt;script&amp;gt;&lt;/code&gt;, captures stdout, appends the result as a tool turn. The user stays in charge — nothing runs without the click. Safer than giving the model unrestricted automation tools, more flexible than the curated script library.&lt;/p&gt;
&lt;h2 id="what-grew-out-of-it"&gt;What grew out of it&lt;/h2&gt;
&lt;p&gt;The original plan was to fine-tune a custom voice — a &amp;ldquo;genius caveman&amp;rdquo; Grug, all short fragments. Hours of LoRA experiments on Gemma 2 2B, Gemma 3 4B, Gemma 4 E4B later, the honest conclusion: voice tuning on small models produces caricature or collapse, and nobody cares about voice when the tools don&amp;rsquo;t work. The pivot was embracing stock Qwen and pouring the effort into the symbolic substrate instead.&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s there now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;On-device Qwen 2.5 7B&lt;/strong&gt; with native tool calling, loading with proper progress feedback&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Conversation persistence&lt;/strong&gt; as JSONL (machine) + Markdown sidecar (human), auto-saved per turn, restored on launch if recent&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tool calls&lt;/strong&gt; through Qwen&amp;rsquo;s native format, fallback text parser, per-tool timeouts, cancel button, pending spinner&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sleep/nap consolidation&lt;/strong&gt; turning episodes into facts, confidence decay, dream logs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-turn recall&lt;/strong&gt; grounding generation on stored facts without touching history&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EventKit&lt;/strong&gt; for calendar/reminders in milliseconds, &lt;code&gt;gh&lt;/code&gt; CLI for GitHub, NSDataDetector for natural-language dates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Right-click menu&lt;/strong&gt;: new conversation, sleep now, open dreams/conversations/memory in Finder&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Markdown-rendered assistant replies&lt;/strong&gt; via MarkdownUI, plain monospaced tool output&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AppleScript Run button&lt;/strong&gt; on any &lt;code&gt;applescript&lt;/code&gt; code block in a reply&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fine-tune loop (REM-derived training candidates → periodic LoRA refresh of stable voice and high-confidence facts) is designed but deferred. Retrieval has a faster iteration loop; weights-baking earns its keep when retrieval stops being enough. Given Higgins has had exactly zero users for three days, that threshold is a while off.&lt;/p&gt;
&lt;p&gt;Meanwhile he tells me what&amp;rsquo;s due tomorrow, doesn&amp;rsquo;t make up dates, remembers the cat&amp;rsquo;s name is Charlie, and opens PR review queues when I ask. Good enough for now.&lt;/p&gt;</description></item></channel></rss>