<?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: Aarush Karak</title>
    <description>The latest articles on DEV Community by Aarush Karak (@3ni8ma).</description>
    <link>https://dev.to/3ni8ma</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%2F4076659%2F5780d02b-9a82-42fe-a961-a5c540bb29e5.png</url>
      <title>DEV Community: Aarush Karak</title>
      <link>https://dev.to/3ni8ma</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/3ni8ma"/>
    <language>en</language>
    <item>
      <title>GitHub Automation with the gh CLI: Triage, PRs, and Workflows</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Tue, 01 Sep 2026 16:38:29 +0000</pubDate>
      <link>https://dev.to/3ni8ma/github-automation-with-the-gh-cli-triage-prs-and-workflows-297h</link>
      <guid>https://dev.to/3ni8ma/github-automation-with-the-gh-cli-triage-prs-and-workflows-297h</guid>
      <description>&lt;h2&gt;
  
  
  gh as the Complete API Client
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;gh api&lt;/code&gt; exposes the entire REST and GraphQL surface with auth built in. The pattern that unlocks everything: &lt;code&gt;gh api repos/{owner}/{repo}/issues&lt;/code&gt; piped to jq, and &lt;code&gt;gh run watch&lt;/code&gt; for workflow observability. The CLI is a thin wrapper over the API — everything the website can do, a script can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated Issue Triage
&lt;/h2&gt;

&lt;p&gt;The triage loop: fetch unlabeled issues, classify by keywords and titles, apply labels, assign priorities, and route to the right maintainer. A nightly script keeps a backlog organized without anyone touching the web UI. The discipline: classification must be conservative — an untriaged issue is better than a wrong label.&lt;/p&gt;

&lt;h2&gt;
  
  
  PR Review Automation
&lt;/h2&gt;

&lt;p&gt;The reviewer's checklist as code: check CI status, verify the diff touches only declared files, confirm the description matches the change, and comment when something fails the check. &lt;code&gt;gh pr view --json&lt;/code&gt; + &lt;code&gt;gh pr diff&lt;/code&gt; make the review scriptable; the script comments, a human decides.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workflow Lifecycle Management
&lt;/h2&gt;

&lt;p&gt;Beyond triggering: &lt;code&gt;gh workflow disable&lt;/code&gt; for flaky jobs, &lt;code&gt;gh run rerun&lt;/code&gt; for transient failures, and &lt;code&gt;gh api&lt;/code&gt; to approve protected-branch runs. The automation pattern: a cron that checks for failed runs older than a threshold and either reruns them once or files an issue — flaky CI stops being noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cross-Repo Coordination
&lt;/h2&gt;

&lt;p&gt;One script, many repos: a loop over &lt;code&gt;gh repo list&lt;/code&gt; runs the same triage or release check everywhere, and &lt;code&gt;gh issue create --repo&lt;/code&gt; files findings in each. For the 3-4 repos one person actually maintains, cross-repo automation collapses hours of manual clicking into one scheduled pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identity and Safety
&lt;/h2&gt;

&lt;p&gt;Automated actions run as a bot user or a PAT — the audit trail must show who did what. The safety patterns: &lt;code&gt;--dry-run&lt;/code&gt; flags everywhere, confirmation prompts for destructive ops (deletes, force-pushes, label removal), and rate-limit awareness (&lt;code&gt;gh api rate_limit&lt;/code&gt;).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The bank-driven fallback wrote this post because the LLM proxy was unreachable — structure and facts come from the topic outline, and the next regeneration will enrich it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;gh as the Complete API Client&lt;/li&gt;
&lt;li&gt;Automated Issue Triage&lt;/li&gt;
&lt;li&gt;PR Review Automation&lt;/li&gt;
&lt;li&gt;Workflow Lifecycle Management&lt;/li&gt;
&lt;li&gt;Cross-Repo Coordination&lt;/li&gt;
&lt;li&gt;Identity and Safety&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  FAQ
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in gh as the complete api client?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in automated issue triage?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in pr review automation?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The gh CLI makes GitHub a scripting surface: triage, review checks, workflow hygiene, and cross-repo maintenance all become scheduled, testable, and reviewable. The discipline — conservative classification, dry runs, honest audit trails — is what keeps the automation trusted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/3ni8ma" rel="noopener noreferrer"&gt;View the project on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>cli</category>
      <category>automation</category>
      <category>devops</category>
    </item>
    <item>
      <title>Running a Local LLM Proxy: OpenAI-Compatible Gateways</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Sat, 29 Aug 2026 16:01:46 +0000</pubDate>
      <link>https://dev.to/3ni8ma/running-a-local-llm-proxy-openai-compatible-gateways-2ka8</link>
      <guid>https://dev.to/3ni8ma/running-a-local-llm-proxy-openai-compatible-gateways-2ka8</guid>
      <description>&lt;h2&gt;
  
  
  Why a Gateway
&lt;/h2&gt;

&lt;p&gt;Every AI tool — editors, agents, scripts — speaks the OpenAI chat-completions dialect. A local gateway that speaks that dialect and forwards to whatever model you actually run makes every tool plug into local inference with zero code changes. One port, many consumers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compatibility Contract
&lt;/h2&gt;

&lt;p&gt;The surface that must match: &lt;code&gt;/v1/chat/completions&lt;/code&gt; with &lt;code&gt;messages&lt;/code&gt;, &lt;code&gt;model&lt;/code&gt;, &lt;code&gt;temperature&lt;/code&gt;, &lt;code&gt;max_tokens&lt;/code&gt;; SSE streaming via &lt;code&gt;stream: true&lt;/code&gt;; and error responses shaped like OpenAI's. Tools that validate models (a hardcoded model list) will reject your endpoint — accept any model name and map it, or report it honestly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Streaming Without Breaking Clients
&lt;/h2&gt;

&lt;p&gt;Clients that expect streaming fail on buffered responses. The implementation: read the upstream stream chunk-by-chunk, re-emit as SSE &lt;code&gt;data:&lt;/code&gt; frames, flush on each token, and terminate with &lt;code&gt;data: [DONE]&lt;/code&gt;. Timeouts are the silent killer — an idle upstream must send keep-alive comments or the client hangs forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching as the Cost Killer
&lt;/h2&gt;

&lt;p&gt;Identical prompts are common across sessions (system prompts, templates, retries). A prompt-hash cache with TTL serves repeated requests instantly and cuts provider spend. The design decision: cache exact matches only — semantic caching of LLM output is where correctness dies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Routing and Fallback
&lt;/h2&gt;

&lt;p&gt;A gateway that routes by model name, cost budget, or availability. The fallback ladder: local model → free tier → paid provider, with health checks that demote a failing upstream. The pattern: retry with backoff on 429/5xx, fail over on repeated failures, and never silently return a different model than requested.&lt;/p&gt;

&lt;h2&gt;
  
  
  Operational Details That Matter
&lt;/h2&gt;

&lt;p&gt;launchd or systemd supervision, structured logs with per-request latency, and a health endpoint (&lt;code&gt;/v1/models&lt;/code&gt;) for uptime monitoring. The gateway becomes the single place where model behavior — version, temperature caps, token limits — is controlled for the whole machine.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The bank-driven fallback wrote this post because the LLM proxy was unreachable — structure and facts come from the topic outline, and the next regeneration will enrich it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Why a Gateway&lt;/li&gt;
&lt;li&gt;The Compatibility Contract&lt;/li&gt;
&lt;li&gt;Streaming Without Breaking Clients&lt;/li&gt;
&lt;li&gt;Caching as the Cost Killer&lt;/li&gt;
&lt;li&gt;Routing and Fallback&lt;/li&gt;
&lt;li&gt;Operational Details That Matter&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  FAQ
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in why a gateway?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in the compatibility contract?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in streaming without breaking clients?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;A local LLM proxy is the quiet infrastructure that makes local AI practical: one compatible endpoint, streaming done right, caching for cost, and honest routing. It turns 'AI tools need APIs' into 'everything already works, it's just local now.'&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/deepseek-free-proxy" rel="noopener noreferrer"&gt;View the project on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>api</category>
      <category>proxy</category>
      <category>localai</category>
    </item>
    <item>
      <title>Vite Plugin Development: Hooks, Transforms, and Virtual Modules</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Thu, 27 Aug 2026 16:01:55 +0000</pubDate>
      <link>https://dev.to/3ni8ma/vite-plugin-development-hooks-transforms-and-virtual-modules-4n7h</link>
      <guid>https://dev.to/3ni8ma/vite-plugin-development-hooks-transforms-and-virtual-modules-4n7h</guid>
      <description>&lt;h2&gt;
  
  
  The Plugin Container Model
&lt;/h2&gt;

&lt;p&gt;A Vite plugin is an object with hooks, layered over Rollup. The phases: &lt;code&gt;config&lt;/code&gt; (adjust Vite config), &lt;code&gt;configResolved&lt;/code&gt; (read final config), &lt;code&gt;buildStart&lt;/code&gt; (initialize), &lt;code&gt;transform&lt;/code&gt; (rewrite module source), &lt;code&gt;generateBundle&lt;/code&gt; (write output files), &lt;code&gt;closeBundle&lt;/code&gt;. Understanding when each runs — dev vs build — is half the API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transforms: Rewriting Modules Safely
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;transform&lt;/code&gt; hook receives code + id and returns modified code. The discipline: only touch files you own (check &lt;code&gt;id&lt;/code&gt; patterns), return &lt;code&gt;null&lt;/code&gt; to pass through untouched, and never rely on textual assumptions about frameworks — use the loaders' ASTs or the module graph instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Virtual Modules for Injected Code
&lt;/h2&gt;

&lt;p&gt;Virtual modules let a plugin provide modules that don't exist on disk: &lt;code&gt;virtual:my-plugin-data&lt;/code&gt;. Intercept in &lt;code&gt;resolveId&lt;/code&gt;, emit in &lt;code&gt;load&lt;/code&gt;. This is how plugins inject runtime data (config, asset manifests, environment) without touching the user's source tree — and how sitemap plugins feed route lists to components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build-Time Asset Generation
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;generateBundle&lt;/code&gt; hook writes files into the output: sitemaps, robots, feeds, OG images. The pattern: collect data during earlier hooks (routes from the module graph, page metadata from transforms), then emit &lt;code&gt;this.emitFile({ type: 'asset', fileName, source })&lt;/code&gt;. Assets emitted here are hashed, deployed, and cacheable like any other build output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dev Mode: Middleware and HMR
&lt;/h2&gt;

&lt;p&gt;In dev, plugins get an HTTP middleware (&lt;code&gt;configureServer&lt;/code&gt;) and HMR hooks. A sitemap plugin doesn't need it, but a plugin serving mock APIs or proxying websockets lives here. The rule: dev features must not leak into the production bundle — gate everything on &lt;code&gt;config.command&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing and Testing Plugins
&lt;/h2&gt;

