tutorial #10: 148 Opens, 0 Replies — How My Forge Cold Email v1 Completely Failed

Zenn version: https://zenn.dev/mintototo1/articles/buildlog-2026-05-23-cold-email-v2

I sent 100 emails and got 148 opens.

Someone opened multiple times. People were reading. But 0 replies.

Between May 12–14, I sent 100 cold emails through Instantly for Forge — my AI engineering placement side project. Targeted companies actively hiring AI engineers, personalized each one.

Results:

  • Opens: 148 (including multiple opens per recipient)
  • Replies: 0
  • Bookings: 0

The subject lines worked. People were reading. Nobody replied. I couldn’t figure out why. So I went back and read the body.

The problem was the offer sequence

My v1 email led with: “Freelance AI engineer placement, 30% fee, I’ll send you details.”

If you get an email from someone you’ve never heard of, and it immediately says “want to start a 30% fee arrangement? I’ll send details” — you don’t reply. That’s obvious. I just didn’t see it.

The first email should give something for free, not pitch. I skipped that step. The people who opened probably thought “not now” and closed it. That’s all it was.

What I changed in v2

Changed first-touch offer to: free 15-min audit + 1-page AI hiring roadmap.

// v1: expecting a reply
I'll send you details.
Please reach out at your convenience.

// v2: calendar booking completes the action
Let me audit your AI hiring setup in 15 minutes
and put together a 1-page roadmap.
Book here → https://cal.com/mint-office-hours/free-15min

Also cut body from 280–400 chars down to 200–320 chars.

One next action: calendar booking. No waiting for a reply.

v2 results aren’t in yet — sent this week, will write about it next week. No idea if the 0 moves.

9 in 10 Docker Compose files skip the basic security flags

Most common findings across the corpus, by share of parsed files affected, coloured by severity

I created compose-lint, a security linter for Docker Compose files, and pointed it at 6,444 public docker-compose.yml and compose.yaml files from GitHub. (More on why below.)

Three numbers stood out:

  • 91% of the files that parse have at least one security finding.
  • 68% have at least one HIGH or CRITICAL finding.
  • The same three issues top every category I looked at, including the official vendor examples people are told to copy.

I don’t read this as engineers being careless. It’s about defaults. Docker Compose ships with the hardening switched off, almost nobody turns it on, and the examples people learn from don’t either.

Why this exists

By day I lead a team of security engineers at a large financial institution, where Compose barely comes up. Production runs on Kubernetes and ECS, both with mature security tooling around them. At home in my lab, though, Compose is the right tool: quick, low-ceremony, enough to stand up a stack on a Saturday.

What bugged me was the asymmetry. Kubernetes and Terraform have a deep bench of scanners: Checkov, Trivy, kube-bench, Kubescape. Compose is an afterthought in most of them. The Compose-specific tools I found solved adjacent problems instead. Hadolint lints Dockerfiles, not Compose files. dclint checks Compose structure and style, not security.

What I wanted was simple: a zero-config, OWASP/CIS-grounded linter I could drop into CI and run against my own stacks. So I wrote one. Then I got curious whether the stuff I kept fixing in my own files showed up everywhere else.

It does. That’s what this writeup is about, and I’m putting the tool out there in case it’s useful to anyone who builds the way I do.

Personal project; the views here are my own, not my employer’s.

Here’s what I found.

How the corpus is built

I split the files into four tiers. A single “X% of Compose files do Y” number is misleading when it averages a polished Bitnami example against someone’s half-finished homelab:

  • canonical (327 files) — official upstream examples: awesome-compose, Bitnami, Grafana, Vaultwarden. The stuff READMEs tell you to copy-paste.
  • popular (3,977) — repos with ≥50 stars and a recent Compose file. Production-adjacent code.
  • selfhosted (588) — app-store / template registries: CasaOS, runtipi, Dockge. Home-LAN threat model.
  • longtail (1,552) — a stratified sweep of GitHub code search. The median file in the wild.

Every file goes through the same rule set (compose-lint 0.7.0), and every rule is grounded in the OWASP Docker Security Cheat Sheet or the CIS Docker Benchmark. The full methodology, including what the study deliberately doesn’t claim, is in the canonical report.

Finding 1: nobody flips the hardening flags

Three findings fire on roughly 90% of every file in the corpus:

Most common findings across the corpus, by share of parsed files affected, coloured by severity

  • Filesystem not read-only (read_only: true missing) — 91%
  • No capability restrictions (cap_drop: [ALL] missing) — 91%
  • Privilege escalation not blocked (no-new-privileges missing) — 90%

They’re rated MEDIUM, not CRITICAL, because each one is a missing control rather than active misuse. That’s also what makes them interesting. The Compose hardening triple is almost never set.

The fix takes about 30 seconds per service:

services:
  app:
    image: nginx:1.27@sha256:...   # pin a digest, not just a tag
    read_only: true                # CL-0007
    cap_drop: [ALL]                # CL-0006
    security_opt:
      - no-new-privileges:true     # CL-0003

Add a tmpfs: for whatever paths your app writes to and you’ve cleared the three most common findings in the corpus.

“Aren’t these just optional config choices, not real vulnerabilities?” Mostly fair, and it’s worth being exact about what a “finding” is here. The linter isn’t claiming anything was exploited. It’s flagging that a file leaves a recommended hardening control unset. Whether that matters is a judgment call that belongs to the org or the engineer, based on their context and risk tolerance. What the linter takes off your plate is the “did I even know this control existed?” part. read_only, cap_drop: [ALL], and no-new-privileges aren’t my preferences about tidy YAML; they’re named controls in the CIS Docker Benchmark and the OWASP Docker Security Cheat Sheet. A finding means the config diverges from that published baseline. Closing the gap or accepting it is your call.

Finding 2: even copy-paste vendor examples aren’t clean

You’d expect the official examples to be the hardened ones. Being copied is their entire job. They’re the cleanest tier in the corpus, and they still come in at 83%:

Share of files with at least one finding by tier: canonical 82.8%, popular 95.2%, selfhosted 100%, longtail 78.3%

A few things jump out:

  • Self-hosted app-store templates: 100%. Every single one trips at least one rule. They’re built for “works on your LAN in one click,” which in practice means exposed ports, root users, and big host mounts.
  • Popular repos aren’t better than the long tail. Stars don’t buy hardening discipline.
  • Canonical examples are config demos, not hardening exemplars. People copy them into production anyway.

That last point is most of the story. The examples teach the unhardened shape, and the shape propagates.

A fair caveat here, especially if you’re running a homelab: threat model matters. A single-user box behind a firewall and Tailscale is a different risk calculus than something exposed to the internet, and a finding is usually something to decide about rather than an emergency. Start with what actually bites. A mounted Docker socket is full host takeover whether or not you meant to expose it, so fix those first and treat the MEDIUM pile as gradual cleanup. It’s why the CI gate defaults to fail-on: high.

Finding 3: ~10% of ordinary files don’t even parse

This is the one I didn’t expect. In the long-tail tier, 9.6% of files don’t parse as a valid Compose file at all, against well under 1% everywhere else:

Parse-error rate by tier: canonical 0.6%, popular 0.7%, selfhosted 0.0%, longtail 9.6%

And it’s almost never broken YAML. It’s shape errors: people writing services as a dictionary of strings.

# What a lot of people write (does not parse):
services:
  nginx: nginx:1.27

# What Compose actually wants:
services:
  nginx:
    image: nginx:1.27

A file that doesn’t parse with a real Compose engine was never deployed by one. So these are docs snippets, tutorial follow-alongs, half-finished first drafts. None of them are getting linted before they ship, and the parse-error rate is really a map of where unreviewed config piles up.

(If you’re wondering: “long tail” here means the low-visibility mass of ordinary repos, not a statistical distribution tail.)

Why it’s all MEDIUM-heavy

One more chart, because the severity mix surprises people:

Findings by severity: MEDIUM 78.5%, HIGH 20.3%, CRITICAL 1.1%, LOW 0.0%

Nearly four out of five findings are MEDIUM. That’s the hardening-triple misses from Finding 1, which hit almost every file and are MEDIUM by design. CRITICAL findings are rarer but real: a mounted Docker socket, cap_add: ALL, a bind-mounted /. The Docker socket one alone, which is full host takeover, shows up on 6.4% of parsed files and 8% in the popular tier.

LOW is almost empty, and that’s by construction. Only one of compose-lint’s 21 rules is LOW (a healthcheck someone explicitly turned off), because the tool’s whole scope is security misconfiguration, where the floor is MEDIUM. So read “0.0% LOW” as a fact about the tool, not as the small stuff being fine.