&lt;p&gt;A Vite plugin is a library: &lt;code&gt;vite-plugin-*&lt;/code&gt; naming, ESM-first, exports map, and a test suite that runs the real Vite build against a fixture project. The &lt;code&gt;buildTest&lt;/code&gt; pattern: a temp project, &lt;code&gt;build()&lt;/code&gt;, and assertions on output files — faster and more honest than unit-testing hooks in isolation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The bank-driven fallback wrote this post because the LLM proxy was unreachable — structure and facts come from the topic outline, and the next regeneration will enrich it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Plugin Container Model&lt;/li&gt;
&lt;li&gt;Transforms: Rewriting Modules Safely&lt;/li&gt;
&lt;li&gt;Virtual Modules for Injected Code&lt;/li&gt;
&lt;li&gt;Build-Time Asset Generation&lt;/li&gt;
&lt;li&gt;Dev Mode: Middleware and HMR&lt;/li&gt;
&lt;li&gt;Publishing and Testing Plugins&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  FAQ
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in the plugin container model?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in transforms: rewriting modules safely?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in virtual modules for injected code?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Vite plugins are the cleanest expression of the bundler-as-platform idea. Hook order, transform discipline, and virtual modules cover most real needs — and a plugin that generates build artifacts (sitemaps, feeds) is one of the highest-leverage integrations a site can have.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/vite-plugin" rel="noopener noreferrer"&gt;View the project on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>vite</category>
      <category>plugins</category>
      <category>buildtools</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Browser Automation That Looks Human</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Tue, 25 Aug 2026 16:02:07 +0000</pubDate>
      <link>https://dev.to/3ni8ma/browser-automation-that-looks-human-4m94</link>
      <guid>https://dev.to/3ni8ma/browser-automation-that-looks-human-4m94</guid>
      <description>&lt;h2&gt;
  
  
  What Detection Systems Measure
&lt;/h2&gt;

&lt;p&gt;A fingerprint is a vector: user agent, WebGL vendor and renderer strings, canvas hash, audio context, screen metrics, fonts, timezone, and navigator property consistency. Bot detectors score the vector for anomalies — a headless Chromium with &lt;code&gt;navigator.webdriver=true&lt;/code&gt; fails instantly; the game is scoring like a real device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patching the Leaky Surface
&lt;/h2&gt;

&lt;p&gt;The leaks: &lt;code&gt;navigator.webdriver&lt;/code&gt;, missing &lt;code&gt;window.chrome&lt;/code&gt;, the &lt;code&gt;--headless&lt;/code&gt; UA, and iframe parent attributes. The fixes: CDP session to override properties before any script runs, spoofed WebGL strings, and a realistic UA/fingerprint pair. The rule: every override must be consistent — a Chrome UA with a Safari canvas hash is worse than no spoofing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Input That Reads as Human
&lt;/h2&gt;

&lt;p&gt;Automated typing is instant and perfectly regular; humans type with variable inter-key latency, occasional corrections, and pauses. The pattern: randomized per-character delays with log-normal distribution, jittered mouse movement along bezier curves, and scroll speeds that vary. Timing consistency across a session matters more than any single delay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Session Persistence: The Cookie Jar That Matters
&lt;/h2&gt;

&lt;p&gt;Sites trust continuity: a fresh context with no cookies, no localStorage, and a first-seen IP screams automation. The pattern: persistent browser profiles stored between runs, realistic storage state, and reusing the same context for related tasks. Trust compounds over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Orchestrating Multi-Context Automation
&lt;/h2&gt;

&lt;p&gt;Different tasks need different identities. The architecture: a context manager that provisions profiles, health-checks them (does this context still pass a fingerprint test?), and rotates on failure. A context that gets flagged is retired, not reused — one burned context can poison everything it touched.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ethics of Blending In
&lt;/h2&gt;

&lt;p&gt;Human-like automation exists on a spectrum: scraping public data with throttling is defensible; impersonating a user or evading explicit blocks is not. The operational rules that keep a project defensible: rate limits well under human capacity, respect robots.txt and ToS, and never automate account creation or payment flows.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The bank-driven fallback wrote this post because the LLM proxy was unreachable — structure and facts come from the topic outline, and the next regeneration will enrich it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;What Detection Systems Measure&lt;/li&gt;
&lt;li&gt;Patching the Leaky Surface&lt;/li&gt;
&lt;li&gt;Input That Reads as Human&lt;/li&gt;
&lt;li&gt;Session Persistence: The Cookie Jar That Matters&lt;/li&gt;
&lt;li&gt;Orchestrating Multi-Context Automation&lt;/li&gt;
&lt;li&gt;The Ethics of Blending In&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  FAQ
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in what detection systems measure?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in patching the leaky surface?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in input that reads as human?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Human-like browser automation is a fingerprint consistency problem, not a feature list. Every patched leak must be coherent with the identity you're projecting — and the engineering discipline matters less than the boundaries you refuse to cross.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/anomalyco/camofox-browser" rel="noopener noreferrer"&gt;View the project on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>browserautomation</category>
      <category>playwright</category>
      <category>stealth</category>
      <category>fingerprinting</category>
    </item>
    <item>
      <title>Supabase as a Production Backend: Auth, RLS, and Realtime</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Sat, 22 Aug 2026 16:06:47 +0000</pubDate>
      <link>https://dev.to/3ni8ma/supabase-as-a-production-backend-auth-rls-and-realtime-3afj</link>
      <guid>https://dev.to/3ni8ma/supabase-as-a-production-backend-auth-rls-and-realtime-3afj</guid>
      <description>&lt;h2&gt;
  
  
  The Postgres-First Pitch
&lt;/h2&gt;

&lt;p&gt;Supabase is PostgreSQL with batteries: the auth schema, storage, and realtime are all Postgres features exposed over APIs. The practical consequence: everything you know about Postgres — indexes, views, EXPLAIN — transfers directly. Data lives in tables you own, not a proprietary store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Auth: Sessions, Not Tokens
&lt;/h2&gt;

&lt;p&gt;Email/password auth with refresh tokens, session persistence in localStorage, and &lt;code&gt;onAuthStateChange&lt;/code&gt; listeners. The trap: relying on the token's JWT instead of letting RLS decide. Auth answers 'who is this user'; RLS answers 'what may they see' — keeping those separate is the whole architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Row-Level Security as the API Boundary
&lt;/h2&gt;

&lt;p&gt;RLS policies are the authorization layer: &lt;code&gt;USING&lt;/code&gt; clauses filter reads, &lt;code&gt;WITH CHECK&lt;/code&gt; clauses gate writes. A user-scoped policy (&lt;code&gt;user_id = auth.uid()&lt;/code&gt;) means no endpoint can ever leak another user's rows — even a buggy join. Policies are per-table SQL, testable with &lt;code&gt;set local role authenticated&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Realtime Without the Socket Fleet
&lt;/h2&gt;

&lt;p&gt;Supabase realtime is Postgres replication broadcast over websockets. For a dashboard, the pattern: subscribe to channel filters, apply deltas to local state, and treat the subscription as a cache — not the source of truth. Reconnection with exponential backoff is mandatory; the server does not guarantee delivery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage, Edge Functions, and the Free Tier
&lt;/h2&gt;

&lt;p&gt;Storage buckets with MIME policies and signed URLs for private files; edge functions (Deno) for webhooks and server-side logic that must not ship in the client. The free tier caps: database size, monthly auth users, edge function invocations — know the limits before choosing the architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Zero-Cost Stack Composition
&lt;/h2&gt;

&lt;p&gt;Supabase pairs with a Vercel frontend and a Python backend that reads the same Postgres. The seam: the Python service uses a service-role key (server-only) while the client uses user JWTs with RLS. One database, two trust boundaries, zero middleware to maintain.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; The bank-driven fallback wrote this post because the LLM proxy was unreachable — structure and facts come from the topic outline, and the next regeneration will enrich it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Postgres-First Pitch&lt;/li&gt;
&lt;li&gt;Auth: Sessions, Not Tokens&lt;/li&gt;
&lt;li&gt;Row-Level Security as the API Boundary&lt;/li&gt;
&lt;li&gt;Realtime Without the Socket Fleet&lt;/li&gt;
&lt;li&gt;Storage, Edge Functions, and the Free Tier&lt;/li&gt;
&lt;li&gt;The Zero-Cost Stack Composition&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  FAQ
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in the postgres-first pitch?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in auth: sessions, not tokens?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What is the key idea in row-level security as the api boundary?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; It is one of the core decisions that shape this topic. The section above walks through the reasoning, the tradeoffs, and the practical takeaway in context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Supabase is a shortcut to a real Postgres backend, not a toy. The discipline — RLS as the security boundary, sessions over tokens, treating realtime as a cache — is what separates a demo from something you'd trust with real data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/DoxDock" rel="noopener noreferrer"&gt;View the project on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>supabase</category>
      <category>postgres</category>
      <category>auth</category>
      <category>rls</category>
    </item>
    <item>
      <title>Orchestrating 24/7 AI Agents on a Mac</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Thu, 20 Aug 2026 16:20:38 +0000</pubDate>
      <link>https://dev.to/3ni8ma/orchestrating-247-ai-agents-on-a-mac-36ob</link>
      <guid>https://dev.to/3ni8ma/orchestrating-247-ai-agents-on-a-mac-36ob</guid>
      <description>&lt;h2&gt;
  
  
  Why launchd is the only serious process manager on macOS
&lt;/h2&gt;

&lt;p&gt;The first mistake is reaching for cron. Cron is a timer, not a process manager. If a cron job crashes at 3 AM, nothing restarts it. If it runs for 14 hours past its schedule, cron does not care. And cron's environment is a stripped-down shell with no PATH, no &lt;code&gt;launchctl&lt;/code&gt; session access, and no sane way to get secrets.&lt;/p&gt;

&lt;p&gt;Docker Desktop on macOS is a VM inside your laptop. It handles restarts well, but it costs 2–4 GB of RAM just to idle, and it inserts a virtualization layer between an agent and the host filesystem. For a lightweight program that makes HTTP calls and writes JSON, that is a waste of the exact resource — memory — that agents consume fastest.&lt;/p&gt;

&lt;p&gt;I compared the options before committing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runner&lt;/th&gt;
&lt;th&gt;Restart policy&lt;/th&gt;
&lt;th&gt;Logging&lt;/th&gt;
&lt;th&gt;Idle RAM cost&lt;/th&gt;
&lt;th&gt;Fits an LLM agent loop?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cron&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None built-in&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;No: no supervision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nohup + manual&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Redirect to file&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;No: dies with your session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;launchd&lt;/td&gt;
&lt;td&gt;KeepAlive, throttled, event-driven&lt;/td&gt;
&lt;td&gt;StandardOut/Err paths, newsyslog integration&lt;/td&gt;
&lt;td&gt;~0&lt;/td&gt;
&lt;td&gt;Yes, and it is native&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker Desktop&lt;/td&gt;
&lt;td&gt;Full container restart&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;2–4 GB&lt;/td&gt;
&lt;td&gt;Overkill on a laptop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;launchd runs as PID 1 on macOS. It supervises every daemon on the system, it survives logout, it can restart processes with exponential backoff built into &lt;code&gt;ThrottleInterval&lt;/code&gt;, and it costs nothing until a process starts. The only real question was how to make an agent loop that fits launchd's model — a model where a job either runs or exits with a status code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent loop that never returns
&lt;/h2&gt;

&lt;p&gt;An agent in aion is a &lt;code&gt;while True&lt;/code&gt; loop. That is not laziness; it is a deliberate contract with launchd. launchd's &lt;code&gt;KeepAlive&lt;/code&gt; handles crashes. The agent itself handles idleness. If there is no work, the loop sleeps on the queue. If there is work, it claims a row, calls the LLM, executes tool calls, writes results, and loops. A job that "finishes" and exits is, by design, a failure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import json
import os
import time

API_KEY = os.environ["AION_LLM_KEY"]
MAX_BACKOFF = 300  # seconds

def claim_next_task() -&amp;gt; dict | None:
    # Single-writer claim; see the queue section below.
    ...

def run_cycle() -&amp;gt; None:
    task = claim_next_task()
    if task is None:
        time.sleep(2)
        return
    try:
        response = call_llm(task["payload"], tools=TOOLS, api_key=API_KEY)
        for call in response["tool_calls"]:
            execute_tool(call)
        mark_done(task["id"], response)
    except RateLimitError:
        requeue(task["id"], delay=min(2 ** task["attempts"], MAX_BACKOFF))
    except ToolValidationError:
        requeue(task["id"], delay=1)  # bad payload, not a crash

if __name__ == "__main__":
    while True:
        run_cycle()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The subtle part is the rate-limit path. When an LLM API returns 429, the agent does not die — that would trigger launchd's restart logic and burn another request immediately. Instead, the agent catches it, compute the exponential delay itself, and requeues the task with &lt;code&gt;next_run_at&lt;/code&gt; in the future. The process stays alive, the queue holds the work, and the API key cools down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The launchd plist that survives reboots
&lt;/h2&gt;

&lt;p&gt;The entire orchestration story hinges on one XML file in &lt;code&gt;~/Library/LaunchAgents/&lt;/code&gt;. Everything else in aion is glue around it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;
&amp;lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&amp;gt;
&amp;lt;plist version="1.0"&amp;gt;
&amp;lt;dict&amp;gt;
    &amp;lt;key&amp;gt;Label&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;com.aion.worker&amp;lt;/string&amp;gt;

    &amp;lt;key&amp;gt;ProgramArguments&amp;lt;/key&amp;gt;
    &amp;lt;array&amp;gt;
        &amp;lt;string&amp;gt;/opt/homebrew/bin/python3&amp;lt;/string&amp;gt;
        &amp;lt;string&amp;gt;/Users/me/aion/agent_loop.py&amp;lt;/string&amp;gt;
    &amp;lt;/array&amp;gt;

    &amp;lt;key&amp;gt;WorkingDirectory&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;/Users/me/aion&amp;lt;/string&amp;gt;

    &amp;lt;!-- Start once even if it exited cleanly before --&amp;gt;
    &amp;lt;key&amp;gt;RunAtLoad&amp;lt;/key&amp;gt;
    &amp;lt;true/&amp;gt;

    &amp;lt;key&amp;gt;KeepAlive&amp;lt;/key&amp;gt;
    &amp;lt;dict&amp;gt;
        &amp;lt;!-- Restart only on crashes: keep alive while the job is running,
             and relaunch when it exits with a non-zero status. --&amp;gt;
        &amp;lt;key&amp;gt;SuccessfulExit&amp;lt;/key&amp;gt;
        &amp;lt;false/&amp;gt;
    &amp;lt;/dict&amp;gt;

    &amp;lt;!-- Minimum seconds between automatic relaunches. Default is 10;
         raising it prevents a hot crash loop from hammering the LLM API. --&amp;gt;
    &amp;lt;key&amp;gt;ThrottleInterval&amp;lt;/key&amp;gt;
    &amp;lt;integer&amp;gt;30&amp;lt;/integer&amp;gt;

    &amp;lt;key&amp;gt;StandardOutPath&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;/Users/me/Library/Logs/aion/worker.log&amp;lt;/string&amp;gt;
    &amp;lt;key&amp;gt;StandardErrorPath&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;/Users/me/Library/Logs/aion/worker.err&amp;lt;/string&amp;gt;

    &amp;lt;key&amp;gt;EnvironmentVariables&amp;lt;/key&amp;gt;
    &amp;lt;dict&amp;gt;
        &amp;lt;key&amp;gt;PYTHONUNBUFFERED&amp;lt;/key&amp;gt;
        &amp;lt;string&amp;gt;1&amp;lt;/string&amp;gt;
    &amp;lt;/dict&amp;gt;

    &amp;lt;key&amp;gt;ProcessType&amp;lt;/key&amp;gt;
    &amp;lt;string&amp;gt;Background&amp;lt;/string&amp;gt;
&amp;lt;/dict&amp;gt;
&amp;lt;/plist&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Load it with &lt;code&gt;launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.aion.worker.plist&lt;/code&gt;. Unload with &lt;code&gt;bootout&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TIP:&lt;/strong&gt; ThrottleInterval is the minimum time launchd waits before relaunching a crashed job. Set it to at least 30 seconds. The default of 10 is enough for a fast crash loop to burn real money against a paid LLM API before you wake up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code&gt;KeepAlive&lt;/code&gt; dict with &lt;code&gt;SuccessfulExit = false&lt;/code&gt; is the key decision: if the agent exits with code 0, launchd leaves it dead. That gives you an escape hatch — if you need to stop an agent permanently for maintenance, a graceful shutdown path that exits zero stops the restarts. Any nonzero exit, launchd treats as a crash and relaunches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crashes are cheap; crash loops are not
&lt;/h2&gt;

&lt;p&gt;Every crash costs at least one wasted LLM call when the loop restarts and re-fetches its context. A crash loop at 10-second intervals is not just noise; it is a credit-draining feedback loop. The fix is layered backoff, enforced in two places.&lt;/p&gt;

&lt;p&gt;First, inside the agent, as shown above: catch rate limits and requeue with &lt;code&gt;min(2 ** attempts, 300)&lt;/code&gt; seconds. Second, in the watchdog (next sections), which kills agents that exceed their retry budget.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def next_delay(attempt: int) -&amp;gt; int:
    """1, 2, 4, 8, ... capped at 5 minutes."""
    return min(2 ** attempt, 300)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I have watched a buggy tool result cause 40 API calls in 6 minutes because I trusted &lt;code&gt;KeepAlive&lt;/code&gt; to solve availability. It solves availability, not cost. The backoff budget is the real guardrail.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;WARNING:&lt;/strong&gt; An agent that crash-loops for an hour can outspend a month of normal operation. Cap attempts per task in the queue, and cap restarts per hour in the watchdog, before you ever deploy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The SQLite queue that keeps agents honest
&lt;/h2&gt;

&lt;p&gt;With multiple agents (price monitoring, email triage, research), you need a durable work queue. On a Mac, Postgres and Redis are operational overhead you do not want. SQLite is a file; it survives reboots, it is transactional, and macOS ships a perfectly modern version.&lt;/p&gt;

&lt;p&gt;The schema is deliberately small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE tasks (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  agent TEXT NOT NULL,
  payload TEXT NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending',      -- pending | running | done | dead
  attempts INTEGER NOT NULL DEFAULT 0,
  max_attempts INTEGER NOT NULL DEFAULT 5,
  next_run_at INTEGER NOT NULL DEFAULT 0,       -- unixepoch; future = delayed
  created_at INTEGER NOT NULL DEFAULT (unixepoch()),
  updated_at INTEGER
);

CREATE INDEX idx_tasks_claim
  ON tasks(status, next_run_at)
  WHERE status = 'pending';

PRAGMA journal_mode = WAL;
PRAGMA busy_timeout = 5000;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The claim operation is a single statement that atomically moves one pending row to running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UPDATE tasks
SET status = 'running', updated_at = unixepoch()
WHERE id = (
  SELECT id FROM tasks
  WHERE status = 'pending' AND next_run_at &amp;lt;= unixepoch()
  ORDER BY created_at
  LIMIT 1
)
RETURNING *;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules kept this reliable. First, exactly one writer: only the main aion daemon writes to the queue; agents read through a tiny local API. No locking spaghetti. Second, WAL mode and &lt;code&gt;busy_timeout&lt;/code&gt; mean a crashed agent holding a connection cannot block the daemon forever. If an agent dies mid-task, the daemon marks its &lt;code&gt;running&lt;/code&gt; row as &lt;code&gt;pending&lt;/code&gt; again with a delay — the crash is invisible to the rest of the fleet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory: the silent agent killer
&lt;/h2&gt;

&lt;p&gt;LLM agents leak memory. Tool results pile up, context windows grow, and every retry embeds the previous failure into the next prompt. On a Linux box, OOM-killer events show up in &lt;code&gt;dmesg&lt;/code&gt;. On macOS, the system just starts swapping, and the machine turns into a beach ball festival while your agent "works."&lt;/p&gt;

&lt;p&gt;I learned to treat RSS as the primary health metric, not CPU. The watchdog reads memory usage from &lt;code&gt;ps&lt;/code&gt; — no psutil dependency needed, though it is nicer if you have it — and kills anything above a per-agent ceiling. launchd immediately restarts the agent, and the queue still holds its incomplete task.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import os
import signal
import subprocess
import time

MAX_RSS_MB = 4096  # agents have a 4 GB ceiling in aion
HEARTBEAT_TTL_S = 180

def rss_mb(pid: int) -&amp;gt; int:
    out = subprocess.check_output(
        ["ps", "-o", "rss=", "-p", str(pid)]
    )
    return int(out)  # ps reports RSS in KB; convert below

def is_stale(agent_dir: str) -&amp;gt; bool:
    hb = os.path.join(agent_dir, "heartbeat")
    if not os.path.exists(hb):
        return True
    age = time.time() - os.path.getmtime(hb)
    return age &amp;gt; HEARTBEAT_TTL_S

while True:
    for pidfile in os.listdir("/var/run/aion"):
        pid = int(open(f"/var/run/aion/{pidfile}").read().strip())
        rss_kb = rss_mb(pid)
        if rss_kb &amp;gt; MAX_RSS_MB * 1024:
            os.kill(pid, signal.SIGKILL)
            # launchd sees the nonzero exit and relaunches the job.
        if is_stale(f"/var/run/aion/{pidfile}"):
            os.kill(pid, signal.SIGKILL)
    time.sleep(30)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key trick is coordinating with launchd rather than fighting it: the watchdog never tries to "restart" an agent. It only kills. launchd's &lt;code&gt;KeepAlive&lt;/code&gt; is the restart mechanism, and because &lt;code&gt;ThrottleInterval&lt;/code&gt; is set, a repeated leak-and-kill cycle gets slower over time instead of faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Logs: owning the aftermath at 3 AM
&lt;/h2&gt;

&lt;p&gt;Launchd writes &lt;code&gt;StandardOutPath&lt;/code&gt; and &lt;code&gt;StandardErrorPath&lt;/code&gt; as plain files with no rotation. A 24/7 agent writes megabytes of JSON per day; within a month, a worker log can consume gigabytes. The macOS-native answer is &lt;code&gt;newsyslog(8)&lt;/code&gt;, the same tool that rotates system logs.&lt;/p&gt;

&lt;p&gt;Drop this into &lt;code&gt;/etc/newsyslog.d/aion.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# logfile                   owner:group  mode  count  size  when  flags
/Users/me/Library/Logs/aion/worker.log  me:staff  644  7  1000000  *  Z
/Users/me/Library/Logs/aion/worker.err  me:staff  644  7  1000000  *  Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps 7 rotated files, compresses old ones with gzip (&lt;code&gt;Z&lt;/code&gt;), and triggers rotation at 1 MB. The configuration is checked by &lt;code&gt;newsyslog&lt;/code&gt; on a timer, so there is nothing else to schedule.&lt;/p&gt;

&lt;p&gt;The deeper lesson: treat logs as data, not as prose. Every agent cycle should emit one JSON line with a correlation ID (&lt;code&gt;task_id&lt;/code&gt;), the LLM model, token counts, and latency. When the price feed breaks at 3 AM, you want &lt;code&gt;grep 2025-06-12T02:47&lt;/code&gt; to return five structured lines, not a wall of print statements. I lost two days to debugging a deadlock that only appeared in a 400 MB text file that had been truncated by the filesystem. Structured logs would have shown the stuck tool call immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets without plaintext env files
&lt;/h2&gt;

&lt;p&gt;The naive approach is &lt;code&gt;EnvironmentVariables&lt;/code&gt; in the plist. Do not do it. Plist files are readable by any process running as your user, they leak into &lt;code&gt;launchctl&lt;/code&gt; output, and they are one accidental &lt;code&gt;git add&lt;/code&gt; away from being pushed to a public repo. (I did that. It was one character away from being in the commit history of a now-deleted repository.)&lt;/p&gt;