So why are all the flags off by default?

Because Docker optimizes for “it runs the first time.” A writable filesystem, the full capability set, privilege escalation left on: that’s the path of least surprise. Your container starts, your app works. Hardening is opt-in, and opting in means knowing the control exists, confirming your app still works without that capability or that write access, and adding a few lines per service.

And “knowing it exists” is the hard part. There’s no single secure-Compose baseline to copy from. The controls are scattered across the Compose spec, the Docker run reference, the OWASP Docker Security Cheat Sheet, and the CIS benchmark. You’d have to already know that no-new-privileges is a thing, that cap_drop: [ALL] goes before a targeted cap_add, that read_only: true usually needs a tmpfs for whatever your app writes. Most people writing a Compose file aren’t container-security specialists. They want their stack up. Expecting everyone to carry that whole surface in their head is how you end up with a 91% finding rate. A linter flips it around: you don’t memorize anything, you fix the line it points at and read why.

And it compounds. The examples never opt in, so the next person copies the unhardened shape, ships it, and becomes the next example someone copies. The corpus is that loop at scale. None of it is exotic. It’s the accumulated weight of a sensible default that nobody goes back to revisit.

What this is not

A few things I’m explicitly not claiming, because it’s easy to over-read this:

  • It measures how common misconfigurations are, not whether they were exploited. A finding is divergence from hardening guidance, nothing more.
  • It’s descriptive sampling, not statistical inference. No p-values, no population estimates.
  • It’s GitHub-only and public-only. Private and enterprise Compose may look different.

The full “what this study does not claim” section in the report lays out every boundary.

Try it on your own files

compose-lint is MIT-licensed, zero-config, and depends only on PyYAML. A few ways to run it:

# one-off, locally
pipx install compose-lint && compose-lint docker-compose.yml

# or the published image (distroless, nonroot)
docker run --rm -v "$(pwd):/src" composelint/compose-lint

In CI, there’s a GitHub Action:

# .github/workflows/compose-lint.yml
name: compose-lint
on: [pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: tmatens/[email protected]
        with:
          pattern: "**/*compose*.y*ml"   # docker-compose.yml, compose.yaml, …
          fail-on: high

fail-on: high (the default) fails only on HIGH/CRITICAL, so you can adopt it without drowning in the MEDIUM backlog on day one, then tighten later. There’s also a pre-commit hook, JSON and SARIF output (SARIF feeds GitHub Code Scanning), and compose-lint --explain CL-0007 to print any rule’s rationale and fix.

For what it’s worth on a tool you’d wire into CI: every rule cites OWASP, CIS, or Docker docs, the image is distroless and nonroot, and releases ship SLSA provenance and Sigstore attestations. Details are in the repo.

The full report has every table, the complete methodology, the per-rule breakdowns, and steps to reproduce it: State of Docker Compose Security

If you maintain a popular Compose example, I’d genuinely love a PR or an issue. Hardening the examples people copy is the highest-leverage fix there is.

How to Forward Android SMS to Telegram Automatically

Bank codes, server alerts, delivery updates and team OTP messages still arrive on one physical Android phone.

If that phone sits in an office drawer, a courier bag or another country, the whole team waits.

This guide compares five practical ways to forward Android SMS to Telegram, from free APKs to Tasker, n8n and a cloud SMS forwarder.

TL;DR: best options

Method Best for Setup time Main trade-off
Cloud SMS forwarder Teams, multiple devices, business workflows 2-5 min SMS passes through a third-party service
Open-source APK Personal use, strict privacy, one phone 15-30 min Manual setup and maintenance
Tasker / MacroDroid Power users already using Android automation 30-60 min Fragile rules and battery optimization issues
n8n webhook Teams with existing automation infrastructure 1-2 hours You maintain the workflow and endpoint
Custom app Regulated environments or internal tooling Days/weeks Highest maintenance cost

What SMS-to-Telegram forwarding requires

Every implementation has the same basic pipeline:

  1. An Android app receives incoming SMS using the SMS permissions.
  2. The app formats the message and decides whether it should be forwarded.
  3. A Telegram bot sends the message to a chat, group or channel.

The difference is where the routing logic lives. It can live on the phone, in Tasker, in an n8n workflow, in your own backend or in a SaaS dashboard.

1. Cloud SMS forwarder

A cloud SMS forwarder pairs your Android phone with a web dashboard. Incoming SMS are sent securely to the service, matched against routing rules, then delivered to Telegram, Email, Slack, Discord, X2Chat or a webhook.

Pros: quick QR pairing, multi-device management, routing by sender or keyword, delivery history, team access, webhooks and less manual Telegram bot configuration.

Cons: SMS passes through a cloud service. If your use case requires zero third-party processing, a self-hosted or open-source option is a better fit.

SMS Sender 24 is in this category. It works well when one team needs to share OTP codes, server alerts or business SMS from several Android phones without maintaining Android automation on every device.

2. Open-source Android apps

Projects such as Spirit532 SMS Forwarder and similar APKs can read SMS locally and send them directly to Telegram Bot API or a webhook.

This is the cleanest option if you want to inspect the code and avoid a cloud account.

Pros: free, auditable, no SaaS dependency, good for one phone and one or two destinations.

Cons: you maintain every phone manually. Rules, bot tokens, chat IDs and battery settings are configured per device. Some projects go years without updates, which matters on newer Android versions.

Example Telegram Bot API call:

POST https://api.telegram.org/bot<TOKEN>/sendMessage
Content-Type: application/json
{
  "chat_id": "-1001234567890",
  "text": "SMS from +15551234567: Your verification code is 482913"
}

3. Tasker or MacroDroid

Tasker and MacroDroid can listen for SMS events and call Telegram Bot API through an HTTP action. This is powerful if you already understand Android automation.

Pros: very flexible, cheap, local-first, can combine SMS with other Android triggers.

Cons: hard to support as a team workflow. A missed battery permission, Android update or broken profile can silently stop forwarding. Debugging is usually on the person holding the phone.

This method is great for personal automation. It is less great when three people depend on that phone to receive production alerts or bank OTP codes.

4. n8n webhook workflow

If your team already runs n8n, Make or another automation platform, the Android app can send SMS to a webhook. The workflow can then filter messages and forward them to Telegram.

Example payload:

curl -X POST https://n8n.example.com/webhook/sms 
  -H "Content-Type: application/json" 
  -d '{
    "sender": "AWS",
    "body": "Your alarm is in ALARM state",
    "sim": "work",
    "received_at": "2026-05-23T09:15:00Z"
  }'

Pros: great for teams that already automate alerts and business workflows. You can branch to Telegram, Slack, email or CRM.

Cons: you still need a reliable Android collector app, a public HTTPS endpoint, secrets management, monitoring and retries.

5. Custom Android app

Building your own Android app is reasonable for regulated environments, internal device fleets or very specific routing requirements. It gives you full control over data handling and backend architecture.

Pros: maximum control, private backend, custom security model.

Cons: Android SMS permissions, background execution, foreground services, device reboot handling and battery optimization are easy to underestimate.

The first version is the simple part. Keeping it reliable is the real work.

Android 14/15 pitfalls

SMS forwarding is not just an HTTP problem. Modern Android aggressively limits background work, especially for apps installed outside Google Play.

Watch for these issues:

  • Battery optimization: the app may stop receiving or forwarding after several hours unless excluded from battery restrictions.
  • Foreground service rules: long-running background services need a visible notification and correct service type.
  • Boot handling: forwarding should resume after the phone restarts.
  • Connectivity gaps: the phone needs Wi-Fi or mobile data. Good apps queue messages and retry.
  • Dual SIM: personal and work SIMs often need different routing rules.

Which method should you choose?

Scenario Recommended method Why
One personal phone, one Telegram chat Open-source APK Free and simple enough
Team OTP codes Cloud SMS forwarder Rules, history and team destinations matter
Server SMS alerts Cloud forwarder or n8n Reliability and routing are more important than novelty
Strict privacy / no third party Open-source or custom app Keep data under your control
Existing automation stack n8n webhook Easy to plug into current workflows

Honest note

SMS Sender 24 is an independent cloud tool built to solve a real SMS forwarding problem.

It is convenient for teams, but it is not the right choice if your policy requires zero third-party processing. In that case, use an open-source app or self-host the whole pipeline.

FAQ

Can I forward only specific SMS?

Yes. Use sender names, phone numbers, keywords or SIM slots as routing conditions.

For example, bank OTP messages can go to one Telegram chat while courier updates go to another.

Does the Android phone need internet?

Yes. The phone needs Wi-Fi or mobile data to forward messages.