&lt;p&gt;The right place for secrets on macOS is the Keychain, via the &lt;code&gt;security&lt;/code&gt; CLI. The agent reads its API key at startup, holds it in memory, and never writes it to disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
set -euo pipefail

# Store once:
#   security add-generic-password -U -a worker -s aion-api -w "$LLM_KEY"
export AION_LLM_KEY=$(
  security find-generic-password -w -a worker -s aion-api
)
exec /opt/homebrew/bin/python3 /Users/me/aion/agent_loop.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One tradeoff: Keychain access requires your login session to be unlocked. If you reboot and never log in, the keychain is locked and the agent cannot start. For a personal machine that is acceptable — you are logging in anyway. For a headless Mac mini used as a server, use the login keychain and disable auto-lock, or generate app-specific tokens with a shorter lifetime and rotate them monthly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What aion v2 does differently
&lt;/h2&gt;

&lt;p&gt;The first version of aion had the watchdog and the agent in the same process. That was a mistake: when the watchdog crashed, everything died, and launchd restarted the whole thing — including the queue reader, which then double-claimed tasks. Splitting the supervisor (aion daemon) from the workers (launchd jobs) was the architectural fix that made the system boring. Boring is what you want at 3 AM. If I rebuilt it today, I would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use one launchd job per agent&lt;/strong&gt;, not one job running many agents. A crash in one agent should never be able to take down the others in the same process group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add &lt;code&gt;WatchPaths&lt;/code&gt;&lt;/strong&gt; so configuration changes auto-reload workers, instead of a manual &lt;code&gt;bootout&lt;/code&gt;/&lt;code&gt;bootstrap&lt;/code&gt; dance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model the heartbeat as a database row&lt;/strong&gt;, not a file mtime. Files get cleaned by macOS cleanup tools; the SQLite database is durable and queryable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the backoff calculator in the queue schema itself&lt;/strong&gt;, as a check constraint: &lt;code&gt;attempts &amp;lt;= max_attempts&lt;/code&gt;. That makes the invariant impossible to violate by accident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hardest-won lesson of this whole project: the orchestrator's job is to fail fast, restart clean, and make the crash log legible. Everything else — the prompts, the tool schemas, the model choice — is application code. The moment I stopped treating process management as "just a script that keeps things running" and started treating it as a small, distributed-systems problem with a state machine, the agents became genuinely unattended.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Can this run on an Intel Mac?&lt;/strong&gt; Yes. launchd and SQLite behave the same; I deployed aion on a 2018 Intel Mac mini and the only difference was the memory ceiling (it needed 2 GB, not 4 GB).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does the Mac need to be logged in?&lt;/strong&gt; The agents run in a user session, so yes — the session must be active. For a headless setup, consider a dedicated user account with automatic login and the login keychain unlocked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if the network drops?&lt;/strong&gt; The agent process stays alive and the LLM call raises a connection error, which we catch, requeue with backoff, and sleep. launchd never sees a crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run cloud agents alongside aion?&lt;/strong&gt; Yes. I run aion for latency-sensitive tasks and a cloud worker for batch jobs; they share the same SQLite schema, and a small sync script pushes finished tasks upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building a 24/7 AI agent farm on a Mac is less about the agents and more about the boring plumbing: a process supervisor that restarts things, a queue that survives crashes, a watchdog that kills leaks, and logs you can actually read after midnight. launchd does the heavy lifting; all I added is a loop, a heartbeat, and some discipline around backoff and secrets.&lt;/p&gt;

&lt;p&gt;The result is a system that runs for weeks without human intervention, costs nothing to idle, and — most importantly — fails loudly the moment something is actually wrong. That is the entire point of orchestration: not to make agents infallible, but to make their failures cheap and visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;launchd is the only first-class process supervisor on macOS; cron has no restart policy and Docker Desktop wastes 2–4 GB of RAM on a laptop.&lt;/li&gt;
&lt;li&gt;KeepAlive with &lt;code&gt;SuccessfulExit = false&lt;/code&gt; plus a raised &lt;code&gt;ThrottleInterval&lt;/code&gt; gives you crash recovery without a costly restart loop.&lt;/li&gt;
&lt;li&gt;Agents must never exit on transient errors; catch rate limits and requeue with exponential backoff inside the process.&lt;/li&gt;
&lt;li&gt;Use SQLite (WAL mode, single writer) as the durable work queue; one atomic &lt;code&gt;UPDATE ... RETURNING&lt;/code&gt; claim prevents double-processing.&lt;/li&gt;
&lt;li&gt;Monitor RSS and heartbeat staleness from a separate watchdog that only kills, letting launchd handle the restart.&lt;/li&gt;
&lt;li&gt;Store LLM API keys in the macOS Keychain, never in the plist or a plaintext env file.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>automation</category>
      <category>launchd</category>
      <category>aiagents</category>
      <category>orchestration</category>
    </item>
    <item>
      <title>Building Custom MCP Servers: Extending AI with Tools</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Tue, 18 Aug 2026 17:53:35 +0000</pubDate>
      <link>https://dev.to/3ni8ma/building-custom-mcp-servers-extending-ai-with-tools-4od6</link>
      <guid>https://dev.to/3ni8ma/building-custom-mcp-servers-extending-ai-with-tools-4od6</guid>
      <description>&lt;h2&gt;
  
  
  The Protocol That Actually Standardizes Tooling
&lt;/h2&gt;

&lt;p&gt;MCP (Model Context Protocol) is essentially a JSON-RPC 2.0 service with a schema for capabilities, tools, resources, and prompts. The server declares tools; the host discovers them; the model decides when to call them. That sounds abstract until you realize what it replaces: a pile of bespoke LangChain wrappers, ad-hoc API endpoints, and "let me stuff the whole repo into the prompt" hacks. MCP is the rare protocol that wins on boredom. It's simple, stateless (if you want it to be), and has enough transport options (stdio, SSE, streamable HTTP) that you can put it in front of a local CLI or a remote session.&lt;/p&gt;

&lt;p&gt;We chose Python with the &lt;code&gt;mcp&lt;/code&gt; SDK because our text-processing pipeline was already Python-native. But the protocol constraints pushed us into a cleaner design: every tool needs a JSON Schema for input, a name, and an output shape that gets packed back into a content block. There's no "just return the dict" wiggle room. That forced us to define a contract for every query before writing any search logic. Contracts are a feature when you're building an interface that a probabilistic system will be driving.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Slice-Grep Does Not Scale
&lt;/h2&gt;

&lt;p&gt;Before &lt;code&gt;codebase-memory-mcp&lt;/code&gt;, I watched agents operate on codebases roughly like this: they get a prompt, they try to recall from training data, they fail, they request a file path, the host returns a raw file, and the agent starts spraying grep calls. The agent never gets a map. It never gets "here are the ten modules that touch the payment webhook." It gets a hunk of source code and a hope.&lt;/p&gt;

&lt;p&gt;A codebase has structure at a level above raw text: symbols, import graphs, path hierarchies, role-specific conventions (tests vs. production, migrations vs. models). The first painful lesson was that embeddings alone can't express that. A vector search will happily match a comment about "retry policy" in a README and a test fixture with zero actual retry logic. Semantic similarity is necessary but not sufficient. So we built a hybrid index: a SQLite database with three layers — (1) file metadata and path structure, (2) symbol-level records (function names, class names, exports), and (3) chunked text with 768-dimension embeddings stored via &lt;code&gt;sqlite-vec&lt;/code&gt;. The MCP tools we exposed became the SQL layer for the model, carefully shaped so that a model knows &lt;em&gt;when&lt;/em&gt; it has a precise question (symbol lookup) and &lt;em&gt;when&lt;/em&gt; it should perform a fuzzy recall (semantic search).&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the Tool Surface
&lt;/h2&gt;

&lt;p&gt;The biggest question in an MCP server is: what is the toolkit? We kept it to seven tools initially. Every subsequent tool we proposed had to earn its place by solving a category of agent failure we'd actually observed.&lt;/p&gt;

&lt;p&gt;The first tool is &lt;code&gt;semantic_search&lt;/code&gt;, which takes a query string and a number of results. It does embedding-based retrieval with pre-filtering. The second is &lt;code&gt;search_symbol&lt;/code&gt;, an exact symbol/lookup tool using a trigram index and the language parser (tree-sitter) — no embeddings, just identifier-aware matching. That distinction matters. A model should never use fuzzy search when it knows the exact name of a function. The third and fourth are &lt;code&gt;get_file_structure&lt;/code&gt; and &lt;code&gt;read_file_lines&lt;/code&gt;, which handle the "can you show me the tree" and "show lines 40–70" operations. We added &lt;code&gt;find_references&lt;/code&gt; for cross-file references, and &lt;code&gt;get_recent_commits&lt;/code&gt; to answer "what changed recently" without reading every diff. Finally, &lt;code&gt;remember&lt;/code&gt; and &lt;code&gt;recall&lt;/code&gt; let the agent store a note about a design decision into a separate SQLite table, allowing memory to persist across separate MCP sessions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from mcp.server import Server
from mcp.server.models import InitializationOptions

async def handle_semantic_search(query: str, limit: int = 5) -&amp;gt; dict:
    """Embed a query and search over the combined table."""
    embedding = embedder.embed(query)          # (768,)
    sql = """
        SELECT path, start_line, text, 
               ivec_distance(chunk_embedding, ?) AS distance
        FROM chunks
        WHERE path IN (SELECT path FROM files WHERE indexed_at IS NOT NULL)
        ORDER BY distance
        LIMIT ?
    """
    rows = await db.execute(sql, [embedding, limit])
    return {"results": [chunk_to_dict(r) for r in rows]}

def build_mcp_server(db, embedder) -&amp;gt; Server:
    server = Server("codebase-memory")
    @server.list_tools()
    async def list_tools():
        return [
            Tool(
                name="semantic_search",
                description="Search code by semantic similarity. Prefer this when you know the intent but not the identifier.",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "query": {"type": "string"},
                        "limit": {"type": "number", "minimum": 1, "maximum": 20}
                    },
                    "required": ["query"]
                }
            ),
        ]
    @server.call_tool()
    async def call_tool(name: str, arguments: dict):
        if name == "semantic_search":
            result = await handle_semantic_search(**arguments)
            return {"content": [{"type": "text", "text": json.dumps(result)}]}
        raise ValueError(f"Unknown tool: {name}")
    return server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;semantic_search&lt;/code&gt; hides the embedding dimension and the distance metric. The model doesn't need to know that &lt;code&gt;ivec_distance&lt;/code&gt; is an L2 metric. It just needs a ranked list. The tool surface is a contract, not an implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Schema That Makes the Model Honest
&lt;/h2&gt;