If connectivity drops, a reliable app should queue messages and retry when the connection returns.

Is this different from SMS gateways like Twilio?

Yes. Twilio and similar services provide virtual numbers or outbound messaging APIs.

SMS forwarding starts from a real Android phone and forwards incoming messages from its SIM card.

Can it work with two SIM cards?

Yes, if the collector app records the SIM slot and the service supports SIM-based rules.

This is useful when one phone has both personal and work SIM cards.

Is Telegram the only destination?

No. Telegram is the most common destination, but many teams also forward to Email, Slack, Discord, X2Chat or a webhook.

Originally published at https://smssender24.com/en/blog/forward-android-sms-to-telegram.html

From AI Code Generation to AI System Investigation

I Released AI DB Investigator – A Skill for Structured Database Analysis

AI is very good at generating answers.

But when it comes to database problems, raw answers are not enough.

A slow query, a missing index, a suspicious schema, a broken relation, or an unclear data model usually cannot be solved by guessing. These problems require investigation.

That is why I released AI DB Investigator.

GitHub repo:
https://github.com/miller-28/skill-ai-db-investigator

What is AI DB Investigator?

AI DB Investigator is a skill designed to help AI analyze databases in a more structured way.

Instead of asking an AI model a broad question like:

Why is my database slow?

The goal is to guide the model into a more disciplined investigation flow.

A good database investigation usually requires several steps:

  1. Understand the schema.
  2. Identify important tables and relationships.
  3. Look at indexes.
  4. Inspect query patterns.
  5. Detect possible bottlenecks.
  6. Separate symptoms from root causes.
  7. Suggest safe, practical improvements.

AI DB Investigator is built around that idea.

Not magic.
Not guessing.
A structured investigation process.

Why I built it

I have spent many years working with production systems, especially around backend architecture, PostgreSQL, Redis, APIs, and distributed flows.

One thing becomes clear after enough production experience:

Database problems are rarely isolated.

A performance issue may look like a slow query, but the real cause may be:

  • a missing composite index
  • a bad join pattern
  • an overloaded table
  • unnecessary eager loading
  • poor pagination
  • wrong data modeling
  • missing constraints
  • too much logic happening in the wrong layer
  • data growth that changed the original assumptions

The database does not fail loudly at first.

It whispers.

Then it slows down.

Then it becomes the center of the fire.

So I wanted a skill that helps AI behave less like a random answer generator and more like a careful technical investigator.

The idea behind the skill

The core idea is simple:

AI should not only answer database questions.
It should investigate them.

That means the AI should slow down and ask the right technical questions before jumping to conclusions.

For example:

  • What database engine is being used?
  • What is the schema?
  • Which tables are involved?
  • Is this about performance, correctness, design, migrations, or reliability?
  • Are there indexes?
  • Are there foreign keys?
  • Is the query using OFFSET pagination?
  • Are there N+1 query patterns?
  • Is the data volume small, medium, or already production-scale?
  • Is the issue caused by the database, the application, or both?

This is the difference between generic AI assistance and operational AI assistance.

What this skill is useful for

AI DB Investigator can help with:

  • database schema review
  • PostgreSQL investigation
  • query analysis
  • index suggestions
  • performance bottleneck detection
  • database design review
  • migration reasoning
  • relationship mapping
  • production debugging
  • explaining suspicious DB behavior
  • turning vague DB problems into structured diagnostic steps

It is especially useful when the problem is not yet clearly defined.

Sometimes the real value is not the final answer.

Sometimes the value is forcing the investigation into the right shape.

Why this matters for AI-assisted development

A lot of AI development today focuses on generation:

  • generate code
  • generate tests
  • generate docs
  • generate APIs
  • generate UI

That is useful.

But production work is not only generation.

Production work is also diagnosis.

A senior engineer spends a lot of time asking:

  • What is really happening here?
  • What changed?
  • Where is the pressure?
  • What assumption is now false?
  • What is safe to change?
  • What should not be touched?
  • What is the smallest useful fix?

That mindset is hard to capture with a generic prompt.

A skill gives the AI a repeatable operating pattern.

That is the direction I find interesting: not only using AI to write code faster, but teaching AI how to approach technical systems with discipline.

Small tool, larger direction

This is not meant to be a huge framework.

It is intentionally focused.

The purpose is to encode a specific kind of engineering judgment into a reusable form.

For me, this is part of a larger shift:

Developers are moving from writing every line manually
to designing, directing, and refining intelligent workflows.

The developer becomes less of a typist and more of an orchestrator.

But orchestration only works when the AI has structure.

Without structure, AI improvises.

With structure, AI can investigate.

Repository

The project is available here:

https://github.com/miller-28/skill-ai-db-investigator

Feedback, ideas, issues, and suggestions are welcome.

Especially around:

  • PostgreSQL investigation flows
  • schema analysis
  • query review patterns
  • useful diagnostic prompts
  • real-world DB debugging examples

Final thought

Database work rewards patience.

The best answers usually come after the right questions.

AI DB Investigator is my attempt to give AI a better path through that process.

Not just to answer.

To investigate.

I gave Gemini 3.5 Flash a CVE-fix PR to review. It found another bug in the same file.

This is a submission for the Google I/O Writing Challenge

Across 3 real production PRs, I asked Gemini 3.5 Flash to do a code review. The model — announced this week at Google I/O 2026 — caught 3 legitimate bugs, hallucinated 0, in roughly 4 seconds per PR. The middle PR was the patch for a known security vulnerability in Fastify (CVE-2026-25223, a validation-bypass). The model flagged a second, unrelated regex bug in the exact file being patched.

Here’s what I learned building a code-review agent in about 2 hours with Google’s new model.

Why I tested this

At the I/O keynote, Sundar Pichai pitched Gemini 3.5 Flash as “frontier intelligence combined with action” — optimized for agentic coding and long-horizon tasks. Code review is the perfect stress test: it requires reasoning about code semantics, cross-file context, and judgment about what matters.

Reading another 50 hype threads on X felt pointless. So I built the smallest possible agent that could actually use the model on real code, ran it on three concrete PRs, and counted what it got right, what it made up, and what it missed.

The architecture

Three stages, ~80 lines of TypeScript, runs on Node 20+:

INPUT                  PROCESSING                       OUTPUT
─────                  ──────────                       ──────
owner/repo#N    →      1. fetch the .diff URL      →    stdout (colored summary)
                       2. truncate if > 150k chars      out/{slug}.json
                       3. build prompt + schema         out/{slug}.md
                       4. Gemini 3.5 Flash call
                       5. Zod-parse the response

No GitHub token (public PRs use the unauthenticated .diff URL). No octokit. No frameworks. Just the new @google/genai SDK with structured output.

The core

The heart of the pipeline is a single review() function — pass it a diff, get back a typed array of issues:

import { GoogleGenAI } from "@google/genai";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });

const IssueSchema = z.object({
  file: z.string(),
  line: z.number().nullable(),
  severity: z.enum(["low", "medium", "high", "critical"]),
  category: z.enum(["bug", "security", "performance", "style", "logic", "maintainability"]),
  message: z.string(),
  suggestion: z.string().nullable(),
});

const ReviewSchema = z.object({
  summary: z.string(),
  issues: z.array(IssueSchema),
});

const SYSTEM_PROMPT = `You are a senior code reviewer. Analyze the unified git
diff below and produce a JSON review.

Rules:
- Flag REAL issues only — no nitpicks, no style preferences.
- Prefer fewer, higher-quality issues over volume.
- Each "message" must explain WHY it matters (impact, not just observation).
- If you cannot see enough context to be sure, lower the severity.

Return the full review as JSON matching the provided schema.`;

async function review(diff: string) {
  const res = await ai.models.generateContent({
    model: "gemini-3.5-flash",
    contents: `${SYSTEM_PROMPT}nn--- DIFF ---n${diff}`,
    config: {
      responseMimeType: "application/json",
      responseJsonSchema: zodToJsonSchema(ReviewSchema),
    },
  });
  return ReviewSchema.parse(JSON.parse(res.text ?? "{}"));
}

A few details worth flagging:

  • Model string: "gemini-3.5-flash". GA since May 19, 2026.
  • Structured output: use responseJsonSchema (not the older responseSchema). It validates against the Zod-derived schema and returns conformant JSON. No regex-parsing the response, no try/catch for malformed output.
  • No temperature tuning: Google explicitly recommends not setting temperature, top_p, or top_k on the 3.5 family — the model handles sampling internally.

Full repo at the end. Now the interesting part.

The three PRs

I picked PRs with very different shapes to see how the model behaved across contexts.