&lt;p&gt;The SQLite schema is the quiet under-appreciated piece. An MCP request comes in, and a tool handler runs SQL. If the schema is sloppy, the model gets ambiguous results and starts to make things up. We designed the &lt;code&gt;chunks&lt;/code&gt; table with one urgent constraint: &lt;code&gt;UNIQUE(file_id, start_line)&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE files (
    id INTEGER PRIMARY KEY,
    path TEXT NOT NULL UNIQUE,
    language TEXT,
    last_commit_sha TEXT,
    last_modified_at TEXT,
    indexed_at TEXT,
    is_test BOOLEAN DEFAULT FALSE
);
CREATE TABLE chunks (
    id INTEGER PRIMARY KEY,
    file_id INTEGER REFERENCES files(id),
    start_line INTEGER NOT NULL,
    end_line INTEGER NOT NULL,
    text TEXT NOT NULL,
    chunk_embedding BLOB,          -- 768 floats, serialized
    symbol_names TEXT,             -- JSON array
    UNIQUE(file_id, start_line)
);
CREATE VIRTUAL TABLE chunks_vectors USING vec0(
    chunk_embedding FLOAT[768],
    chunk_id INTEGER
);
CREATE INDEX idx_chunks_symbol_names ON chunks(symbol_names);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;WARNING:&lt;/strong&gt; Don't store the embedding as a JSON string and hope sqlite-vec will parse it on the fly. It won't. Two separate tables — one for metadata, one virtual vector table — keep insertion and query fast. The vec0 virtual table selects for chunk_id and distance, which we then join back into the chunks table.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One decision that paid off: instead of storing embeddings for files, we store embeddings for &lt;em&gt;chunks&lt;/em&gt;, and every chunk carries &lt;code&gt;symbol_names&lt;/code&gt; as JSON. That makes semantic search over the &lt;code&gt;chunks&lt;/code&gt; table naturally useful for "where do we handle &lt;code&gt;refund&lt;/code&gt;" queries with line-level precision. The model doesn't need to guess a file path if the search tool gives back &lt;code&gt;src/payments/refunds.py:42&lt;/code&gt;. We also marked &lt;code&gt;is_test&lt;/code&gt; on the &lt;code&gt;files&lt;/code&gt; table and filtered it for retrieval by default, because the agent should not be learning happy-path patterns from test utility functions unless it explicitly asks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SQLite Over Postgres or a Vector Database
&lt;/h2&gt;

&lt;p&gt;We had this debate for two weeks. We spun up a &lt;code&gt;pgvector&lt;/code&gt; instance. We benchmarked Pinecone. We knew that in production, one might want a Postgres server anyway. But this project runs locally, on a developer's machine, often in a terminal with no Postgres. The deciding metric wasn't raw vector recall — it was cold-start time and dependency count. SQLite has zero external service to babysit, and &lt;code&gt;sqlite-vec&lt;/code&gt; compiles cleanly into a Python extension. That means you can clone this repo, run &lt;code&gt;python -m codebase_memory index .&lt;/code&gt;, and have a fully queryable local index in 40 seconds for a medium-sized repository. Meanwhile, a managed vector database requires an API key, a network call on every embedding lookup, and an orchestration layer to keep the remote index consistent with the local checkout. For a developer tool that should &lt;em&gt;disappear&lt;/em&gt; into the editor, local-first was the only rational choice.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;SQLite + &lt;code&gt;sqlite-vec&lt;/code&gt;
&lt;/th&gt;
&lt;th&gt;&lt;code&gt;pgvector&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Hosted vector DB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold-start time&lt;/td&gt;
&lt;td&gt;&amp;lt; 1s (no network)&lt;/td&gt;
&lt;td&gt;2–5s if local, more if remote&lt;/td&gt;
&lt;td&gt;10s+ (auth, handshake)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependency footprint&lt;/td&gt;
&lt;td&gt;Low (Python wheel, SQLite)&lt;/td&gt;
&lt;td&gt;Medium (Postgres server, extension)&lt;/td&gt;
&lt;td&gt;High (SDK, credentials)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Env-specific queries&lt;/td&gt;
&lt;td&gt;Native SQL + &lt;code&gt;ivec_distance&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;SQL + &lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Python API, no SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory / session mutations&lt;/td&gt;
&lt;td&gt;Trivially transactional&lt;/td&gt;
&lt;td&gt;Transactional, but heavier&lt;/td&gt;
&lt;td&gt;Requires logic in the API layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling ceiling&lt;/td&gt;
&lt;td&gt;GB-scale local corpora&lt;/td&gt;
&lt;td&gt;TB-scale shared&lt;/td&gt;
&lt;td&gt;TB-scale distributed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best use case&lt;/td&gt;
&lt;td&gt;Single-agent local context&lt;/td&gt;
&lt;td&gt;Multi-agent shared backend&lt;/td&gt;
&lt;td&gt;Cross-team semantic search&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We chose SQLite because the model's lifetime for a given MCP session is measured in minutes, not months. The index is a mirror of a specific checkout at a specific commit — ephemeral by design. If the code changes, re-index that file; don't build a warehouse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool Internals: The Guardrail Pattern
&lt;/h2&gt;

&lt;p&gt;The most interesting engineering pattern inside this server is what I call the guardrail: a two-phase read where we validate &lt;em&gt;the shape&lt;/em&gt; of the query before touching whatever the model asked to do. The reason is simple. A model's JSON output is occasionally malformed; its &lt;code&gt;arguments&lt;/code&gt; object can have the right key but a wildly out-of-range value. In one early session, the agent called &lt;code&gt;read_file_lines&lt;/code&gt; with &lt;code&gt;start_line = -1&lt;/code&gt; and &lt;code&gt;end_line = 1000000&lt;/code&gt;. We would happily have returned the entire file. So every tool now follows the same discipline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate types and bounds with a tiny custom validator that returns a &lt;em&gt;human-readable&lt;/em&gt; error message.&lt;/li&gt;
&lt;li&gt;Estimate the result size before executing (e.g., &lt;code&gt;SELECT COUNT(*)&lt;/code&gt; for a line range).&lt;/li&gt;
&lt;li&gt;Cap the result at a bounded size.&lt;/li&gt;
&lt;li&gt;Return &lt;code&gt;is_truncated: true&lt;/code&gt; if the result was clipped.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pattern prevents three failure modes we saw in the wild: unbounded memory bloat from a monster file read, silent hallucination from a truncated result that wasn't marked truncated, and cascading agent retries caused by unhelpful JSON-RPC error messages. Instead of &lt;code&gt;"Error: invalid value"&lt;/code&gt;, the model sees &lt;code&gt;"start_line must be &amp;gt;= 1; received -1"&lt;/code&gt;. That single change cut agent retry loops by a measurable share — roughly a third of our early task-completion failures traced back to the model misusing a tool because the error message was useless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Streamable HTTP Dance
&lt;/h2&gt;

&lt;p&gt;We started with &lt;code&gt;stdio&lt;/code&gt; transport. It's perfect for a local backend that your editor spawns. But when we wanted to run the server on a different machine and connect from a client over &lt;code&gt;http://localhost:8765&lt;/code&gt;, we had to upgrade to the streamable HTTP transport. That's where MCP's spec has sharp edges.&lt;/p&gt;

&lt;p&gt;The MCP Python SDK provides a &lt;code&gt;StreamableHTTPSessionManager&lt;/code&gt; and a &lt;code&gt;/message&lt;/code&gt; endpoint. The first naive iteration blocked on a single request per connection, which meant our client couldn't interleave tool calls. The fix was to run the server with &lt;code&gt;mcp.run(transport="streamable-http")&lt;/code&gt; and make sure the client kept the session ID in the &lt;code&gt;Mcp-Session-Id&lt;/code&gt; header across requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const transport = new StreamableHTTPClientTransport(
  new URL("http://localhost:8765/message"),
  {
    requestInit: {
      headers: {
        "Content-Type": "application/json",
        "Mcp-Session-Id": sessionId,
      },
    },
  }
);

const client = new Client({ name: "agent", version: "0.1.0" });
await client.connect(transport)