PR Type Lines Why
express#6190 Small refactor ~10 Baseline: clean code, no real issues
fastify#6414 Security-sensitive +398 / −147 The patch for CVE-2026-25223
express#6100 Small refactor ~15 Different file, different style

Final scorecard

PR #1 (express#6190):    +0  −0   Model agreed: no issues
PR #2 (fastify#6414):    +3  −0   3 hits, 0 hallucinations
PR #3 (express#6100):    +0  −0   Model agreed: no issues
──────────────────────────────────────────────────────────────
Total:                   +3  −0   Zero false positives.

What it caught — the headline

PR #2 is the one that mattered. Fastify pull #6414 rewrote the entire content-type parser to fix a security flaw (CVE-2026-25223) where attackers could bypass body validation by appending a tab character to Content-Type (e.g. application/jsontx). The fix introduced a new ContentType class and replaced the old loose string-matching logic.

This is exactly the kind of high-stakes, security-sensitive refactor where an automated reviewer either earns its place or doesn’t.

The model flagged three issues. Here’s each one, verified against the actual code.

Hit 1: inconsistent variable use in existingParser

MEDIUM · logic — The existingParser method checks contentType === "application/json" and this.customParsers.has(contentType) using the original contentType string instead of the newly calculated, normalized ct variable.

Looking at the new code in lib/content-type-parser.js:

ContentTypeParser.prototype.existingParser = function (contentType) {
  if (typeof contentType === 'string') {
    const ct = new ContentType(contentType).toString()
    if (contentType === 'application/json' && this.customParsers.has(contentType)) {
      return this.customParsers.get(ct).fn !== this[kDefaultJsonParse]
    }
    if (contentType === 'text/plain' && this.customParsers.has(contentType)) {
      return this.customParsers.get(ct).fn !== defaultPlainTextParser
    }
  }
  return this.hasParser(contentType)
}

The model is right. ct is the normalized version, but the conditional guards still test the raw contentType. Since customParsers only holds normalized keys (see line 85: this.customParsers.set(normalizedContentType, parser)), any header with a different case or trailing parameters silently skips the fast path. Subtle, easy to miss in review.

Hit 2: a regex missing its end anchor

HIGH · security — The subtypeNameReg regular expression is missing a trailing $ anchor. Consequently, any string starting with a valid subtype will match successfully.

This one is the headline. In the brand new file lib/content-type.js, the patch defines two parallel regexes:

const typeNameReg     = /^[w!#$%&'*+.^`|~-]+$/      // has $
const subtypeNameReg  = /^[w!#$%&'*+.^`|~-]+s*/    // no $

The subtype regex anchors at the start but not at the end. Inputs like application/json/extra pass the validation gate where they shouldn’t. In a PR whose entire purpose is fixing a validation-bypass CVE, a senior reviewer would put this in red on the first pass. The model put it in HIGH on the first pass.

I am not claiming this is itself exploitable at the same severity as the original CVE — the downstream parsers may not be reachable in a way that materializes the bug. But the pattern is exactly the class of issue that did materialize as CVE-2026-25223. Pattern-recognition of dangerous shapes is half of what code review is.

Hit 3: stateful global regex

MEDIUM · bug — The keyValuePairsReg regex is defined globally with the /g flag. Because of this, it is stateful and relies on lastIndex. If parsing throws an exception or future modifications exit the loop early, lastIndex will not reset to 0.

Confirmed at the top of lib/content-type.js:

const keyValuePairsReg = /([w!#$%&'*+.^`|~-]+)=([^;]*)/gm

Used inside a class constructor with .exec() in a loop. In healthy execution, lastIndex resets to 0 when exec returns null. But the failure mode — exception inside the loop body, or any future break — silently corrupts every subsequent parse for the lifetime of the process. The model’s suggested fix (use matchAll instead) is exactly the JavaScript-idiomatic answer.

This is a latent footgun, not a live bug. Severity MEDIUM is arguably high. But it’s a real thing the model saw.

What it didn’t catch — the honest part

Two failure modes worth being honest about.

Cross-file context. The model only sees the diff. It can’t tell whether a function called by the changed code is safe, whether a removed branch was load-bearing somewhere else, or whether tests actually cover the new behavior. For PR #6414 in particular, the upstream callers of the new ContentType class are not in the diff, and the model never reasoned about them.

Severity calibration is rough. The regex-without-anchor is HIGH. The stateful /g is MEDIUM. In practice, those probably want to swap — the regex one is a clear pattern with security relevance, the global-regex one is a latent footgun unlikely to fire. Junior-reviewer instincts.

I also can’t conclusively measure what the model missed without reviewing every comment thread on the PR by hand. The merged commit went through multiple rounds of feedback (commits like “address feedback”, “refactor algorithm”, “appease coverage”), so reviewers did catch things, but how many of those are in-diff issues a tool could have seen versus broader design decisions — I’d need another afternoon to know.

What I’d actually use this for

Three takeaways after running this on real code:

  1. It earns a place as a first-layer pre-review. Specifically: PRs that touch parsers, validators, or anything that consumes external input. The cost is around $0.003 per PR. The cost of not running it is shipping a regex without an anchor on a security-sensitive code path.
  2. It does not replace human reviewers. It cannot reason about distributed state, concurrency, transactions, or anything that requires understanding multiple files in concert.
  3. Hallucination rate was zero in this sample — but the sample is tiny. The literature on similar models suggests false positives in the 15-25% range on real-world PRs. Three out of three being valid is great but is not a benchmark.

The 80 lines of TypeScript that produced this run are on GitHub. Two things that are non-obvious about the setup:

  • @google/genai v2 uses responseJsonSchema, not responseSchema. Easy to get wrong if you’re translating tutorial code from an older Gemini.
  • Public GitHub PRs expose a .diff endpoint that requires no auth. You don’t need octokit for an MVP.

If you try it on PRs with shapes I didn’t test — concurrency-heavy, multi-file, generated code — tell me what you find. The interesting question is where the model breaks, not where it works.

Built and tested in May 2026 with Gemini 3.5 Flash, GA two days before publication.

Leveraging the Wrong Scaling Patterns Will Lose You in Production

The Problem We Were Actually Solving

In hindsight, we were trying to optimize for the wrong problem. We were optimizing for the treasure hunt engine to scale horizontally within a single availability zone in AWS, ignoring the warning signs that we were going to get slammed with requests and lose all that scaling to an eventual single point of failure once we started distributing our data across regions. Our team was convinced that scaling vertically with more instances within the same availability zone would solve all our problems, and that was where we erred.

What We Tried First (And Why It Failed)

Our initial solution was to add more RDS instances behind our NGINX load balancer, hoping that we could scale out our database to meet the increased traffic. But we soon realized that adding more instances didn’t solve our disk I/O problem, nor did it alleviate our high CPU usage issue. Our MySQL instances were choking on the increased traffic, and we were seeing long query times that were impacting our ability to serve requests. At that point, we knew we’d have to go back to the drawing board.

The Architecture Decision

We decided to implement a sharding solution using a combination of AWS Route 53, S3, and DynamoDB. We created separate shards for different regions, and each of these shards handled the treasure hunt sequence for its respective region. This would solve our scaling issues and allow us to distribute our traffic across multiple regions and availability zones. We also implemented a caching layer using Redis to reduce the load on DynamoDB and MySQL. However, we still had our issues.

What The Numbers Said After

After implementing our sharding solution and caching layer, we saw a significant reduction in our slow_request_ratio metric. But we were still getting intermittent issues with our NGINX instances timing out and throwing 502 errors. Our Apache JMeter tests showed that our system could still handle the increased load, but the real-world performance was far from ideal. It turned out that our Redis instances were still not being properly sized for the increased traffic, and we were seeing some nasty Redis timeouts that were impacting our application’s ability to serve requests.

What I Would Do Differently

In hindsight, I would have advocated for a more decentralized architecture from the start. I would have pushed for a solution that didn’t rely so heavily on RDS and MySQL, and instead opted for a more event-driven architecture that leveraged the scalability and durability of our AWS services. I would have also implemented more robust monitoring and logging to catch issues like the Redis timeouts before they became critical. Most importantly, I would have taken the time to educate our team about the scaling challenges we were about to face, and we could have avoided the 3am call from the ops team.

The Return of Recursion: How 5M-Parameter Models Are Outperforming Frontier LLMs on Reasoning in 2026

Diagram contrasting Chain-of-Thought (generates thinking tokens one by one, linear, expensive) vs Recursive Latent Reasoning (refines hidden representations in a loop, no tokens emitted until final answer).

The Return of Recursion: How 5M-Parameter Models Are Outperforming Frontier LLMs on Reasoning in 2026

TL;DR Summary

  • Tiny recursive models with 5-7 million parameters are achieving state-of-the-art on deterministic reasoning tasks that frontier LLMs score 0% on — including Sudoku-Extreme, ARC-AGI puzzles, and maze navigation
  • The key innovation: reasoning in latent space instead of generating “thinking tokens” like Chain-of-Thought — delivering 100x speedups and 75% token reduction
  • Probabilistic TRM (7M params) achieves 98.75% on Sudoku-Extreme using Gaussian noise to escape local optima, while DeepSeek-R1 scores 0.0%
  • RecursiveMAS applies recursion to multi-agent systems — agents communicate via latent representations (“telepathically”), cutting tokens by 75.6% and improving accuracy by 8.3%
  • Attractor Models (27M params) outperform 1.3B Transformers trained on twice as many tokens — by solving for fixed points instead of iterating

Direct Answer Block

Recursive models revive a pre-transformer AI concept — iterative reasoning — but with modern training methods that avoid the vanishing gradient problems that killed RNNs. Instead of generating Chain-of-Thought tokens (slow, expensive), they refine representations in hidden latent space through loops. A 5M-parameter TRM achieves 87.4% on Sudoku-Extreme where DeepSeek-R1 scores 0%, while probabilistic extensions push this to 98.75% — at less than 0.0001x the cost of frontier LLMs.

Introduction

The AI industry has spent three years scaling transformers — more parameters, more data, more compute. Chain-of-Thought reasoning made them smarter but also slower and more expensive: every reasoning step is a token, every token costs money, and long chains hit context limits. Meanwhile, a parallel research thread has been quietly reviving recursion — and the results are startling. Models with 5 million parameters are solving puzzles that billion-parameter systems fail completely, using 100x less compute and generating 75% fewer tokens. Here’s how recursive architectures work, why they’re making a comeback, and where they fit in production.

Why did the AI industry abandon recursion — and why are 5M-parameter models now beating frontier LLMs at reasoning?

The story of recursion in AI is a story of training instability. Recurrent Neural Networks (RNNs) were the dominant architecture before transformers. They processed sequences iteratively — refining a hidden state through repeated passes — which is, conceptually, exactly what recursive reasoning models do today.

The problem was vanishing and exploding gradients. When you backpropagate through a recursive loop, gradients either shrink to zero (vanishing) or blow up to infinity (exploding) as the number of iterations grows. Training became unstable. The transformer’s solution — process everything in parallel with attention, no recurrence — eliminated the gradient problem and enabled the scaling revolution of 2018-2025.

But attention has its own scaling problem: quadratic compute cost. Each token attends to every other token. Chain-of-Thought makes this worse — every reasoning step generates a new token that must attend to every previous token. Long reasoning chains become exponentially expensive.

“Autoregressive LLMs hit a reasoning wall — Chain-of-Thought forces models to externalize intermediate thoughts token by token, becoming slow and memory-intensive as sequences grow.” — AlphaSignal summary of the recursive architecture revival

The recursive models being published in 2026 solve the gradient problem that killed RNNs through modern training innovations:

  • TRM and HRM use weight-sharing across recursion steps, keeping the parameter count tiny (5-27M) and making gradient flow manageable
  • Attractor Models use implicit differentiation — solving for fixed points analytically rather than through backpropagation-through-time — making training memory constant in effective depth
  • Probabilistic TRM injects Gaussian noise at each step and uses a learned Q-head for early stopping, avoiding convergence to suboptimal solutions

The result: recursion is back, and it works. The arXiv:2605.19943 paper on Probabilistic TRM demonstrates 91.2% accuracy on Pencil Puzzle Bench vs 55.1% for frontier LLMs — “at less than 0.0001x the cost, using only 7M parameters.”

How does recursive latent reasoning differ from Chain-of-Thought — and why does it deliver 100x speedups?

Diagram contrasting Chain-of-Thought (generates thinking tokens one by one, linear, expensive) vs Recursive Latent Reasoning (refines hidden representations in a loop, no tokens emitted until final answer).

The fundamental difference is where reasoning happens:

Chain-of-Thought (autoregressive):

  • Model generates reasoning steps as text tokens: “Step 1: Let me think about this… Step 2: If X then Y… Step 3: Therefore…”
  • Each token must be generated, then fed back as input for the next token
  • Token generation is sequential — cannot parallelize
  • All intermediate tokens count toward context length and API costs
  • Each token invokes the full forward pass of a massive model

Recursive Latent Reasoning:

  • Model refines a hidden representation through a loop — no tokens emitted until the final answer
  • The loop runs in the model’s latent space (hidden states, not text)
  • Iteration count is determined by convergence or a fixed budget, not by token generation speed
  • No intermediate tokens = no context bloat, no token costs for reasoning steps
  • The loop uses a tiny model (5-27M params), not a massive transformer

The 100x speedup claim comes from this architectural difference: each Chain-of-Thought step requires a full forward pass through a billion-parameter model and generates a token. Each recursive latent step requires a forward pass through a million-parameter model and produces no token. The HRM paper (cited in the newsletter) demonstrated up to 100x speedup for deterministic reasoning tasks compared to autoregressive CoT approaches.

The token reduction is even more dramatic. RecursiveMAS — which applies recursive principles to multi-agent systems — achieved 75.6% token reduction by round 3 (arXiv:2604.25917). Agents pass continuous latent representations to each other instead of text messages. Only the final answer is converted to text.

What are HRM, TRM, Probabilistic TRM, RecursiveMAS, and Attractor Models — and how do they compare?

Five distinct recursive approaches have emerged. Here’s how they compare:

Model Parameters Key Innovation Best Result vs Frontier LLMs
HRM (Hierarchical Reasoning Model) 27M H-L dual-module loop: slow abstract planning + fast detailed computation ARC-AGI SOTA with 1,000 examples 100x speedup on deterministic tasks
TRM (Tiny Recursive Model) 5-7M Single 2-layer weight-sharing network; increase recursion steps, not layers 87.4% Sudoku-Extreme DeepSeek-R1: 0.0%
Probabilistic TRM 7M Gaussian noise at each step enables diverse exploration; Q-head selects best 98.75% Sudoku-Extreme 0.0001x cost, 91.2% vs 55.1% frontier on puzzles
RecursiveMAS Multi-agent Agents communicate via latent states (“telepathically”); recursive collaboration loop 8.3% accuracy gain, 2.4x speedup, 75.6% fewer tokens Matches or exceeds on code + medical reasoning
Attractor Models 27M Implicit differentiation solves for fixed points; equilibrium internalization 91.4% Sudoku-Extreme, beats 1.3B Transformer Claude/GPT o3 fail completely on maze tasks

HRM (arXiv:2603.22871 — March 2026)

The oldest of the modern recursive models. Uses two modules: H (high-level) for slow abstract planning and L (low-level) for fast detailed computation, coupled in a recursive loop. Inspired by human cognition — the dual-process theory where System 2 (slow, deliberate) plans and System 1 (fast, automatic) executes. Achieved state-of-the-art on ARC-AGI puzzles with only 1,000 training examples.

TRM (published at ICLR 2026 Latent & Implicit Thinking Workshop)

Strips HRM to its essence: a single 2-layer weight-sharing network. The key insight: increase recursion steps, not layers. More recursion depth improves generalization more than more parameters. The 5M-parameter TRM hit 87.4% on Sudoku-Extreme — a task where DeepSeek-R1 scored 0.0%. The TRM+Mamba-2 hybrid from arXiv:2602.12078 improved pass@2 on ARC-AGI by +2.0% while maintaining parameter parity.

Probabilistic TRM (arXiv:2605.19943 — May 2026)

TRM’s deterministic recursion can converge to suboptimal solutions with no escape mechanism. PTRM solves this by injecting Gaussian noise at each recursion step, creating parallel trajectories that explore diverse solution basins. A learned Q-head (initially used for early stopping in TRM) selects the best trajectory. The improvement: Sudoku-Extreme from 87.4% to 98.75%, Pencil Puzzle Bench from 62.6% to 91.2% — nearly double frontier LLM accuracy.

RecursiveMAS (arXiv:2604.25917 — April 2026)

Applies recursion to the multi-agent paradigm. Instead of agents exchanging text messages (expensive, verbose), they pass continuous latent representations through a lightweight RecursiveLink module — described as “telepathic” communication. The system is trained with an inner-outer loop algorithm for whole-system co-optimization. Results: 8.3% accuracy gain across 9 benchmarks, 1.2x-2.4x inference speedup, 34.6-75.6% token reduction.

Attractor Models (arXiv:2605.12466 — May 2026)

The most mathematically novel approach. Instead of iterating a fixed number of times, Attractor Models solve for a fixed point using implicit differentiation. The model proposes output embeddings, then an attractor module refines them by solving for equilibrium — training memory stays constant regardless of effective depth. The most remarkable finding: equilibrium internalization — after training, the model’s initial output is already near equilibrium, allowing the solver to be removed at inference with little degradation. A 770M Attractor Model outperforms a 1.3B Transformer trained on twice as many tokens.

How does Probabilistic TRM use Gaussian noise to break out of local optima and achieve 98.75% on Sudoku-Extreme?

Deterministic recursion has a fundamental weakness: it follows the same path every time. If that path leads to a suboptimal solution, there’s no escape — the recursion converges to a local minimum and stays there.

Probabilistic TRM introduces stochastic exploration as a test-time compute scaling strategy:

  1. Inject Gaussian noise at each deep recursion step — small perturbations that nudge the latent state into neighboring regions of the solution space
  2. Run multiple parallel trajectories — each with different noise realizations, exploring different solution basins
  3. Use the Q-head for selection — the same Q-head originally designed for early stopping in TRM now scores each trajectory’s quality
  4. Select the best trajectory — highest Q-head score wins

The key insight: this requires no retraining. The original TRM’s Q-head — trained for early stopping — naturally generalizes to trajectory selection. The noise injection is applied at inference time only. The PTRM paper shows accuracy gains across multiple benchmarks without any task-specific augmentations.

“PTRM injects Gaussian noise at each deep recursion step, enabling parallel trajectories to explore diverse solution basins, and selects among them using the model’s existing Q head. Without requiring retraining or task-specific augmentations, PTRM enables substantial accuracy gains.” — arXiv:2605.19943 abstract

The practical implication: for deterministic reasoning tasks (puzzles, logic, math proofs), you can take an existing tiny recursive model and improve its accuracy by 10-30% simply by adding noise at inference time and running a few parallel trajectories. No model modification needed.

How does RecursiveMAS apply recursion to multi-agent systems — and why does “telepathic” latent communication reduce tokens by 75%?

Standard multi-agent systems work like a chat room: Agent A generates text, Agent B reads it and generates text, Agent C reads both and generates text. Every message consumes tokens, adds latency, and accumulates error as text summaries lose information.

RecursiveMAS changes the communication channel: agents pass continuous latent representations — floating-point vectors in the model’s hidden space — through a lightweight RecursiveLink module. The module is a small learned network that transforms one agent’s latent state into a format the next agent can process.

This is described as “telepathic” communication because:

  • No information is lost to text compression (a latent vector preserves more information than a text summary)
  • No tokens are consumed (the communication is continuous, not discrete)
  • Communication is parallelizable (multiple agent pairs can exchange latent states simultaneously)
  • The RecursiveLink module is optimized end-to-end with the agents, so the latent format evolves to be maximally useful

The results from arXiv:2604.25917:

  • 75.6% token reduction by round 3 (vs text-based MAS)
  • 2.4x end-to-end speedup (latent communication is faster than text generation)
  • 8.3% accuracy improvement (latent states preserve more information than text summaries)

The framework was evaluated under 4 representative agent collaboration patterns across 9 benchmarks spanning mathematics, science, medicine, search, and code generation. The latent approach consistently outperformed text-based alternatives across all patterns.

The inner-outer loop training algorithm deserves attention: the outer loop optimizes the whole multi-agent system, while the inner loop handles per-agent recursion. Shared gradient-based credit assignment propagates across recursion rounds — meaning later agents can influence the training of earlier agents, and vice versa.

Where do recursive models fit in production — and when should you still use frontier LLMs instead?

A decision matrix: Recursive models for deterministic logic (Sudoku, mazes, puzzles, math proofs). LLMs for language, creativity, and general knowledge. Both for hybrid systems.

Recursive models are specialized reasoning engines, not general-purpose language models. The deployment boundary is clear:

Use recursive models for:

  • Deterministic logic tasks: Sudoku, constraint satisfaction, puzzle solving, theorem proving
  • Pattern recognition: ARC-AGI puzzles, Raven’s matrices, abstract reasoning
  • Latency-critical applications: Robotics, embodied AI, real-time systems where 100ms matters
  • Cost-sensitive tasks: Running 7M-parameter models locally vs API calls to billion-parameter models
  • Data-scarce domains: Scientific exploration where training examples are limited (HRM achieved SOTA with 1,000 examples)

Use frontier LLMs for:

  • Language understanding and generation: Creative writing, summarization, translation, conversation
  • General knowledge tasks: Question answering, fact retrieval, explanation
  • Code generation: Real-world software engineering (note: RecursiveMAS showed gains on code benchmarks, but general SWE requires LLM capabilities recursive models don’t have)
  • Multi-modal tasks: Images, audio, video understanding — recursive models are currently text/latent only

The hybrid future:

The newsletter source describes the optimal architecture as hybrid systems — recursive models as specialized reasoning engines inside LLM-powered applications. An LLM handles the interface (understanding user intent, generating explanations, formatting output), then delegates deterministic reasoning tasks to a recursive sub-component that returns results in milliseconds rather than seconds.

The Attractor Models paper suggests another direction: equilibrium internalization. If models can learn to internalize reasoning to the point where the solver can be removed at inference, then recursive training becomes a way to produce standard feed-forward models that have internalized deeper reasoning — no recursion needed at inference time.

Frequently Asked Questions

Q: Can I run a recursive model on my laptop?

Yes. These models are 5-27 million parameters — orders of magnitude smaller than even a “small” LLM (1B+). A 7M-parameter TRM or PTRM runs easily on consumer hardware. The challenge is that recursive inference loops may require multiple forward passes, but even 50 passes through a 7M model is computationally trivial compared to one pass through a 70B LLM.

Q: Are recursive models a replacement for transformers?

No. They’re complementary. Recursive models excel at deterministic reasoning and pattern recognition. LLMs excel at language, creativity, and general knowledge. The most promising direction is hybrid systems where recursive models serve as reasoning engines inside LLM-based applications.

Q: Why can’t I just use CoT with my existing LLM?

You can — for many tasks, CoT works well. But for specific classes of problems (Sudoku, mazes, ARC-AGI), CoT fails because the problem requires exploring a solution space iteratively, not generating a linear chain of reasoning. Frontier LLMs score 0% on these tasks. Recursive models are designed specifically for iterative solution-space exploration.

Q: How do recursive models handle tasks they weren’t trained on?

Generalization is where they shine. Because recursive models have so few parameters (5-27M), they can’t memorize — they must learn general reasoning strategies. TRM achieved 45% on ARC-AGI-1 with 5M parameters, while frontier LLMs with orders of magnitude more parameters struggle. The weight-sharing across recursion steps acts as a strong regularizer.

Q: What’s the difference between HRM and TRM?

HRM uses two separate modules (H for abstract planning, L for detailed computation) in a coupled loop. TRM simplifies this to a single weight-sharing network. TRM is smaller (5-7M vs 27M), simpler, and achieved competitive results. Probabilistic TRM builds on TRM. Attractor Models are a different approach — solving for fixed points rather than iterating.

Q: Is the recursive architecture revival connected to the “Titans” and “deep thinking” trends?

Yes — they’re parallel developments. Titans (Google, 2025) introduced neural memory modules for long context. Deep thinking approaches extend reasoning through iterative refinement. The recursive architecture revival is the most radical version: tiny models that replace autoregressive token generation with latent-space iteration entirely, rather than augmenting it.

Glossary

  • Recursive latent reasoning: Iterative refinement of hidden representations in a model’s latent space without emitting intermediate tokens — the core mechanism behind TRM, HRM, and related architectures
  • Chain-of-Thought (CoT): An autoregressive reasoning method where models generate intermediate reasoning steps as text tokens — effective but slow and token-expensive
  • Weight-sharing: Using the same parameters across multiple recursion steps, keeping model size tiny while enabling deep computation through iteration count
  • Probabilistic recursion: Injecting Gaussian noise at each recursion step to explore diverse solution basins, then selecting the best trajectory — improves accuracy without retraining
  • Equilibrium internalization (Attractor Models): A phenomenon where fixed-point training causes the model’s initial output to already be near equilibrium, allowing the solver to be removed at inference
  • Test-time compute scaling: Improving model accuracy by spending more computation at inference (more iterations, more trajectories) rather than during training

Author

Ramsis Hammadi — AI/ML engineer specializing in GenAI, LLM engineering, and automation. Full bio →

I built my first Chrome extension because editing localStorage was annoying

DataSideKick opend view

I finally published my first Chrome extension.

It is called DataSidekick, and I built it because I got tired of fighting DevTools every time I needed to inspect or edit localStorage and sessionStorage.

DataSideKick opend view

Also, yes, I paid the legendary $5 Chrome Web Store developer fee, so emotionally I had to ship something. Otherwise, it would have been the worst investment portfolio of my life.

What is DataSidekick?

DataSidekick is a Chrome extension that opens as a side panel and helps developers work with browser storage in a cleaner way.

Instead of digging through DevTools and editing raw strings, you get a focused interface for:

  • viewing localStorage
  • viewing sessionStorage
  • searching by key or value
  • editing simple values inline
  • editing JSON visually
  • importing data from JSON
  • exporting data to JSON
  • hiding noisy keys
  • requesting access per site/origin

You can check it out here:

https://datasidekick.site

Why I built it

Browser storage is simple until it is not.

A lot of apps store useful state in localStorage, but the default developer experience is still pretty rough:

{"user":{"name":"Rodrigo","settings":{"theme":"dark"}}}

Technically readable? Sure.

Pleasant to edit? Absolutely not.

I wanted something closer to a small developer cockpit: open the panel, find the key, inspect the value, edit it safely, and move on.

The feature I cared about most: visual JSON editing

The main idea was simple:

If a value is valid JSON, don’t treat it like a random string.

So instead of this:

{"a":"b","settings":{"theme":"dark"}}

DataSidekick displays structured JSON in a more readable way, so it becomes easier to inspect and edit nested values.

That was the first “okay, this is actually useful” moment.

Chrome permissions were the real boss fight

The extension needs to read and edit storage from the current page. That means permissions matter.

At first, I had an unnecessary permission in the manifest and the Chrome Web Store rejected the extension.

Fair enough. My bad.

The final approach is more intentional:

  • no unnecessary downloads permission
  • site access is requested when needed
  • the user sees when access is required
  • data stays local in the browser

For a tool that touches browser storage, I think this transparency is important.

Exporting without the downloads permission

One small lesson: exporting a JSON file does not necessarily require the Chrome downloads permission.

This is enough for my use case:

const blob = new Blob([JSON.stringify(payload, null, 2)], {
  type: "application/json"
});

const url = URL.createObjectURL(blob);

const a = document.createElement("a");
a.href = url;
a.download = "datasidekick-export.json";
a.click();

URL.revokeObjectURL(url);

Since I was not using chrome.downloads.download(), the permission was unnecessary.

Chrome Web Store noticed. Chrome Web Store was right.

Painful, but useful.

Current features

The first public version includes:

  • Chrome Side Panel UI
  • localStorage and sessionStorage support
  • visual JSON editor
  • key/value search
  • import/export JSON
  • per-site access flow
  • hidden keys
  • noisy key filtering
  • favorites
  • dark/light mode
  • font size controls
  • playground on the website

The playground

I also added a playground to the site so people can understand the idea before installing anything.

That was important because browser storage tools can feel a bit sensitive. I wanted people to see what the extension does before giving it access to a page.

Try it here:

https://datasidekick.site

What I learned

A few things I took away from this first release:

  1. Small dev tools are still worth building.
  2. Chrome extension permissions deserve real attention.
  3. A simple UX decision can matter more than a complex feature.
  4. Shipping to a store is very different from “it works locally”.
  5. Paying $5 makes you emotionally committed.

What’s next?

This is my first Chrome extension, but definitely not the last.

Some ideas for future versions:

  • IndexedDB support
  • better diff view
  • undo/redo
  • schema validation
  • storage history
  • better JSON editing interactions

For now, I am happy it is live.

If you work with localStorage or sessionStorage often, I would love your feedback:

https://datasidekick.site

Or plugin link: DataSideKick

Built by a developer who just wanted to stop editing JSON like a caveman.

I Built ContextFabric: One Private Memory Layer Across Claude, ChatGPT, Cursor, and More with Local Gemma 4

This is a submission for the Gemma 4 Challenge: Build with Gemma 4

AI tools remember now, but they remember in separate silos. Claude has projects, ChatGPT has personalization, Cursor indexes your codebase, and somehow you still end up re-explaining the same decisions, constraints, preferences, and project state every time you move between tools.

That felt backwards to me.

If memory is becoming part of the AI operating system, then personal context should not be trapped inside one vendor’s product. It should be portable, permissioned, local-first, and owned by the user.

So I built ContextFabric: a local AI memory layer powered by Gemma 4.

What I Built

ContextFabric is a desktop app, local daemon, memory graph, and browser extension bridge that lets AI tools share approved context without sending your personal memory to a cloud memory server.

The idea is simple:

  1. Import your real project context: repos, folders, markdown, PDFs, ChatGPT exports, Claude exports, notes, and documents.
  2. Gemma 4 runs locally through Ollama and extracts structured memory nodes.
  3. ContextFabric stores those nodes in a local SQLite graph.
  4. External tools request access.
  5. You approve the request.
  6. The browser extension injects the right context into Claude, ChatGPT, Cursor, Gemini, Perplexity, and other AI tools.

The five core memory node types are:

  • project: what you are building
  • decision: choices already made and why
  • preference: stable working preferences
  • style: how you communicate, design, or code
  • person: collaborators and relevant human context

This is not meant to replace Claude projects, ChatGPT memory, or Cursor indexing.

It solves a different problem: your context should be portable across them.

Demo

Browser extension injection:

The demo shows the full loop:

  • paste messy project context
  • Gemma 4 extracts structured memory nodes
  • nodes are saved locally with confidence scores
  • AI Query answers with sources
  • a permission request controls external access
  • the browser extension injects approved context into an AI chat tool

Code

GitHub:

https://github.com/Boweii22/ContextFabric

Live Site:

https://boweii22.github.io/ContextFabric/

The project is built with Electron, React, TypeScript, SQLite, Express, Ollama, and a Manifest V3 browser extension.

The local app exposes two loopback APIs:

  • 127.0.0.1:47821 for the desktop app permission/token API
  • 127.0.0.1:7749 for the simple demo daemon UI and compatibility endpoints

Both are bound to loopback, not 0.0.0.0.

That matters because the privacy claim is not just a paragraph in a README. The architecture does not expose a public server for your memory graph.

How I Built It

The architecture has six parts:

User-owned sources
repos, exports, docs, notes, PDFs
        |
        v
Local ingestion
chunking + metadata
        |
        v
Gemma 4 via Ollama
extract + reason
        |
        v
SQLite memory graph
nodes + embeddings
        |
        v
Permissioned daemon
localhost only
        |
        v
Browser extension
injects context

The first hard problem was extraction.

I did not want a generic summary. I wanted durable memory. That means the model has to decide whether a piece of text contains a project fact, a decision, a preference, a style signal, or a person.

Here is the actual extraction schema prompt from the project:

export const CONTEXT_NODE_TYPES = ['project', 'style', 'decision', 'preference', 'person'] as const

export const CONTEXT_EXTRACTION_SYSTEM_PROMPT = `You are ContextFabric's local Gemma 4 context extractor.

Your job is to read one piece of user-owned context and output ONLY valid JSON.
No markdown. No prose. No comments. No trailing commas.

Extract durable context nodes that another AI assistant should remember later.
Use only facts supported by the input. Do not invent people, projects, tools, or decisions.

Allowed node types:
- project: what the user is building, maintaining, researching, or planning.
- style: how the user writes, communicates, designs, codes, or prefers answers to be shaped.
- decision: a choice already made, including why, tradeoffs, rejected alternatives, or reversibility.
- preference: a stable working preference, constraint, tool choice, privacy preference, format preference, or habit.
- person: a collaborator, stakeholder, user, client, author, or named human with relevant relationship/role context.

Return this exact JSON shape:
{
  "nodes": [
    {
      "type": "project" | "style" | "decision" | "preference" | "person",
      "title": "short human-readable title",
      "summary": "one factual sentence, max 220 characters",
      "confidence": 0.0,
      "evidence": "short direct evidence phrase from the input, max 180 characters",
      "entities": ["important names, tools, projects, people"],
      "tags": ["lowercase-keywords"]
    }
  ]
}`

The parser is intentionally defensive. Gemma 4 is good at structured output, but production code still needs repair paths.

export function parseContextExtraction(raw: string): ContextExtractionParseResult {
  const errors: string[] = []
  const parsed = parseJsonObject(raw)

  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
    return { ok: false, result: { nodes: [] }, errors: ['Output is not a JSON object.'] }
  }

  const root = parsed as Record<string, unknown>
  if (!Array.isArray(root.nodes)) {
    return { ok: false, result: { nodes: [] }, errors: ['Missing nodes array.'] }
  }

  const nodes: ExtractedContextNode[] = []
  for (const [index, value] of root.nodes.entries()) {
    const node = normalizeNode(value, index, errors)
    if (node) nodes.push(node)
  }

  return { ok: errors.length === 0, result: { nodes: nodes.slice(0, 6) }, errors }
}

The second hard problem was assembling context for different tools.

Claude, ChatGPT, and Cursor do not want the same payload. Claude benefits from concise prose sections. ChatGPT works well with a compact bullet brief. Cursor needs engineering-focused context.

So ContextFabric asks Gemma 4 to assemble app-aware context briefs:

export const PAYLOAD_ASSEMBLY_SYSTEM_PROMPT = `You are ContextFabric's local Gemma 4 payload assembler.

Goal:
Turn user-approved local memory nodes into one coherent context brief for another AI tool.

Rules:
- Use ONLY the supplied memory nodes. Do not invent facts, names, features, dates, metrics, or claims.
- Prefer stable project, decision, style, preference, and person nodes over raw conversation/code snippets.
- Write a useful brief, not a JSON dump.
- Include source node ids inline as [node:id] after concrete claims.
- If the nodes do not support a requested claim, omit it.
- Respect the requested app format.
- Stay under the requested maximum word count.

App formats:
- claude: concise prose with sections "Context", "Decisions", "Working Style", "How to Use This".
- chatgpt: short bullet-oriented brief with "Known Context", "Preferences", "Relevant Sources".
- cursor: engineering-focused brief with "Project", "Architecture / Decisions", "Coding Preferences", "Files / Sources".
- generic: compact neutral brief with clear source ids.

Return JSON only:
{
  "payload": "the final context brief",
  "usedNodeIds": ["node-id"],
  "warnings": ["optional warning when data is thin or uncertain"]
}`

The third hard problem was making the local model usable on normal hardware.

I hit memory issues while testing Gemma locally, so ContextFabric creates a constrained Ollama profile called cf-gemma4.

const res = await fetch(`${this.baseUrl}/api/create`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    model: this.constrainedModelName,
    from: sourceModel,
    parameters: {
      num_ctx: this.runtimeContext,
      num_predict: 64,
      num_batch: 4,
    },
    stream: false,
  }),
  signal: ctrl.signal,
})