const result = await client.callTool({
  name: "semantic_search",
  arguments: { query: "how do we validate the webhook signature?", limit: 5 },
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TIP:&lt;/strong&gt; If you ever see MCP error -32001: Session not found, it's almost always because your transport client is not holding onto the Mcp-Session-Id header. The server rejects you as a new session and discards any pending tool state. Guard the session header like it's a bearer token.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The bigger protocol-level lesson: MCP servers should expose only a few slow operations as blocking calls. It is tempting to make &lt;code&gt;semantic_search&lt;/code&gt; asynchronously trigger an index rebuild if the corpus is stale. Don't. The model expects a tool call to return &lt;em&gt;something&lt;/em&gt; promptly. Long-running operations belong either in a separate MCP resource or behind a &lt;code&gt;read&lt;/code&gt;-style notification. We turned index refreshes into a one-off &lt;code&gt;/refresh&lt;/code&gt; tool that returns immediately and writes to a status row the agent can query next time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prompt Budget Math
&lt;/h2&gt;

&lt;p&gt;Every MCP tool call costs tokens — both the tool definitions in the prompt and the returned content. We obsessed over this. The default &lt;code&gt;mcp&lt;/code&gt; SDK sends a long description for each tool. With seven tools, the model context contains roughly 1,800 tokens before any actual code is retrieved. That's fine for a 200k-token context window, but it matters for smaller models that agents commonly drive. So we discovered a rule of thumb: every retrieved chunk should carry more information than the tokens it costs. A 1,024-token chunk is reasonable if the search is precise; a 4,096-line file read is a crime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def should_send_full_file(line_count: int, request_word_count: int) -&amp;gt; bool:
    """Return whether we should read a file or request a targeted range."""
    # A rough heuristic from our logs: models actually use &amp;lt; 30% of a file.
    keep_budget = 800  # target prompt overhead
    if line_count * 8 &amp;gt; keep_budget and request_word_count &amp;lt; 20:
        return False
    return True
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our token accounting led to three practical choices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;read_file_lines&lt;/code&gt; accepts &lt;code&gt;start_line&lt;/code&gt; and &lt;code&gt;end_line&lt;/code&gt;, not a &lt;code&gt;line_count&lt;/code&gt;. The model must specify a limited range.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;semantic_search&lt;/code&gt; returns at most 10 results, but we asked for 5 by default. After three failed iterations, we measured retrieval precision on the &lt;code&gt;semantic_search&lt;/code&gt; results and saw that the first 5 results contained the right answer 78% of the time; pushing to 10 added only 4 points. The extra 5 results were noise, not signal.&lt;/li&gt;
&lt;li&gt;Tool descriptions are ruthlessly short. We avoided prose and wrote them as imperative sentences: "Return at most &lt;code&gt;limit&lt;/code&gt; chunks similar to &lt;code&gt;query&lt;/code&gt;." The model doesn't need a paragraph of backstory for a search tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Failure Modes and How We Hardened Them
&lt;/h2&gt;

&lt;p&gt;Real MCP servers fail in ways that are invisible in demos. The first failure is embedding service drift: if you change your embedding model between indexing and query time, every vector distance becomes garbage. We solved this by storing the model name in an &lt;code&gt;index_info&lt;/code&gt; table and failing loudly on a mismatch — not silently serving results with a warning, because a model that receives a warning is still allowed to act on garbage.&lt;/p&gt;

&lt;p&gt;The second failure is stale index. The worst early bug was an agent modifying a file and then an MCP query returning the pre-modification lines. We set &lt;code&gt;files.last_modified_at&lt;/code&gt; based on git, and we added a &lt;code&gt;refresh&lt;/code&gt; flow that re-chunks files whose mtime changed relative to the index. We also decided to &lt;em&gt;never&lt;/em&gt; serve from the index if the HEAD commit changed since indexing; instead, &lt;code&gt;semantic_search&lt;/code&gt; returns an error asking the agent to call &lt;code&gt;/refresh&lt;/code&gt; first. A stale index is worse than no index.&lt;/p&gt;

&lt;p&gt;The third failure is latency spikes in the embedding call. The very first version embedded queries synchronously inside the tool handler, so a slow local model stalled the entire session. We moved embedding calls to a separate asyncio task pool and set a 500ms timeout on the embedding lookup, falling back to a trigram search if the embedding service was slow. The fallback isn't perfect, but a useful trivial match beats a timed-out far-away match.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Shipping Taught Me
&lt;/h2&gt;

&lt;p&gt;By the time we had the server running reliably, the agent's codebase understanding changed qualitatively. Instead of half-guessing file names, the model would say "let me check the refund service" and call &lt;code&gt;semantic_search("refund authorization flow")&lt;/code&gt;, then &lt;code&gt;read_file_lines("src/payments/refunds.py", 80, 140)&lt;/code&gt; — a real plan. Over a set of 50 internal issue-resolution tasks, the agent completed them in about a third fewer turns on average, and the number of invented file paths dropped to near zero.&lt;/p&gt;

&lt;p&gt;The biggest lesson, though, is about the interface, not the model. Building an MCP server forces you to think about what an &lt;em&gt;external&lt;/em&gt; mind needs to know about your codebase. That editorial process is valuable even if you never use the MCP server. The seven tools we kept read like a caretaker's checklist: What is the structure? What does this symbol mean? Where is it used? In answering those, we surfaced the hidden assumptions in our own codebase — modules we thought were named clearly, and function names that everyone in the office "knew" but the rest of the world could never find.&lt;/p&gt;

&lt;p&gt;If you're going to extend an AI with custom tools, build them as if the model were a new developer, not a superintelligence. Give it an index. Give it a cork board. Teach it to search by intent before searching by identifier. And above all, give it a protocol that doesn't blur the line between memory and fact.&lt;/p&gt;

&lt;h3&gt;
  
  
  FAQ
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Do I need a vector database to build an MCP server for codebase memory?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No. For local repositories under roughly a gigabyte of source text, SQLite with sqlite-vec is faster to set up, simpler to debug, and has no network latency. A dedicated vector database is worth it only when you have multiple agents sharing a central index or persisting across many machines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; How large should the text chunks be for code embedding?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; We started at 512 tokens with no overlap, then settled on 1,024 tokens with a 128-token overlap. That balance gave us line-level precision for symbols while avoiding fragmenting function bodies across chunks. The overlap prevents us from losing a symbol that straddles a boundary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; What are the downsides of exposing raw SQL via MCP tools?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Raw SQL hides too much semantic intent from the model and invites injection-style errors. A model that reads an entire table might "fix" something that wasn't broken. The MCP tool interface should be a curated set of read-only operations with sensible defaults; keep the SQL on the server side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Will the MCP transport stay stable?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; The protocol is evolving — streamable HTTP is still being finalized — but the core abstraction of tools, resources, and prompts is stable. We version-lock the mcp and @modelcontextprotocol/sdk packages, and we isolate transport-specific code so that changing from stdio to HTTP is a one-line switch inside our SDK.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The custom MCP server did not make the model unconditionally trustworthy — nothing will. What it changed was the cost of a mistake. Instead of a single hallucinated path that could waste minutes of agent loops, our tools constrained the model to a small search space with bounded, honest results. The server is a boundary that separates "the model knows" from "the model can find out," and that boundary is the most important thing we built.&lt;/p&gt;

&lt;p&gt;We also learned that the protocol itself is a design discipline. JSON-RPC schemas for tool inputs, explicit result sizes, and clear failure messages all forced us to define the semantics of a codebase lookup precisely. There is no "intuitive" version of &lt;code&gt;semantic_search&lt;/code&gt;; there is only the version whose parameters you can write a contract for, and the version that behaves badly when the model guesses wrong.&lt;/p&gt;

&lt;p&gt;If you are adding memory and context to your AI agent, don't just embed the README. Index the symbols, the file structure, the tests, and the commit history, then expose them through a small, deliberate interface. The model will still be wrong. But it will be wrong on a tighter leash, with better clues, and far less confidence.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/anomalyco/codebase-memory-mcp" rel="noopener noreferrer"&gt;View the project on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>aiagents</category>
      <category>llm</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Sitemaps Are Free SEO — Here's the Vite Plugin That Generates Yours</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Sun, 16 Aug 2026 22:59:14 +0000</pubDate>
      <link>https://dev.to/3ni8ma/sitemaps-are-free-seo-heres-the-vite-plugin-that-generates-yours-3lg4</link>
      <guid>https://dev.to/3ni8ma/sitemaps-are-free-seo-heres-the-vite-plugin-that-generates-yours-3lg4</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Every static site I've shipped has gone through the same ritual: build the site, then manually write a sitemap.xml, forget to update it when a route changes, and discover a month later that Google indexed half my pages. Sitemaps are boring, so they get done wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/vite-plugin" rel="noopener noreferrer"&gt;@3ni8ma/vite-plugin-sitemap&lt;/a&gt; generates &lt;code&gt;sitemap.xml&lt;/code&gt; (and &lt;code&gt;robots.txt&lt;/code&gt;, if you want it) as part of your Vite build. You declare your routes once, and every build emits a fresh, correct sitemap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;sitemap&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@3ni8ma/vite-plugin-sitemap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;sitemap&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/about&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/projects&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/contact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;changefreq&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;monthly&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&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;That's it. &lt;code&gt;vite build&lt;/code&gt; now writes a sitemap into &lt;code&gt;dist/&lt;/code&gt; automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Options Worth Knowing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;Default&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;changefreq&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;monthly&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Change frequency per URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;priority&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Crawl priority (0.0–1.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;lastmod&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;now&lt;/td&gt;
&lt;td&gt;Last-modified date&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;robotsTxt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Also generate &lt;code&gt;robots.txt&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disallow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Paths to disallow in robots.txt&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;disallow&lt;/code&gt; option is the sleeper feature — point it at your admin routes and staging paths, and your robots.txt stays honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Bother
&lt;/h2&gt;

&lt;p&gt;Sitemaps are the difference between search engines discovering your site in days versus weeks. For a static site, it's one dependency and ten lines of config. The plugin is on &lt;a href="https://www.npmjs.com/package/@3ni8ma/vite-plugin-sitemap" rel="noopener noreferrer"&gt;npm&lt;/a&gt;, takes zero runtime dependencies, and runs entirely at build time.&lt;/p&gt;

&lt;p&gt;If you ship static sites with Vite, this is five minutes that pays for itself in crawl coverage.&lt;/p&gt;

</description>
      <category>vite</category>
      <category>seo</category>
      <category>javascript</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Glassmorphism Is Back — and This Tailwind Plugin Makes It Painless</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Sun, 16 Aug 2026 22:59:13 +0000</pubDate>
      <link>https://dev.to/3ni8ma/glassmorphism-is-back-and-this-tailwind-plugin-makes-it-painless-3hn8</link>
      <guid>https://dev.to/3ni8ma/glassmorphism-is-back-and-this-tailwind-plugin-makes-it-painless-3hn8</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Frosted-glass UI is everywhere again — every dashboard, every AI chat sidebar, every "spatial" web app. But hand-rolling the effect means repeating the same four-line blur recipe in every component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;background&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;rgba&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;08&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;backdrop-filter&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nt"&gt;blur&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;12&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;border&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;1&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt; &lt;span class="nt"&gt;solid&lt;/span&gt; &lt;span class="nt"&gt;rgba&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;255&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="err"&gt;12&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;border-radius&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="err"&gt;12&lt;/span&gt;&lt;span class="nt"&gt;px&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the moment a design system has three glass variants, those four lines mutate into a maintenance problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/tailwind-plugin" rel="noopener noreferrer"&gt;@3ni8ma/tailwind-plugin&lt;/a&gt; turns the glass recipe into design tokens you can apply anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"glass-sm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Subtle frosted panel&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"glass-md"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Standard glass card&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"glass-lg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Heavy blur, strong border&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the plugin to your Tailwind config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tailwind.config.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@3ni8ma/tailwind-plugin&lt;/span&gt;&lt;span class="dl"&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;Every &lt;code&gt;glass-*&lt;/code&gt; utility is backed by CSS variables, so you can retheme the entire glass layer from one place — blur radius, border opacity, tint, everything.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;glass-sm / glass-md / glass-lg&lt;/strong&gt; — three depth levels of the frosted effect&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS-variable driven&lt;/strong&gt; — retheme globally, not per-component&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark-mode friendly&lt;/strong&gt; — tints and borders adjust with your palette&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero runtime&lt;/strong&gt; — compiles down to plain utilities at build time&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why It's Worth It
&lt;/h2&gt;

&lt;p&gt;Consistency. The whole point of a design system is that "glass" means one thing everywhere. This plugin makes the glass recipe a named concept instead of a copy-paste ritual — and when the design changes, you change the tokens, not fifty components.&lt;/p&gt;

&lt;p&gt;If your project leans into frosted surfaces, it's on &lt;a href="https://www.npmjs.com/package/@3ni8ma/tailwind-plugin" rel="noopener noreferrer"&gt;npm&lt;/a&gt; and takes one line to install.&lt;/p&gt;

</description>
      <category>tailwindcss</category>
      <category>css</category>
      <category>design</category>
      <category>plugin</category>
    </item>
    <item>
      <title>Building HELIOS: A Gesture-Controlled AI OS in the Browser</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Sat, 15 Aug 2026 17:11:06 +0000</pubDate>
      <link>https://dev.to/3ni8ma/building-helios-a-gesture-controlled-ai-os-in-the-browser-182i</link>
      <guid>https://dev.to/3ni8ma/building-helios-a-gesture-controlled-ai-os-in-the-browser-182i</guid>
      <description>&lt;h2&gt;
  
  
  The wager: an OS in a browser tab
&lt;/h2&gt;

&lt;p&gt;I started HELIOS with three constraints: no native code, no head-mounted display, and no integration that requires a user to install a runtime. The browser would be the OS, the webcam would be the sensor, and WebGL would be the display. That was the whole pitch.&lt;/p&gt;

&lt;p&gt;The first prototype was ugly. I rendered a single flat plane in Three.js, put a fake window texture on it, and used MediaPipe’s hand landmarks to push a cursor around. It felt like a tech demo with a lot of lag. The breakthrough came when I stopped emulating a mouse and started modeling gestures. The first time I pinched two fingers together, dragged a plane sideways, and watched it move as a physical object while the camera tracked my hand, the wager felt real. No Bluetooth controller. No QR code setup. No proprietary SDK. Just a Chrome tab, a laptop camera, and a few dozen lines of TypeScript.&lt;/p&gt;

&lt;p&gt;The technical bet inside that wager is that WebXR is the wrong abstraction for spatial computing on devices people already own. WebXR is great when you want to strap on a headset and leave the room. But most spatial interactions happen while you are sitting in front of a screen, and the most durable spatial sensor you already have is a camera.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not WebXR? Choosing Three.js over the XR ecosystem
&lt;/h2&gt;

&lt;p&gt;WebXR promises a fully immersive 3D scene with controllers, room tracking, and depth sensing. It also demands either a dedicated VR headset or an AR-capable phone. Our target device is a normal laptop with a webcam and a desktop browser. In that environment WebXR does not provide hand tracking without a headset that has built-in cameras. That was the deal-breaker.&lt;/p&gt;

&lt;p&gt;I evaluated both stacks early, and the tradeoffs were clear:&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;WebXR&lt;/th&gt;
&lt;th&gt;Three.js + MediaPipe Hands&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hardware required&lt;/td&gt;
&lt;td&gt;VR/AR headset or AR-capable phone&lt;/td&gt;
&lt;td&gt;Any webcam + browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Input source&lt;/td&gt;
&lt;td&gt;Controller button/trigger events&lt;/td&gt;
&lt;td&gt;21 landmarks from RGB camera&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immersion&lt;/td&gt;
&lt;td&gt;Full screen/exclusive XR session&lt;/td&gt;
&lt;td&gt;Augments the existing desktop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hand tracking&lt;/td&gt;
&lt;td&gt;Only on specific devices&lt;/td&gt;
&lt;td&gt;Works in any Chromium tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fallback input&lt;/td&gt;
&lt;td&gt;Difficult to implement&lt;/td&gt;
&lt;td&gt;Native mouse/keyboard fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distribution&lt;/td&gt;
&lt;td&gt;Requires browser XR API support&lt;/td&gt;
&lt;td&gt;Plain web app, open any URL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Choosing Three.js had a hidden benefit: HELIOS runs in a pop-up window next to your existing apps. It does not claim exclusive control over the display. That is closer to how people actually use AI on a daily basis: a copilot beside you, not a world replacing your monitor.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;INFO:&lt;/strong&gt; Treat the hand as an event bus, not as a cursor. A cursor implies position only; a hand implies predicates like pinching, pointing, moving, and resting.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Hand tracking as an input bus, not a party trick
&lt;/h2&gt;

&lt;p&gt;The first mistake I made with MediaPipe was treating landmark data as a single cursor position. MediaPipe Hands gives you 21 landmarks per hand, each with normalized &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;, &lt;code&gt;z&lt;/code&gt;, and visibility. But the meaning of that data only exists across time. A pinch is not a frame; it is a transition from open fingers to closed fingers. A drag is not a coordinate; it is a pinch that stays held while the wrist moves.&lt;/p&gt;

&lt;p&gt;So I built an input bus. Every time MediaPipe produced a hand frame, we normalized it into a &lt;code&gt;HandFrame&lt;/code&gt; object and published it to every subsystem that cared: the gesture FSM, the window hit-tester, a dwell tracker, and the AI context collector. Each subscriber reacted independently and asynchronously. That separation stopped the UI from coupling to the MediaPipe update loop.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export type Handedness = 'left' | 'right';

export interface HandFrame {
  timestamp: number;
  handedness: Handedness;
  landmarks: Float32Array; // 63 floats: 21 landmarks * (x, y, z), normalized
  scale: number;           // hand scale used to normalize distances
}

type HandListener = (frame: HandFrame) =&amp;gt; void;

export class HandBus {
  private listeners = new Set&amp;lt;HandListener&amp;gt;();
  private latestFrame: HandFrame | null = null;

  publish(frame: HandFrame): void {
    this.latestFrame = frame;
    for (const listener of this.listeners) listener(frame);
  }

  subscribe(listener: HandListener): () =&amp;gt; void {
    this.listeners.add(listener);
    return () =&amp;gt; this.listeners.delete(listener);
  }

  peek(): HandFrame | null {
    return this.latestFrame;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key decision here was allocation discipline. MediaPipe runs at 30 fps; if every frame creates a new array and then throws it away, the garbage collector will cause stutter. We reuse a single &lt;code&gt;Float32Array&lt;/code&gt; and copy into it. The &lt;code&gt;HandBus&lt;/code&gt; is the only place where raw frames are allowed to be mutated; subscribers read the same frame and derive their own state from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gesture grammar: from raw landmarks to deterministic intents
&lt;/h2&gt;

&lt;p&gt;The core contribution of HELIOS is a small gesture grammar built as a finite-state machine per hand. MediaPipe’s classification models are excellent at telling you which finger is extended, but they are not good at answering the question "is this a click, a drag, or a swipe?" That question requires temporal context. A pinch that lasts 80ms and returns to open is a click. A pinch that stays under 0.35 normalized distance for 150ms while the hand moves is a drag. A pinch that rapidly opens then closes again is a flick, which we route to window close or mode switch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two-stage classification
&lt;/h3&gt;

&lt;p&gt;We use a two-stage pipeline. Stage one converts raw landmarks into a clean distance metric: the normalized Euclidean distance between the thumb tip and index fingertip. Dividing by hand scale makes the metric invariant to whether the user is sitting close to or far from the camera. Stage two feeds that scalar into a state machine with hysteresis.&lt;/p&gt;

&lt;p&gt;The hysteresis was non-negotiable. A raw distance threshold will flicker at the boundary; every gesture system hits the same problem. We close the pinch at 0.30 and only release it at 0.42. Between those thresholds the FSM keeps whatever state it is already in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export enum GestureState {
  Rest = 'rest',
  Pinching = 'pinching',
  Dragging = 'dragging',
}

const PINCH_CLOSE = 0.30;
const PINCH_OPEN = 0.42;
const DRAG_HOLD_MS = 120;

export class GestureFSM {
  private state = GestureState.Rest;
  private pinchEnteredAt = 0;

  update(distance: number, now: number): GestureState {
    switch (this.state) {
      case GestureState.Rest:
        if (distance &amp;lt; PINCH_CLOSE) {
          this.state = GestureState.Pinching;
          this.pinchEnteredAt = now;
        }
        break;

      case GestureState.Pinching:
        if (distance &amp;gt; PINCH_OPEN) {
          this.state = GestureState.Rest;
        } else if (now - this.pinchEnteredAt &amp;gt;= DRAG_HOLD_MS) {
          this.state = GestureState.Dragging;
        }
        break;

      case GestureState.Dragging:
        if (distance &amp;gt; PINCH_OPEN) {
          this.state = GestureState.Rest;
        }
        break;
    }
    return this.state;
  }

  reset(): void {
    this.state = GestureState.Rest;
    this.pinchEnteredAt = 0;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Steady-state detection
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Dragging&lt;/code&gt; state only activates if the user holds a pinch for 120ms. That small delay eliminates the ambiguity between click and drag. We also track a "rest" signal: if the centroid of the hand stays within 0.02 normalized coordinates for 600ms, we freeze the pointer. This solved the "Midas touch" problem, where every involuntary hand movement becomes a command. The pointer needs intent, not presence.&lt;/p&gt;

&lt;p&gt;This grammar is intentionally small. We only support five meaningful gestures: point to hover, pinch to select, pinch-hold to drag, open palm to open the launcher, and a fist to dismiss. Each gesture maps to a deterministic &lt;code&gt;UserIntent&lt;/code&gt; that the rest of the system can reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The spatial window manager: z-index is a lie
&lt;/h2&gt;

&lt;p&gt;In a DOM-based window manager, stacking order is a single integer. When two windows overlap, the browser resolves visibility by painting order. HELIOS does not use the DOM for windows; it uses Three.js planes in a 3D scene. That seems more powerful, but it introduces a problem: real people do not want to rotate windows in three dimensions. They want the same flat rectangles they already understand, but with the ability to reach out and push them aside.&lt;/p&gt;

&lt;p&gt;The lesson I learned is that &lt;code&gt;position.z&lt;/code&gt; is not z-index. A window depth value must participate in raycasting, occlusion, and idle animation, but user interaction should stay in screen-space. In HELIOS, every window is a plane constrained to face the camera. Dragging moves the plane in &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt;; the &lt;code&gt;z&lt;/code&gt; coordinate only defines stacking order and a subtle scale animation when a window is brought to the front.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import * as THREE from 'three';

export interface SpatialWindow {
  id: string;
  mesh: THREE.Mesh;
  depth: number;
}

export function pickWindow(
  windows: SpatialWindow[],
  ndc: THREE.Vector2,
  camera: THREE.PerspectiveCamera
): SpatialWindow | null {
  const raycaster = new THREE.Raycaster();
  raycaster.setFromCamera(ndc, camera);

  const meshes = windows.map(w =&amp;gt; w.mesh);
  const hits = raycaster.intersectObjects(meshes, false);

  if (hits.length === 0) return null;
  const hitMesh = hits[0].object as THREE.Mesh;
  return windows.find(w =&amp;gt; w.mesh === hitMesh) ?? null;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;SpatialWindow&lt;/code&gt; also stores an unrounded "desired depth." When you pinch a window and push it forward, we animate it toward that depth with a spring. The actual stacking order in the Three.js scene is determined by sorting by depth, but a window is never allowed to grow so large that it occludes the camera. That would be a browser full-screen tab pretending to be an OS; it defeats the spatial metaphor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring AI into the input loop
&lt;/h2&gt;

&lt;p&gt;The "AI" in HELIOS is not a chatbot bolted on to the side of a canvas. It is an intent parser that receives a structured context object assembled from the same input bus. When you point at a window and say "summarize this," the system builds a prompt with the window’s metadata, the current active element if available, and the transcript. It then calls an LLM through a small proxy server. The response renders into a new spatial window placed next to the one you referenced.&lt;/p&gt;

&lt;p&gt;The crucial rule: the AI is never allowed to block a gesture. We measured the latency budget. Pinch-to-click must feel instantaneous, so the FSM stays under 10ms. An LLM round-trip is often 1-2 seconds. If the AI were in the critical path, every gesture would feel broken. Instead, we split the pipeline. Gestures mutate window state immediately. AI responses arrive asynchronously and update a sidecar state object that the renderer picks up on the next &lt;code&gt;requestAnimationFrame&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We also limited what the LLM can control. It cannot move the pointer. It cannot summon the camera. It can only request actions through a typed &lt;code&gt;Command&lt;/code&gt; object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "verb": "summarize",
  "targetWindowId": "window://notes",
  "language": "en",
  "timestamp": 1725804321000
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command grammar keeps the AI honest and makes failures debuggable. If the model gives us a malformed verb, we discard it silently and log it. The hand remains the root of truth; the AI is an advisor, not a driver.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance: staying at 60 fps with a webcam in the loop
&lt;/h2&gt;

&lt;p&gt;The hardest performance challenge in HELIOS was not rendering. It was keeping the frame budget stable while two CPU/GPU-heavy libraries fight for the same thread. MediaPipe Hands can take 12-18ms per frame on a mid-range laptop GPU, and Three.js rendering of 50-80 draw calls costs another 3-5ms. If you are not careful, you end up with 35 fps and a stuttering window manager.&lt;/p&gt;

&lt;p&gt;We adopted a simple scheduling model. The camera and MediaPipe inference run at 30 fps, but the Three.js renderer runs on &lt;code&gt;requestAnimationFrame&lt;/code&gt; at 60 fps. The &lt;code&gt;HandBus&lt;/code&gt; is the bridge. On every raw frame, we publish to subscribers. If a subscriber like the window manager needs to update, it marks itself dirty. The renderer only re-renders the scene when at least one dirty flag is set or an animation is active. When the hand is still, the frame cost drops to near zero.&lt;/p&gt;

&lt;p&gt;We also learned to measure and cap allocation. The biggest hidden cost was temporary typed arrays for landmark coordinates. Reusing a single &lt;code&gt;Float32Array(63)&lt;/code&gt; buffer saved us from hundreds of garbage collections per second. The effect on frame pacing was immediate: the &lt;code&gt;requestAnimationFrame&lt;/code&gt; callback stopped getting interrupted by GC pauses.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;WARNING:&lt;/strong&gt; Never allocate a multi-thousand-element typed array in the same synchronous path as hand tracking. A hidden GC pause is the difference between a buttery 60fps and a stutter that feels like a bug in the AI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The production tuning numbers were concrete: MediaPipe at 640x480, 30 fps, running on an RTX 3060 laptop GPU; Three.js with 70 draw calls; total hand-to-screen latency around 45ms. We did not chase zero latency because vision-based hand tracking is inherently late by one or two video frames. Instead we made the remaining latency predictable and visually consistent. When a window follows your hand with a constant 45ms delay, the brain compensates. When the delay wavers between 20ms and 90ms, the gesture feels broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure modes that taught us the most
&lt;/h2&gt;

&lt;p&gt;Building HELIOS produced a long list of failures. I want to highlight the four that had the most architectural impact.&lt;/p&gt;

&lt;p&gt;First, the Midas touch. The first build mapped every hand movement to a cursor position, which meant my resting hand near the keyboard would move the pointer across the screen. We fixed it with the rest-state detection described above, plus a spatial "activation zone" near the center of the camera frame. The hand has to be inside that zone before its motion counts.&lt;/p&gt;

&lt;p&gt;Second, calibration drift. MediaPipe normalized coordinates assume a default camera FOV and aspect ratio, but a user’s webcam might be mounted above or below their monitor. The mapping from hand coordinates to screen coordinates is an affine transform we compute once at startup. We show a quick calibration screen where the user moves their palm to four corners. From then on, the pointer tracks. The math is simple, but without it the pointer lands ten centimeters off.&lt;/p&gt;

&lt;p&gt;Third, click versus drag ambiguity. We solved that with the 120ms hold delay in the FSM, but it introduced a new failure: users would try to drag and then release before the transition, resulting in a "sticky drag" where the window jumps forward. We added a visual affordance: the window scales by 1.02 when it enters &lt;code&gt;Dragging&lt;/code&gt;, so users know they have crossed the threshold.&lt;/p&gt;

&lt;p&gt;Fourth, privacy. The webcam feed never leaves the browser. MediaPipe runs locally, and the only data sent to the LLM proxy is a JSON object with gestures, target window metadata, and a text transcript. Users need to know the difference between "the browser sees my hand" and "the AI sees my hand." We put a persistent indicator in the corner of the shell showing whether landmarks or raw frames are being transmitted. Making privacy visible made the architecture better because it forced us to keep inference on-device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the browser won for spatial computing
&lt;/h2&gt;

&lt;p&gt;I started this project expecting the browser to be the weakest link. It turned out to be the strongest. The reason is not technical novelty; it is permission and distribution. Every operating system has a camera permission model, but the web is the only one where I can send someone a URL and have them running a spatial AI interface in under a minute, no driver installs, no app store review, no headset pairing.&lt;/p&gt;

&lt;p&gt;WebAssembly and WebGPU have quietly turned the browser into a legitimate high-performance compute target. MediaPipe Hands runs in WebAssembly with GPU acceleration. Three.js renders thousands of objects at 60fps. The missing ingredient was never runtime performance. It was interaction design that treats the camera as a first-class input device rather than a source of video to be displayed back to the user.&lt;/p&gt;

&lt;p&gt;The browser also wins because it supports graceful degradation. When the camera fails, HELIOS falls back to the mouse. When the hand leaves the frame, the window manager stays interactive. That resilience is built into the DOM. A native spatial app usually has no mouse fallback, so it feels like a dead end. Our browser-based shell can always return to the desktop metaphor, which makes the spatial metaphor feel like an enhancement instead of a takeover.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Treat gestures as temporal state transitions, not single-frame classifications.&lt;/li&gt;
&lt;li&gt;Use an event bus between hand tracking, window management, and AI so no subsystem blocks the input loop.&lt;/li&gt;
&lt;li&gt;Hysteresis and rest-state detection are the cheapest fixes for the Midas touch and jitter.&lt;/li&gt;
&lt;li&gt;Keep the AI out of the critical path; gestures mutate the UI immediately, AI updates asynchronously.&lt;/li&gt;
&lt;li&gt;Reuse typed arrays and measure allocation pressure in real-time input pipelines.&lt;/li&gt;
&lt;li&gt;The browser is a viable spatial computing runtime when you design for fallback and portability.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  FAQ
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Does HELIOS require specialized hardware like a VR headset or depth sensor?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No. A standard webcam with at least 640x480 resolution is enough. MediaPipe Hands estimates 3D landmarks from a single RGB frame, so no depth camera is needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Is HELIOS actually an operating system?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; No, it is an AI shell and spatial interaction layer that runs inside a browser tab. It manages its own windows, handles gesture input, and coordinates AI context, but it does not manage processes or files. It is an OS-inspired interface, not a kernel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; How private is the webcam feed?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; The raw webcam frames never leave the browser. MediaPipe hand tracking runs locally in the tab. The only external request is a JSON payload containing landmarks, window metadata, and optional text transcript, which is sent to the configured LLM proxy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q:&lt;/strong&gt; Can I use HELIOS without the AI features?&lt;br&gt;
&lt;strong&gt;A:&lt;/strong&gt; Yes. The gesture-driven window manager is independent from the LLM integration. If you do not configure an API key, the shell simply skips AI actions and works as a spatial desktop interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;HELIOS was an attempt to prove that the future of spatial computing does not require a headset. It requires a camera, a browser, and an interaction model that respects the difference between a hand and a mouse. I learned more about input latency and interaction design than I did about AI. That was the surprise: the LLM side was straightforward, but making a pinch feel like a physical grip took weeks of tuning.&lt;/p&gt;

&lt;p&gt;The architecture that survives is the one that separates concerns: hand tracking as a bus, gestures as a finite-state machine, windows as Three.js planes, and AI as an asynchronous advisor. Each layer is independent, and that independence is what makes the whole system robust. When the AI fails, gestures still work. When the camera fails, the mouse still works. That is not a failure of vision; it is the opposite. It is a vision that can ship.&lt;/p&gt;

&lt;p&gt;The browser is not a toy runtime. It is the most portable, most permissive, and most resilient platform we have. If you want to see what a browser-native AI shell feels like, the code is open. Clone it, plug in a webcam, and pinch your way through a desktop that no longer needs a mouse.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/HELIOS" rel="noopener noreferrer"&gt;View the project on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>helios</category>
      <category>threejs</category>
      <category>mediapipe</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why Rust is the Future of Systems Programming</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Thu, 13 Aug 2026 18:38:06 +0000</pubDate>
      <link>https://dev.to/3ni8ma/why-rust-is-the-future-of-systems-programming-34a9</link>
      <guid>https://dev.to/3ni8ma/why-rust-is-the-future-of-systems-programming-34a9</guid>
      <description>&lt;h2&gt;
  
  
  The Ownership Revolution
&lt;/h2&gt;

&lt;p&gt;Memory safety without garbage collection, fearless concurrency, and zero-cost abstractions — Rust is reshaping how we build infrastructure. A deep dive into ownership, borrowing, lifetimes, and where Rust excels over C/C++.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ownership Revolution
&lt;/h2&gt;

&lt;p&gt;Rust's ownership model eliminates entire categories of bugs at compile time. Unlike C's manual memory management or Java's GC, Rust's borrow checker enforces strict rules about who can read and write memory. This section breaks down ownership, borrowing, and lifetimes with practical examples of how they prevent use-after-free, double-free, and data races.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fearless Concurrency
&lt;/h2&gt;

&lt;p&gt;Rust's Send and Sync traits make data-race detection a compile-time concern. The standard library's channels, mutexes, and atomic types are designed around these traits. We'll walk through building a concurrent web scraper that Rust guarantees is thread-safe before it ever runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero-Cost Abstractions
&lt;/h2&gt;

&lt;p&gt;Iterators, closures, and generics in Rust compile down to the same machine code as hand-written loops. There is no runtime overhead for abstractions — you pay only for what you use. Benchmarks comparing Rust iterators to C loops demonstrate identical assembly output.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ecosystem: Cargo and Crates.io
&lt;/h2&gt;

&lt;p&gt;Cargo is more than a package manager — it handles builds, tests, benchmarks, documentation, and dependency resolution. The crate ecosystem has matured rapidly, with production-grade libraries for HTTP (reqwest), async runtimes (tokio), serialization (serde), and web frameworks (axum).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Rust Falls Short
&lt;/h2&gt;

&lt;p&gt;Compile times are the most common complaint. Incremental compilation has improved dramatically, but large projects still take minutes to build. Learning curve is steep — the borrow checker fights new users. And Rust's niche in GUI and game development remains small compared to C++.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Verdict
&lt;/h2&gt;

&lt;p&gt;Rust has already won in infrastructure: the Linux kernel now accepts Rust, Cloudflare uses it for edge services, and Discord migrated from Go to Rust for performance-critical paths. For systems programming, embedded, and performance-sensitive applications, Rust is the default choice for new projects.&lt;/p&gt;




&lt;p&gt;Rust's combination of safety, speed, and developer tooling is unmatched in systems programming. While the learning curve is real, the long-term payoff — fewer production bugs, fearless refactoring, and predictable performance — justifies the investment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/rust-projects" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>systemsprogramming</category>
      <category>memorysafety</category>
      <category>concurrency</category>
    </item>
    <item>
      <title>Understanding GPU Memory: VRAM, Bandwidth, and Why Your Model Won't Fit</title>
      <dc:creator>Aarush Karak</dc:creator>
      <pubDate>Thu, 13 Aug 2026 18:33:05 +0000</pubDate>
      <link>https://dev.to/3ni8ma/understanding-gpu-memory-vram-bandwidth-and-why-your-model-wont-fit-doc</link>
      <guid>https://dev.to/3ni8ma/understanding-gpu-memory-vram-bandwidth-and-why-your-model-wont-fit-doc</guid>
      <description>&lt;h2&gt;
  
  
  HBM Architecture
&lt;/h2&gt;

&lt;p&gt;GPU memory is the most constrained resource in ML. This post explains HBM architecture, memory bandwidth vs compute, how model size translates to VRAM usage, and techniques (offloading, recomputation, sharding) to fit larger models.&lt;/p&gt;

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

&lt;p&gt;High Bandwidth Memory (HBM) stacks DRAM dies vertically with through-silicon vias (TSVs) connecting them. HBM2e offers 2.4 GB/s per pin, HBM3 reaches 6.4 GB/s. The A100 has 80GB HBM2e at 2TB/s, the H100 has 80GB HBM3 at 3.35TB/s. Understanding this hierarchy explains why memory bandwidth — not FLOPs — is the bottleneck for transformer inference.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Does the Memory Go?
&lt;/h2&gt;

&lt;p&gt;A 70B parameter model at FP16 needs 70e9 * 2 bytes = 140GB just for weights. Adam optimizer states add another 140GB (momentum + variance at FP32). Gradients add 70GB at FP16. Activations for a 4096-token sequence add ~15GB. Total: ~365GB for training on a single GPU — why 8x A100s are needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory-Efficient Attention
&lt;/h2&gt;

&lt;p&gt;Standard attention computes S = Q@K^T (shape: batch x heads x seq x seq), materializing the full attention matrix. FlashAttention tiles the computation, never storing the full S matrix — reducing memory from O(N^2) to O(N). For a 4096-token sequence, this saves ~500MB per layer. For 80 layers: 40GB saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Activation Recomputation (Checkpointing)
&lt;/h2&gt;

&lt;p&gt;During forward pass, activations are stored for the backward pass. Checkpointing saves only a subset of activations and recomputes the rest during backward. The memory savings are proportional to how many checkpoints are kept. Trading 20% more compute for 50-80% less memory is often worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model Parallelism: Sharding Across GPUs
&lt;/h2&gt;

&lt;p&gt;Model parallelism splits layers across devices. Tensor parallelism shards individual matrix multiplies across GPUs (requires high-bandwidth interconnect like NVLink). Pipeline parallelism assigns layer groups to different devices. Fully Sharded Data Parallelism (FSDP) shards optimizer states, gradients, and parameters across data-parallel workers.&lt;/p&gt;

&lt;h2&gt;
  
  
  CPU Offloading
&lt;/h2&gt;

&lt;p&gt;When even sharded memory doesn't fit, parameters are offloaded to CPU RAM and fetched to GPU on demand. This is slow (PCIe 4.0 x16: ~32GB/s vs HBM's 2TB/s) but enables training models up to ~10x larger than GPU memory alone. Inference offloading is more practical because weights are static and prefetching is predictable.&lt;/p&gt;




&lt;p&gt;GPU memory management is the defining engineering challenge of large-scale ML. Understanding the memory hierarchy, activation memory costs, and parallelism strategies is essential for fitting increasingly large models into available hardware.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/3ni8ma/aura-finance" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gpu</category>
      <category>cuda</category>
      <category>vram</category>
      <category>memory</category>
    </item>
  </channel>
</rss>