This was not about making the model weaker.

It was about making the demo run on real laptops, not just on a perfect GPU workstation.

For the local HTTP daemon, I added a small API that judges can test without understanding the whole Electron app:

compat.post('/extract', async (req: Request, res: Response) => {
  const { text, title = 'HTTP Extract', inputType = 'api', save = false } = req.body
  if (!text?.trim()) {
    res.status(400).json({ error: 'text is required' })
    return
  }

  const result = await extractNodesFromText(db, ollama, text, title, inputType, Boolean(save))
  res.json({
    ok: true,
    saved: Boolean(save),
    savedCount: result.savedCount,
    nodes: result.nodes.map(nodeToPublicJson),
  })
})

compat.get('/context', async (req: Request, res: Response) => {
  const appId = String(req.query.app || req.query.appId || 'generic')
  const query = String(req.query.query || 'current project context, writing style, technical decisions, preferences')
  const nodes = selectTokenNodes(db.getNodes(800), query, 16)
  const assembly = await assembleTokenPayloadWithTimeout(ollama, { appId, query, nodes, maxWords: 800 })
  res.json({ ok: true, appFormat: assembly.appFormat, payload: assembly.payload })
})

That endpoint is what makes the browser extension bridge simple. The extension does not need to know how the graph works. It asks the local daemon for approved context and inserts it into the active AI chat box.

Why Gemma 4

Gemma 4 is not a decorative dependency here.

It is the part of the system that turns ContextFabric from a searchable note bucket into a memory protocol.

I chose Gemma 4 E2B as the target model profile because ContextFabric is supposed to run where personal context actually lives: on laptops, desktops, and eventually smaller edge devices.

A cloud model would have defeated the core privacy constraint. If your private context graph has to leave the machine for extraction, then the product becomes a privacy policy promise instead of a privacy-preserving architecture.

A much larger local model could produce stronger answers, but it would make the product less usable for the people who need it most. The challenge specifically highlights small Gemma 4 models for edge and local use, and that is exactly the design space ContextFabric lives in.

Gemma 4 plays three roles:

1. Context extraction

It reads messy user-owned text and converts it into typed, durable memory nodes.

This is different from summarization. A summary says “what was this text about?” Context extraction asks “what should another AI assistant remember later?”

2. Conflict detection

If a new memory contradicts an existing one, Gemma 4 can mark the conflict or uncertainty. That matters because memory should not silently rot.

For example, if an old preference says “prefer short answers” and a new note says “prefer detailed long answers”, ContextFabric should surface that conflict instead of pretending both are equally true forever.

3. Payload assembly

When Claude, ChatGPT, or Cursor asks for context, Gemma 4 turns relevant graph nodes into a coherent brief with citations and a word limit.

This is where the model’s reasoning is useful: not to invent project facts, but to decide how to package approved facts for another tool.

The architecture also keeps Gemma 4 on the correct side of the trust boundary.

The normal challenge path uses Ollama locally. The daemon binds to loopback. The database is local. The browser extension talks to localhost. There is no ContextFabric cloud memory service receiving your data.

That is the difference between “we care about privacy” and “the data path cannot reach our server because there is no server in the path.”

The Bigger Picture

I do not think the long-term version of this idea is just an app.

I think it is protocol infrastructure.

HTTP made documents portable across servers. SMTP made email portable across providers. ContextFabric is an early sketch of what a personal AI context protocol could look like.

Today, every AI company is building memory as a product feature. That makes sense. Memory improves retention.

But as developers, we should ask a harder question:

Should personal AI context belong to the tool, or to the user?

My answer is the user.

That is why ContextFabric has permission requests, scoped grants, source citations, local storage, and a browser extension bridge. The extension is the adoption wedge: it makes the protocol useful before any AI company agrees to support it natively.

That was the “I never thought of it that way” moment for me.

The future of AI memory should not be one giant memory per vendor. It should be a user-controlled context layer that tools can request access to.

The browser extension is the wedge.

The protocol is the point.

Challenges I Ran Into

The hardest challenge was not building a chat UI.

It was keeping the system honest.

Early versions returned raw code chunks when I asked project-level questions. That was technically “retrieval”, but it was bad memory. I had to improve ranking so durable nodes like project, decision, style, and preference win over random bundled JavaScript or CSS.

The second challenge was local model reliability. Gemma 4 needs enough free memory, and normal laptops are messy. People have Chrome, VS Code, Docker, Discord, and ten other things open.

That led to the constrained Ollama profile, shorter prompts, fallback parsing, and clearer error messages.

The third challenge was browser injection. Claude, ChatGPT, Cursor, and Perplexity do not share one DOM structure. The extension has to find active inputs, avoid stale text areas, handle single-page-app navigation, and never crash the page if the daemon is offline.

The fourth challenge was packaging. A project that only works on my machine is not a challenge submission. I added a one-command startup path, release assets, Chrome extension packaging, screenshots, and a GitHub Pages landing page.

What’s Next

The next version is about turning the prototype into a real protocol.

My roadmap:

  • publish the Chrome Web Store listing after review
  • add native macOS and Windows installers
  • improve LAN sync between devices
  • add richer conflict resolution workflows
  • publish a formal context payload schema
  • build SDKs so indie AI tools can request ContextFabric memory directly
  • explore a standard token format for scoped context grants

The browser extension is useful now, but the bigger win is native integration.

I want AI tools to request context the way apps request OAuth scopes, except the resource is not your Google Drive or GitHub account. It is your personal working context.

Try It Yourself

Repo:

https://github.com/Boweii22/ContextFabric

Live Site:

https://boweii22.github.io/ContextFabric/

Install Ollama:

https://ollama.com

Then run:

git clone https://github.com/Boweii22/ContextFabric.git
cd ContextFabric
npm run start

On macOS, use Node 20 or 22:

nvm install 20
nvm use 20
npm run start

On Windows, use Node 20 via fnm or nvm-windows:

fnm use 20
npm run start

Open the local demo UI:

http://127.0.0.1:7749/ui

The fastest test:

  1. Paste some project context into Extract Context.
  2. Click Extract and save.
  3. Watch Gemma 4 create typed memory nodes.
  4. Open the Claude context preview.
  5. Try the browser extension bridge.

Built by Bowei Tombri for the DEV Gemma 4 Challenge.

If you build with AI tools every day, I am curious: would you rather each tool keep its own memory, or would you prefer a local memory layer that every tool has to request permission from?