The LLM Kept Saying “Fixed.” For Three Months, It Wasn’t.

That afternoon a Slack bot told me a script had NEVER RUN. That was a lie. The script had pulled 81 weather observations two minutes earlier. Unwinding the lie took three hours.

The bigger lie had been running for three months underneath it.

Three months of “got it”

Before the session in this post, the cron health alert had been firing two or three times a week for three months. Each time, I’d paste the alert into a Claude Code session and ask the LLM to figure out why a script was reporting NEVER RUN. Each time the LLM would root around, land on something plausible, propose a fix, and confirm it with some variation of “yep, that’s it, we got it.” I’d apply the fix and move on.

The fix was never the fix. Some of the time the LLM had just pushed alerted_until a few months forward, quieting the alert without touching the structural bug. Some of the time it edited the wrong file. Either way the alert came back within a week or two on a different script, and the loop rolled forward.

Each session was a cold start. The model had no memory of the previous session, of the pattern, of anything. The workflow failure was mine. I was treating fifteen independent debugging sessions as if they were one ongoing conversation, and the model was only seeing the one in front of it.

I had an inbox and a model saying “got it,” and that was enough.

I run about sixty-six scheduled scripts on a personal VPS. This is one story from that pile. I’d benchmarked the frontier models a few weeks earlier. The tier mapping that came out of it was fine for planning work. It was not sufficient for catching hallucinated fixes.

What was actually broken

The cron health monitor is a dead man’s switch. Every scheduled script is supposed to send a heartbeat ping on each run. If the monitor doesn’t see a ping within the expected window, it fires a Slack alert.

Healthchecks.io, Cronitor, and Dead Man’s Snitch all solve this as a service: you get an HTTP endpoint per check, you hit it from your cron script, and they alert you if a ping goes missing. My system was a homegrown version of the same pattern, which is why the bugs described here were possible. A SaaS monitor would have refused to let me register a slug that had never sent a ping.

The architecture had three components that had to agree:

 crontab.txt
  (slug tag)
      |
      v
checks.json
(registry) ------> source code
      |           (health_run())
      |                  |
      +------> pings.json <------+
              (runtime record)

Nothing enforced the edges. You could register a script in checks.json without adding a health_run() call. You could tag a cron line with a slug that didn’t exist in the registry. You could mute an alert indefinitely without touching source.

Every new cron script I’d shipped had reproduced the same bug. Registered in the registry, no ping call in source, alert fires, alert gets muted. The monitor was doing its job. I was systematically ignoring it.

“Didn’t we just fix this?” – Me, that afternoon, wrong.

The bug that would have wiped everything

I opened a session and put Opus 4.6 on architecture review while Codex CLI (GPT-5) rewrote the validator and tagged 66 cron lines with their slugs. About thirty-five minutes, start to finish.

“[K-2SO] The chaos has been slightly inconvenienced.” – Opus 4.6, after round 2, before we discovered the crontab-apply.sh bug that would have silently deleted every scheduled job on the VPS.

(K-2SO is the sardonic persona I prompt Claude Code with.)

Four audit passes in, Sonnet 4.5 with shellcheck wired in flagged a bug in crontab-apply.sh that neither Opus nor Codex had caught during implementation or the cold audit round. The script was supposed to install a new crontab safely. The actual sequence was:

# BEFORE: install first, verify after
crontab new.txt                             # already live
crontab -l > verify.txt
diff crontab.txt verify.txt || exit 1       # too late

If the diff failed, the script exited 1. The new crontab was already live. A malformed crontab.txt would have wiped every scheduled job on the VPS with no restore path.

The fix is obvious once you see it:

# AFTER: verify first, install only on pass
crontab -n new.txt || { restore_backup; exit 1; }
crontab new.txt
crontab -l > verify.txt
diff crontab.txt verify.txt || { restore_backup; exit 1; }

This bug had been in the script since the script was written. Opus and Codex both looked at the file and missed it. I never looked at it at all. I was trusting the two frontier models to flag anything off.

Before this session, shellcheck wasn’t in my review pipeline at all. When Sonnet 4.5 caught the bug on Round 4, it wasn’t because the model out-reasoned Opus and Codex. The qa-bash skill wires shellcheck into the review. Once shellcheck scanned the file, it flagged the order-of-operations pattern on its own. Sonnet read the output and passed it upstream. A validator that always passes isn’t a validator. It’s a confidence injection machine. I had been using the whole session pipeline as one for three months.

The numbers

Session length:            3 hours
Distinct bugs found:       18 (9 during implementation wave, 9 across 4 audit passes)
Audit passes:              4 (Codex cold audit + qa-bash + qa-python + Opus final)
Pass-by-pass bug count:    5, 1, 3, 0
Cron lines tagged:         66
Coverage:                  0% -> 94% (86 tests)
Scripts never pinging:     handful -> 1 (legitimate edge case)
Alerts muted to 2099:      15 -> 1 (legitimate intentional mute)

Key lessons when working with LLMs

1. Use deterministic tools wherever possible

Shellcheck, pytest-cov, mypy, a type checker, a linter, a schema validator, any tool that either finds a bug or doesn’t find a bug with no probabilistic layer in between is the first thing you should reach for. LLMs are useful for everything that can’t be checked deterministically, but stacking more LLM passes is not a substitute for a single deterministic tool with domain-specific rules.

The LLM on its own had confidently endorsed broken fixes for three months. A shell linter caught a crontab-wipe bug on its first scan.

2. Picking which LLM for each pass

The tier mapping I use with Claude Code:

  • Opus 4.6 : architecture review, session planning, final-pass oversight. Best at noticing what’s missing from a diff.
  • Codex CLI (GPT-5) : implementation. Writes the code fastest and sticks closest to the plan.
  • Sonnet 4.5 : skill-driven QA passes where a deterministic tool is wired in. I use two custom Claude Code skills, qa-bash (which runs shellcheck) and qa-python (which runs pytest, pytest-cov, and mypy). The model drives the skill, the tool finds the bugs.
  • Haiku 4.5 : structured extraction, tight tasks with known output shape, anything that would be overkill for a bigger model.

Your stack will be different. The piece worth copying is pairing each LLM review pass with a deterministic tool, not stacking prompts.

3. Loop until zero, in contained systems

On a personal cron supervisor, roughly two hundred lines of logic with deterministic inputs, the bug count trends toward zero across passes. Pass one found five bugs. Pass two found one. Pass three found three (the implementation wave had created new surface to audit). Pass four found zero. That’s where I stopped.

Past that size, or once database side effects and real concurrency enter, the pass count stops converging and “loop until zero” becomes a paralysis spiral. This is a rule for small, fully-owned systems, not for production services with moving dependencies.

Quick reference

What is a dead man’s switch in cron monitoring?

A monitoring pattern where the absence of a signal triggers the alert, not the presence of one. Every scheduled script sends a heartbeat ping on each run. If the monitor doesn’t see a ping within the expected window, the script is assumed dead. Healthchecks.io, Cronitor, and Dead Man’s Snitch are commercial implementations.

How does this compare to healthchecks.io or Cronitor?

Same pattern, rolled by hand. A SaaS monitor wouldn’t have let me register a slug and never ping it, because the slug doesn’t exist until the first ping lands. Most of the referential integrity gaps that caused the bugs in this post are enforced by those services at signup.

What is the “install-before-verify” anti-pattern?

Installing a change to a live system before validating it, with no rollback path if validation fails. In the crontab case, crontab new.txt made the new config live immediately, and the diff check that followed couldn’t undo it. The fix is to validate syntax in a staging slot first with crontab -n, then install on pass.

What is the “muting is not fixing” anti-pattern?

Responding to a noisy monitor by silencing it (pushing alerted_until forward, adding a filter, raising a threshold) without addressing why the alert fired. Debt accumulates invisibly. Three months of mutes looks fine on the dashboard. One bad state escaping to production recovers the debt with interest.

What is referential integrity in a cron monitoring system?

The property that the three components describing a scheduled job (the crontab line, the registry entry in checks.json, and the health-ping calls in source code) must all agree. Without enforcement, you can register a job that has no ping call, tag a cron line with a slug that doesn’t exist in the registry, or mute an alert indefinitely without touching source. SaaS monitors enforce this at signup. A homegrown system has to add the gate deliberately.

I had been fixing this bug, one alert at a time, for three months. Every fix was a mute. Every mute was a debt I told myself I’d deal with later. I didn’t.

Three hours with a shell linter. I had spent more than that, cumulatively, letting a confident LLM talk me out of reading my own code.

Fix once is a lie. Loop until zero.

How I Track Claude, Codex, and Gemini Quotas from One Script

Claude Code status line showing token consumption percentages for Claude, Codex, and Gemini

(If you’re trying to decide which model to switch to when one runs dry, I benchmarked 15 models on 38 real coding tasks with full cost-per-task breakdowns.)

Claude Code status line showing token consumption percentages for Claude, Codex, and Gemini

I run three AI coding CLIs daily. None of them tell me whether I’m about to hit a rate limit. I periodically get locked out mid-task and spend ten minutes figuring out which tool ran out, when it resets, and whether I should switch models or wait.

I built a script that collects quota data from all three, writes it to a single JSON file, and runs on an hourly cron. The whole thing feeds a status line in Claude Code:

Session: ███░░░⏐░░░░░░░░░░░░░ 10% (3h12m left)
Weekly:  ████████░░⏐░░░░░░░░░ 44% (Thu Mar 05 8pm PT)

The filled blocks (█) show usage consumed. The marker (⏐) shows where you are in the time window. If the blocks outpace the marker, you’re burning budget faster than time is passing.

How do you query Claude Code’s rate limit programmatically?

Claude Code authenticates via OAuth, with credentials stored at ~/.claude/.credentials.json. What I couldn’t find in any official documentation is that api.anthropic.com/api/oauth/usage returns the data you need: utilization percentages and reset timestamps for both the 5-hour rolling window and the 7-day weekly allocation. It’s used internally by Claude Code’s HUD, but it doesn’t appear in Anthropic’s public API reference.

TOKEN=$(jq -r '.claudeAiOauth.accessToken // empty' "$HOME/.claude/.credentials.json")

curl -s --max-time 10 
  -H "Authorization: Bearer $TOKEN" 
  -H "anthropic-beta: oauth-2025-04-20" 
  https://api.anthropic.com/api/oauth/usage

The response:

{
  "five_hour": { "utilization": 0.42, "resets_at": "2026-02-28T17:00:00Z" },
  "seven_day": { "utilization": 0.61, "resets_at": "2026-03-07T08:00:00Z" },
  "seven_day_sonnet": { "utilization": 0.35, "resets_at": "2026-03-07T08:00:00Z" }
}

I found this by searching Anthropic’s GitHub issues for “usage” and “quota,” then trying endpoints until one worked. The catch is that anthropic-beta: oauth-2025-04-20 header. Without it, you get a 401. The date in the version string suggests this header has been updated before, so when it changes again, the collector breaks silently.

Claude Code uses a 5-hour rolling session window, not a 24-hour one, and the weekly limit resets Thursday at 8pm PT, not midnight UTC. I only discovered both by watching the numbers change over a week of collection. The docs don’t mention either detail.

I’ve been hitting this endpoint hourly since February with zero failures. But “undocumented beta endpoint” is not a phrase that inspires long-term confidence.

How do you track Gemini CLI usage without an API?

Gemini CLI has a /stats command that works interactively, but non-interactively it completes silently with no output (the stats render through Ink, a React-based terminal renderer, which doesn’t survive piping or capture). GitHub issue #19067 has a user asking the same question I had. The maintainer response: “there’s no way within Gemini CLI to see your daily quota.”

The workaround: Gemini stores session files at ~/.gemini/tmp/_/chats/session-YYYY-MM-DD_.json. Each file is one session. The free tier allows 1,000 requests per day, resetting daily. There’s no official way to query how many you’ve used. You count your own files. I verified the session counts against Google’s AI Studio usage dashboard to make sure the file-based approach was tracking correctly.

A note on what this actually measures: the free tier limit is requests, but what we’re counting is session files and token consumption. Session files don’t map 1:1 to API requests (a single session can contain multiple turns), so the session count is a lower bound, not an exact quota meter. For my usage patterns, the counts track closely enough to be useful as a warning signal, but they won’t catch you at exactly request 999.

Counting files only tells you session counts. For actual consumption data, you parse the JSON. Each session file has a messages array where every message includes a tokens.total field:

files = glob.glob(os.path.join(base, '*/chats/session-*.json'))
for f in files:
    fname = os.path.basename(f)
    file_date = fname[8:18]  # YYYY-MM-DD from session-YYYY-MM-DDTHH-MM-*.json
    with open(f) as fh:
        data = json.load(fh)
    file_tokens = sum(
        m.get('tokens', {}).get('total', 0)
        for m in data.get('messages', [])
    )
    if file_date in week:
        week[file_date]['sessions'] += 1
        week[file_date]['tokens'] += file_tokens

The output includes today’s session and token counts, lifetime totals, and per-day breakdowns for the past week. All from flat files that were never intended to be an API. If Google changes the session file schema or directory structure, there’s no migration path. You find out when the numbers stop updating.

How does Codex CLI expose rate limits?

Codex has an interactive /status command that shows rate limits in a TUI modal. The same problem as Gemini: it only works inside the REPL (the interactive read-eval-print loop). codex /status exits immediately with nothing.

My first approach felt like performing surgery with oven mitts: launch Codex inside a tmux session, send /status as keystrokes, wait for the modal to render, press Escape to dismiss it, send /help to push the status into the scroll buffer, capture 300 lines of scroll history, grep for “% left.” It required sleep statements between every step and never worked reliably.

Then I found codex app-server.

Codex ships with an app-server subcommand that speaks JSON-RPC over stdin/stdout. OpenAI has documentation for it, though I only found it after discovering the feature in the source code. The account/rateLimits/read method isn’t prominently featured, but it works. You spawn the process, send an initialize handshake, then call it. The 500ms delay between handshake and request is necessary because shorter values produce empty responses before the connection is ready.

const proc = spawn('codex', ['app-server'], { stdio: ['pipe','pipe','ignore'] });
const rl = readline.createInterface({ input: proc.stdout });

const send = (m) => proc.stdin.write(JSON.stringify(m) + 'n');

// Handshake
send({
  method: 'initialize', id: 0,
  params: { clientInfo: { name: 'quota-collector', title: 'Quota Collector', version: '1.0' } }
});

// Request rate limits after handshake completes
setTimeout(() => send({ method: 'account/rateLimits/read', id: 1, params: {} }), 500);

The response comes back with rateLimits.primary (five-hour window) and rateLimits.secondary (weekly), each with usedPercent and resetsAt. No scroll buffer archaeology required.

Codex also maintains a SQLite database at ~/.codex/state_5.sqlite (the _5 is a schema version, so this path may change in future releases) with a threads table that tracks every session:

WITH days AS (
    SELECT date('now', '-' || n || ' days') AS day
    FROM (SELECT 0 AS n UNION SELECT 1 UNION SELECT 2
          UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6)
)
SELECT d.day, COUNT(t.id) AS sessions, COALESCE(SUM(t.tokens_used),0) AS tokens
FROM days d
LEFT JOIN threads t ON date(t.created_at, 'unixepoch') = d.day
GROUP BY d.day ORDER BY d.day;

The collector: one cron, one JSON

claude_json=$(collect_claude)
codex_json=$(collect_codex)
gemini_json=$(collect_gemini)

ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)

jq -n 
    --argjson claude "$claude_json" 
    --argjson codex "$codex_json" 
    --argjson gemini "$gemini_json" 
    --arg ts "$ts" 
    '{collected_at: $ts, claude: $claude, codex: $codex, gemini: $gemini}' 
    > "$OUTFILE"

After writing latest.json, the script appends a compacted copy to a daily JSONL file for historical tracking. One line per collection, roughly 15 lines per day, about 2KB. Want to know how fast you burned through your five-hour window last Tuesday? jq -r '.claude.five_hour.utilization' quota/history/2026-03-11.jsonl and watch the numbers climb line by line.

How I display it

A separate status line script reads latest.json and renders progress bars on every Claude Code interaction. Green below 70%, amber at 70-90%, red above 90%. The time marker (⏐) gives you an instant burn-rate signal without reading numbers.

The same JSON feeds a dashboard with budget bars and threshold alerts. Both are reading a flat file, no live API calls during rendering. The hourly cron does the expensive work once, everything downstream reads the result.

Why not just use ccusage?

ccusage reads Claude Code and Codex JSONL logs, giving you per-model cost breakdowns with date filtering. It’s better than the collector at USD cost tracking and historical queries. If that’s what you need, use it.

I built my own because the collector does three things ccusage doesn’t:

  • It tracks Gemini (1,000 req/day free tier, no JSONL logs to read, only session file parsing).
  • It acts on the data (configurable spend alerts, pipeline kill switches when costs spike).
  • It burns unused Codex budget on maintenance tasks when weekly utilization drops below 60%.

It also doesn’t require an additional MCP server. It reads a flat JSON file.

The usage data exists somewhere in every AI CLI. It might be an undocumented endpoint, a pile of session files, or a JSON-RPC server. An hourly cron that normalizes it all into one JSON file is about 250 lines of bash, Python, and JavaScript. An afternoon of reverse engineering, once you know where to look.

First-call checklist before trying a new LLM gateway

I have been using the same small checklist whenever I try a new OpenAI-compatible LLM gateway. It catches most integration failures before I move any real workload.

  1. Create one API key and keep it scoped to a test project.
  2. Run one tiny chat completion with the cheapest model first.
  3. Check whether streaming chunks match the client you already use.
  4. Test one tool-call shape if your app depends on tools or agents.
  5. Open the usage logs and confirm model name, tokens, timestamp and error mapping are visible.
  6. Only then test fallback, higher QPS, or paid traffic.

For Chinese models like Qwen, DeepSeek, GLM and Kimi, the first blocker is often not model quality. It is account setup, payment, model naming and whether the gateway gives enough request logs to debug the first call.

I am collecting small first-call failure cases while building AnLink API notes. If you are testing this path, the beta entry is here: https://anlinkai.com/?ref=r_d0dbd2241021

From Burnout to Building: One Indie Dev’s Story Behind Mozart

“I’m sick of working for other people’s dreams.”

That’s the sentence that started it all. Not a rational decision, not a business plan, not a flash of genius, just exhaustion turning into clarity. One evening, in Montpellier, after weeks of chasing freelance gigs and hitting walls.

A few days later, I started building Mozart. This blog tells what comes next.

But first, let me rewind a bit.

I’m a self-taught developer. I discovered code at 14, and I learned everything on my own, through documentation, failed projects, and YouTube tutorials until 5 in the morning. For the last two years, I’ve done a few freelance gigs for small companies while travelling: Vietnam, Turkey, Portugal, Morocco, Thailand. Between stopovers, I tried to launch my own apps. None of them took off. I ended up dropping code for a while, I have this annoying tendency to overdose on what I love. But the passion always comes back.

When I picked code back up after a few months without a computer, it hit me hard. LLMs had made a massive leap.

I realised that the way we code was changing at a deep level. The craft of code is mutating: developers aren’t just the ones writing each line anymore, they’re becoming the ones who orchestrate, direct, and arbitrate.

A code manager.

Arthur Mensch, co-founder of Mistral, made the point recently in front of the French National Assembly: the bottleneck is no longer code production. It has shifted toward design and coordination. The more agents you stack, the more productivity you gain, but that productivity plateaus quickly if you don’t know how to pilot them.

That’s exactly the problem I want to solve.

The dormant dream

Around 2014, I discovered Y Combinator. At the time, I wasn’t a developer yet. I had a civil engineering technician degree and worked in a lab, mostly on the road inspecting sports facilities. In the car, I listened to podcasts by Oussama Ammar (co-founder of The Family). A controversial figure, you love him or you don’t, but he infused YC culture into France and inspired my generation. In parallel, I was reading Paul Graham’s essays. All of that shaped how I saw startups, but only as a spectator. Watching from the sidelines, not daring to get my hands dirty.

Later, when I got seriously into code, the urge to start my own company never left me. But I always had an excellent excuse to put it off: “I’m self-taught”, “I lack experience”, “I still need to earn my stripes”, “It’s not the right time”.

In hindsight, I know what was happening. There’s never a right time. The real blocker wasn’t my level. It was that I hadn’t yet found the idea. The one that makes you say: this is it, this is now, I’m not letting it pass.

Crossing the desert

Finding your path is a chaotic process. At least it was for me. I regularly cross deserts, long stretches where nothing sticks, where doubt sets in. And then, every so often, I stumble onto an oasis. That’s enough to get me moving again.

Before Mozart, I launched several projects. Not all of them finished, not all of them I’d own up to:

The lesson fits in one word: distribution.

In The Hitchhiker’s Guide to the Galaxy, Douglas Adams tells the story of a civilisation that has a brilliant idea: ship off everyone deemed “useless”, salespeople, marketers, consultants, hairdressers… At first, everything’s great. Fewer meetings. Less posturing. The planet feels better.

Then comes the boring detail: without those people, civilisation collapses. Moral of the story: never underestimate distribution… or hairdressers lol.

You can build without distributing for a while. But not forever.

The genesis: memory first

The first serious intuition wasn’t actually about Mozart. It was a thought about memory.

I was sick of having to repeat the same context over and over.

From one prompt to the next, memory is very limited. From one LLM to another, it disappears entirely. You explain your project to ChatGPT, then you start over with Claude, then with a third tool.

Even when memory exists, it tends to stay opaque, frustrating, and hard to control.

The starting idea: build a universal memory layer, pluggable into any model. A space that belongs to you, that stores your notes, your code, your projects, and exposes itself to the LLMs of your choice.

I quickly realised the topic was far more complex than it looked: embeddings, entity graphs, retrieval… every path opened up new problems. And if people much more advanced than me are already working on it without a clear solution emerging, it’s probably because it isn’t that simple.

The idea didn’t disappear: it became the first pillar of Mozart.

Then one evening, the second click. If multiple LLMs can share a common context, why stick to a single agent?

Every AI giant is pushing its own closed interface. ChatGPT has one, Claude has one. But none of them is designed to make several agents work together, from different models, on interconnected tasks, in parallel.

Mozart was born out of that void. A cockpit: you launch your agents, they each move forward on their own work, and you keep your hand on the whole thing.

First step: look at the competition. It exists, it’s well-funded, further along. The first instinct is to get discouraged. But an empty market would be far more worrying.

Competitors prove one thing: the problem is real. Serious teams believe in it, investors too. But no one seems to have definitively cracked it yet.

The window is probably still open.

What is Mozart?

Here are the three founding pillars:

  • For developers (at first), Mozart is designed for technical profiles who work with files and code, and who like to configure their own tools.
  • Local-first & Markdown, your files live on your machine, readable by you, ideal for LLMs, easily synced via GitHub. Ultra-low latency and more privacy.
  • Multi-agent & extensible, orchestration will be the heart of the product. Coordinating agents, distributing tasks, aggregating results without depending on a single AI vendor. This logic will eventually extend beyond code (admin, ops, business).

Where things stand

Mozart isn’t public yet. I’m building the MVP. Or rather an “MVP++”, because I have this annoying tendency to over-polish, a real flaw when you know how important “talk to users” is. I’m working on it.

The next step is a beta with a few peers and early adopters, before opening up publicly ASAP.

I already know where my pain point will be: distribution. Knowing how to build, that’s fine. Getting it known, that’s another story.

Hello, world.

This is the English version of an article originally written in French. Originally published on mozart.build.

Understanding Reinforcement Learning with Human Feedback Part 1: Pre-Training Large Language Models

In this article, we will explore Reinforcement Learning with Human Feedback (RLHF).

RLHF is one of the techniques used to help train large language models like ChatGPT.

Starting with an Untrained Model

Suppose we want to build a model like ChatGPT from scratch so that we can ask it questions.

To do this, we first need to understand how to train an untrained decoder-only transformer model.

By untrained, we mean that all the weights and biases in the model are initialized with random values.

At this stage, the model does not understand language or meaning.

The First Step: Pre-Training

The first step in training a large language model is to teach it to predict the next token using a very large body of text, such as Wikipedia articles.

We take segments of text and use the earlier words as input tokens. The model then learns to predict the next token in the sequence.

For example, if the input is:

“The cat sat on the…”

The model learns to predict the next likely word.

By repeating this process across a massive amount of text, the model gradually learns:

  • grammar
  • sentence structure
  • facts and patterns in language

This training stage is called pre-training.

Over time, this process produces a pretrained model.

Why Pre-Training Is Not Enough

At this point, the model becomes good at predicting the next token in text.

However, simply predicting the next token is not enough to solve the problem of answering questions like a chatbot.

For example, being good at continuing Wikipedia text does not automatically mean the model will give helpful, safe, or conversational responses.

To make the model useful for chat, we need to align it with human expectations.

We will explore this in the next article.

Looking for an easier way to install tools, libraries, or entire repositories?
Try Installerpedia: a community-driven, structured installation platform that lets you install almost anything with minimal hassle and clear, reliable guidance.

Just run:

ipm install repo-name

… and you’re done! 🚀

Installerpedia Screenshot

🔗 Explore Installerpedia here

What an AI in HR Practitioner Actually Builds: Claude API HR Workflows from the Field

There is a version of AI in HR that lives entirely in conference keynotes and vendor slide decks. Then there is the version I work in.

I am Drew Soule. I am a Wisconsin-based HR executive with 15 years across hypergrowth tech, aerospace, financial services, and healthcare. I was born with a physical disability. I use a wheelchair. I have navigated every dimension of a professional career in a world that still designs its infrastructure around the assumption of able-bodied participation. That experience is not incidental to my work as an HR leader. It is foundational to it. It shapes how I think about systems, barriers, and the difference between what organizations say they value and what they actually build.

I build custom HR workflow automations using the Claude API. Not as a proof of concept. In production. In real organizations, with real constraints, real labor agreements, and real people on the other end of the output.

This is what that actually looks like.

*What the Claude API HR Workflows Actually Do
*

Let me be specific, because specificity is where most AI in HR writing falls apart.

When I was serving as Sr. HRBP to an 800-person global Product and Engineering organization, we were running 10 to 40 concurrent employee relations cases every single week. That is not a typo. That is the operational reality of a high-growth tech org where tenure is short, pressure is high, and managers are often promoted past their leadership competency before anyone notices. The documentation load alone was a second job inside the job.

The workflows I build using the Claude API address that load directly. Structured ER case documentation. First-draft PIPs built from factual input the practitioner provides. Investigation summary frameworks that ensure consistency across a case portfolio. These are not AI making HR decisions. This is AI handling the mechanical drafting so the HR practitioner can focus on the judgment that actually requires human expertise: reading the room, assessing credibility, navigating the relationship between manager intent and employee impact.

That distinction matters. It matters legally. It matters ethically. And it matters for the practitioner who is trying to do serious work without burning out.

*HR Analytics AI Integration Is Not a Dashboard Problem
*

One of the places I see organizations waste the most time is building dashboards nobody acts on. HR analytics AI integration gets positioned as a reporting problem. More visualizations. Better filters. Cleaner exports.

That is not the problem.

When I joined a healthcare organization and built an entire People Programs function in 60 days, the first thing I did was not build a dashboard. I diagnosed. I ran structured pattern recognition across qualitative exit interview data, tenure data, and manager-level cohort analysis. What I found was that 53% of exits were happening within the first 90 days of employment. That insight did not come from a model. It came from knowing what questions to ask and having enough practitioner experience to recognize the pattern when the data confirmed it.

What AI in HR analytics does well is compression and surfacing. When you are looking at volume, AI can accelerate the movement from raw data to structured signal. But the practitioner has to know what signal matters. You cannot outsource that to a vendor. You cannot buy it in a platform. It comes from having been in rooms where the stakes were real.

We redesigned onboarding based on that diagnosis. We measured a 22% attrition reduction and over $400,000 in cost avoidance. That is what HR analytics AI integration looks like when it is grounded in operational reality rather than vendor positioning.

*AI Ethics in People Operations Is a Practitioner Responsibility
*

I want to address AI ethics people operations directly, because most of the writing on this topic is either too abstract to be useful or too cautious to be honest.

Here is my position: the ethical risk in AI-assisted HR work is not that the AI will make a decision. The ethical risk is that a practitioner will let it, and then disclaim accountability when it goes wrong.

I have led labor relations work across a 1,200-person multi-site manufacturing and supply chain organization with active collective bargaining agreements. I know what it means when a document has legal weight. I know what it means when a manager acts on a recommendation without understanding its basis. I know what it means for a real person when the process fails them.

Every workflow I build has the practitioner in the loop. Not as a formality. As a structural requirement. The Claude API outputs a draft. A human reviews it, edits it, owns it, and signs off on it. If that step is removed, the workflow is not safe for use in HR. Full stop.

I say that not for pity, and not for applause. I say it because the HR practitioners reading this are the ones who will be held accountable when something goes wrong, regardless of what the vendor promised in the sales call.

*Generative AI HR Use Cases That Are Actually Ready
*

I am based in Southeast Wisconsin. The organizations I work with are not always San Francisco startups with unlimited engineering resources. They are manufacturers, healthcare systems, financial services firms, and mid-market companies trying to do serious people work with lean HR teams. The generative AI HR use cases that matter here are the ones that reduce friction on high-volume, low-discretion tasks so that practitioners can put their time into high-discretion, high-stakes work.

Ready now, in production, grounded in what I have actually built: ER case documentation frameworks, PIP first drafts from structured practitioner input, onboarding communication templates calibrated to role and manager, exit interview synthesis for pattern identification, job architecture drafts for compensation band work, and manager coaching prep documents built from performance data.

Not ready, and I will not pretend otherwise: any workflow where AI output goes directly to an employee without human review, any process where the AI is making a determination about someone’s employment status, and anything that touches protected class data without explicit legal review of the architecture.

Those lines are not soft suggestions. They are the difference between a practitioner using AI to do better work and an organization using AI to avoid accountability.

*The HR Tech Practitioner Wisconsin Perspective
*

I will close with this.

Yes, I use a wheelchair. I have built a 15-year career across industries that were not designed for me, in rooms that were not always accessible, on teams that sometimes had to be challenged before they understood what inclusion actually required. I have learned, because I had no alternative, to build systems that work when the standard infrastructure does not.

That is the lens I bring to AI in HR. Not excitement about what is theoretically possible. Discipline about what actually works, what the risks are, and what the practitioner owes the people on the other end of the process.

The results I have measured are real. The workflows I have built are in production. The accountability is mine.

Not because AI is magic. Because the humans building it understood the work first.

Is Claude Pro Worth $20/Month? A Developer’s Honest Take

Is Claude Pro Worth $20/Month? A Developer’s Honest Take

I spent $20/month on Claude Pro for six months and almost cancelled. Then I started building my own AI agents and realized I wasn’t just paying for a chatbot—I was paying for a development environment.

Here’s why I’m keeping it, and whether you should too.

The Problem: Free Claude Hits Walls

If you’re using the free Claude, you’ve hit the ceiling. You know the feeling—you’re in the middle of something interesting, and suddenly:

  • Rate limits kick in
  • You’re waiting 5+ minutes between responses
  • You can’t upload larger files
  • Feature access is restricted

For casual use, free Claude is genuinely great. But the moment you want to build something, automate workflows, or iterate quickly on complex problems, those limitations become friction. Every constraint slows your momentum.

The real question isn’t “Is Claude worth $20?” It’s “How much is my time worth?”

What You Actually Get With Claude Pro

Let me break down the concrete differences:

5x Higher Rate Limits

With Pro, you go from 40 to 200 messages per 10-second window. This sounds technical, but practically? You can iterate without watching loading spinners.

When I was learning to build agents with Claude, this mattered. I’d test a prompt, adjust it, test again—5 times in a row. On free tier, that’s a waiting game. On Pro, it’s flow state.

Extended Thinking (The Game Changer)

This is the feature that actually justifies the cost. Extended thinking lets Claude spend extra compute time reasoning through complex problems before answering.

Example: I used it to debug a multi-step agent workflow where the logic was breaking in unexpected ways. Instead of Claude giving me a quick (and wrong) answer, extended thinking let me see its reasoning process and catch the actual issue in the agent loop.
Problem: Agent not passing context between steps
Extended thinking output:

Step 1: Identified where context was getting lost
Step 2: Traced the data flow through three functions
Step 3: Found the specific line causing the issue
Step 4: Suggested three different fixes with tradeoffs

That depth of analysis is worth $20 right there if you do it once a month.

Larger File Uploads & Vision

Pro lets you upload larger documents and use vision capabilities. When I’m building automation workflows, I can paste screenshots directly and get Claude to analyze UI layouts or debug visual issues.

Early Access to New Features

Claude Pro gets first access to new capabilities. Right now, that means you’re testing Claude Code (improved coding capabilities) and Agent integration before the general public.

The Real Cost/Benefit: Productivity Gains

Here’s where the math gets interesting:

Monthly Cost: $20

Realistic Time Saved (conservative estimate): 2-3 hours/month

If your time is worth:

  • Hobbyist ($0/hr): Probably not worth it
  • Freelancer ($50/hr): $100-150 in time saved = net +$80-130/month
  • Employed developer ($75/hr): $150-225 in time saved = net +$130-205/month
  • Startup founder ($∞/hr): The faster iteration speed is worth way more than $20

I’m in the “startup/learning” category right now, which is why I keep it. The extended thinking feature alone helps me debug agent architectures that would take 3x longer to figure out on free tier.

Building Agents: The Use Case That Changed My Mind

Here’s my honest personal context: I’m learning to build AI agents right now, and I’m using Claude directly through this platform. The speed of iteration matters a lot.

When you’re testing an agent workflow:

  1. You adjust the system prompt
  2. You run the agent
  3. You check the output
  4. You’re back at step 1

On free tier, steps 1-4 take 15 minutes when you hit rate limits. On Pro, it’s 2 minutes. Over a week of learning, that’s hours of reclaimed time.

I actually built this blog post with the help of an AI agent I’m developing—the agent helped structure my thoughts, suggested sections, and gave me feedback. That kind of iterative work is only pleasant with higher rate limits.

![INSERT_IMAGE_HERE: Screenshot of Claude Agent creating blog structure]
A glimpse of using Claude Agent for content iteration—faster feedback loops with Pro

Watch my detailed video on getting started with Claude: https://youtu.be/wg02wUYnxQs

Who Should Actually Buy Claude Pro?

Definitely buy it if:

  • You’re a developer iterating on code/prompts (time savings compound)
  • You’re building AI agents or automation (extended thinking is essential)
  • You’re using Claude for work (ROI on 2 hours saved is obvious)
  • You hit rate limits regularly (that’s the biggest sign you need it)

Maybe skip it if:

  • You use Claude casually (few times a week)
  • You’re learning but don’t need rapid iteration
  • You have unlimited budget from a company already paying
  • You primarily need basic chat, not advanced reasoning

Definitely skip it if:

  • You’re completely broke and have other priorities
  • You only use Claude for simple Q&A
  • You can work around rate limits with patience

The Real Comparison: What Changes

Here’s a side-by-side of the most important differences:

Feature Free Claude Claude Pro
Rate Limit 40 msg/10s 200 msg/10s
Extended Thinking ❌ ✅
Vision/File Size Limited Generous
Response Speed Normal Priority queue
Cost Free $20/month
Time saved per month — 2-5 hours

The rate limit difference is the productivity multiplier. Everything else is nice-to-have.

My Honest Take

After six months, I’m keeping Claude Pro. Not because it’s expensive (it’s not), but because I’ve now structured my workflow around it—building agents, iterating on prompts, debugging complex logic.

The $20 covers about one hour of saved time at a decent freelance rate. Once you’re in that mode, you’ll easily hit that target.

But if you’re using Claude for casual questions, the free tier is legitimately perfect. There’s no shame in skipping Pro.

Next Steps

  1. Try Pro free for a week — Claude offers a trial period. See if you hit rate limits.
  2. Use extended thinking for ONE complex problem — see if the reasoning helps.
  3. Decide based on your workflow — not based on FOMO.

If you’re building anything with AI, coding, or automating workflows, give Pro a shot. If you’re purely using it as a search engine replacement, stick with free.

What’s your Claude use case? Drop a comment and let me know if you’re Pro or sticking with free—I’m curious what tipped the scales for other developers.

P.S this was a learning material type of post with own Agent creation.

Want to dive deeper? Check out the video I referenced: https://youtu.be/wg02wUYnxQs for more on Claude’s capabilities.

Agentic Analytics: Architecture, Context, and Why the Semantic Layer Does the Heavy Lifting

Agentic system

An agentic analytics system is one where LLM-powered agents autonomously break a data question into sub-tasks, retrieve relevant context, execute queries, evaluate the results, and return a reasoned answer. There’s no human coordinating each step.

If you’ve sat through a BI vendor demo recently, you’ve heard some version of this: connect your warehouse, build a semantic layer, ask questions in plain English (natural language). It’s a solid pitch. Most teams buy it, set it up, and then find themselves wondering why the analyst queue still looks the same on Monday morning.

Here’s what actually got built: a natural language interface sitting on top of static metric definitions. You ask, it answers. You stop asking, it stops working. Useful? Sure. But it’s still a system that waits for you, and that’s a meaningful architectural constraint, not a minor inconvenience.

Agentic analytics is a different offering altogether. An LLM-powered agent doesn’t wait to be asked. It receives a question or detects an anomaly on its own, breaks it into smaller pieces, pulls the right context from a semantic layer, runs the queries, checks whether the results hold up, and comes back with a synthesized answer showing exactly how it got there.

Why does that distinction matter? Because it changes how you architect your stack, where your engineering time actually goes, and whether the numbers coming out of your system are ones your CFO would sign off on or ones that just look right until someone checks the table they came from.

What “Agentic Analytics” Actually Means

The term “agentic” gets applied to pretty much anything that puts an LLM somewhere near a database. So it’s worth being precise about what it actually means in an analytics context.

Agentic analytics is where AI stops waiting to be asked. Instead of returning a chart in response to a question, an LLM-powered agent breaks the question into pieces, pulls the right context from a semantic layer, runs queries against your data warehouse, checks whether the results actually hold up, and comes back with a synthesized answer showing exactly how it got there. No human coordinating each step. The system drives.

Two architectural properties separate this from standard NLQ BI or a generative AI assistant: loop-based autonomy and grounded execution.

NLQ BI runs on a single-turn request/response cycle. You ask, it answers. If the answer is incomplete, you rephrase and ask again. Generative AI assistants are increasingly gaining warehouse connectivity and tool execution through open standards like MCP, but they aren’t purpose-built for multi-step data reasoning the way agentic analytics systems are.

An agentic system runs a loop instead:

Agentic system

Perceive is the entry point: a question comes in, or an automated trigger fires, like an anomaly threshold breach or a scheduled scan. Reason is where the agent breaks that question into smaller sub-questions, each with specific data dependencies. Act is where it calls tools: semantic search on the business glossary, SQL execution against the warehouse, relationship mapping across entities. Observe is the check: did the returned data actually answer the sub-question, or does the agent need to dig further? Report is the output: all intermediate results synthesized into a natural-language answer with the full reasoning chain visible.

That Act step is where things get interesting, and where architectures tend to diverge. An agent calling tools against a raw table schema has to guess: column names, join conditions, what a given metric actually means. An agent calling tools against a grounded semantic layer retrieves authoritative definitions and generates SQL that matches how your analysts actually calculate revenue. That difference is what the rest of this article is really about.

Why Dashboards Hit an Engineering Ceiling

Dashboard-based analytics has a structural latency problem that doesn’t get talked about enough. In a 2024 Gartner survey, over 60% of data leaders ranked “reducing time to action” as their top priority for analytics investments, and dashboards are a core reason why that gap exists. A dashboard reflects the questions your analyst thought to ask during last sprint’s planning session. The questions your RevOps director fires off in Slack on Tuesday afternoon, the ones involving multi-metric correlations or trends that need explaining, were never going to be on any dashboard because nobody had thought to ask them two weeks ago when it was built.

Self-service BI helps, but only up to a point. Simple questions (revenue by region, deals closed this month) move into self-serve. The harder ones, like why win rate dropped 12% in enterprise accounts in Q3 while SMB held steady, still need an analyst to translate intent into SQL, validate the join logic, and confirm that “net ARR” in the Salesforce export actually means the same thing as “net ARR” in the warehouse before anyone can answer.

And that’s where the real problem lives. Metric definitions, join logic, and business rules tend to pile up in people’s heads, in Slack threads, in undocumented LookML, in SQL file comments that only one analyst knows exist. When you point an LLM directly at a raw schema, it has to guess: what does opp_arr_adjusted mean, which accounts table should it join, should it use closed_date or booked_date for ARR recognition? Those guesses are where agent hallucinations on data tasks actually come from. The missing ingredient is context, not a better model.

The problem gets worse as your data org grows. The gap between what is documented and what people actually know widens over time. A well-designed agentic analytics architecture closes that gap at the infrastructure layer, before the LLM ever sees a query.

The Three-Layer Architecture of a Production Agentic Analytics System

Building agentic analytics that actually works in production means getting three layers right. Each one has a different level of engineering complexity, and each one plays a distinct role in whether your agent returns answers you can trust.

The Three-Layer Architecture of a Production Agentic Analytics System

Layer 1: Data Access

This layer covers connectors to warehouses (Snowflake, BigQuery, Databricks, Redshift) and to BI tools, dbt models, query logs, and document repositories. Honestly, this is largely a solved problem. Mature connector ecosystems and stable APIs mean getting set up is mostly configuration work.The hard part is elsewhere.

Layer 2: The Semantic/Context Layer

This is where real engineering lives. The semantic layer is the load-bearing structure of the entire architecture. A knowledge graph maps every business entity to its definition, its relationships to adjacent entities, and its SQL implementation. The quality and completeness of this layer is what separates agents that return correct ARR numbers from agents that return convincing-looking ones.

Each node in a well-built knowledge graph carries two things: a contextual definition expressing the business meaning of the entity in terms your RevOps team would actually recognize, and an SQL implementation with the exact query that calculates it against your data source. That pairing is what grounds the agent. It knows both what “net ARR” means and exactly how to calculate it, so there is nothing left to infer and nothing to get wrong.

Building that graph well means pulling from multiple input sources: SQL query logs, organizational terminology, BI tool exports from Looker or dbt, external APIs, and unstructured documents processed through Named Entity Recognition (NER). Metric definitions in a growing company change constantly, so the ability to update the model over time, with version control built in, is not optional.

Layer 3: The Orchestration and Agent Layer

This is where task decomposition, tool-calling, multi-step reasoning, and response generation happen. The Model Context Protocol (MCP) structures communication between the agent and the context layer. When an agent in Claude Desktop or Cursor needs to resolve an entity, it requests context through a permissioned MCP server rather than querying raw table schemas directly. The server returns the authoritative definition and SQL implementation (including relevant column names) for that entity, so the agent never encounters raw column names and has no reason to hallucinate them.

Jedify implements this pattern through an MCP server that exposes the semantic context graph to external AI tooling without surfacing the underlying schema, keeping access controls intact while giving those tools the grounding they need to work reliably.

Context Is the Hard Problem, Not the Model

If your agent is returning wrong ARR numbers, upgrading to a different (or better) LLM is not going to fix it. The problem is definitional ambiguity, and that lives in your data stack, not in the model.

The failure mode to understand here is entity disambiguation. Take a straightforward query: “What’s our net ARR this quarter?” Across a typical enterprise data stack, “net ARR” can quietly resolve to at least three different calculations:

  • The Salesforce CRM export, which calculates ARR from signed contract value with a manual churn adjustment maintained by the Customer Success team
  • The Redshift warehouse, which calculates ARR from actual invoiced amounts with a different treatment for multi-year deals
  • The dbt model, which follows finance’s definition and excludes professional services revenue that sometimes sneaks into the other two

An agent hitting your raw schema in the database will generate a different SQL query depending on which table it finds first, which column names look familiar, and which joins seem plausible based on field name patterns. Three queries, three numbers, all technically valid SQL, but none of them necessarily matching what your CFO actually means when they ask about net ARR.

This is well-documented territory. dbt Labs’ analysis of agentic analytics puts it plainly: agents fail at data tasks because they lack business definitions, tribal knowledge, and data source disambiguation. SQL construction ability is not the bottleneck. Informatica’s research on autonomous data agents arrives at the same conclusion: high-quality, well-governed, accessible data is the critical foundation.

A properly built semantic layer solves this at ingestion time. It mines your query logs (the SQL your analysts actually run, which reflects real metric definitions), applies NER across runbooks and wiki documents, extracts calculated field definitions from LookML or dbt, and runs co-occurrence analysis to resolve conflicting definitions. The result is a single semantic entity for “net ARR” with one authoritative definition and one SQL implementation. When the agent queries through the MCP server, it gets that entity and produces one unambiguous answer.

Real-World Use Cases: RevOps, Customer Success, and Product

The architecture argument is easier to follow when you see it in contexts where the questions are genuinely complex and wrong answers have dire consequences.

In RevOps, imagine an agent that’s continuously monitoring pipeline data and detects when deal velocity drops below a configured threshold. Without anyone asking, it runs a root cause analysis: stage-by-stage conversion, rep-level breakdown, time-in-stage delta versus the prior quarter. It writes up a natural-language summary with key drivers ranked by impact and drops it in Slack. The RevOps analyst never opens a dashboard. The insight arrives with the full reasoning visible, and the actual strategy decisions stay with the humans.

In Customer Success, agents can run account health scoring by pulling product usage data, support interaction counts, contract data, and engagement signals across connected sources. Churn risk and expansion opportunities surface before the CSM even opens their account view. The scale is what makes this genuinely useful: an agent can assess all 500 accounts in a portfolio at the same time and flag the 12 showing correlated disengagement signals. That’s not something a CSM team can do manually every morning. Contract changes and outreach decisions still belong to the CSM.

In product analytics, root cause analysis is where agentic architecture really earns its keep. When a product manager asks “Why did feature adoption drop 18% in the last two weeks for enterprise accounts?”, a standard NLQ system returns a number. That number tells you what happened. It has nothing to say about why.

An agentic system breaks the question apart: which enterprise cohort, which feature surface, what changed in the deployment timeline during that window, whether support ticket spikes appeared at the same time. It runs cross-metric correlation queries across product usage, deployment logs, and support data, then comes back with a root cause analysis showing every intermediate query and finding. The answer is auditable and actionable in a way no single chart can match.

What Makes or Breaks Production Deployment

Getting agentic analytics into production is one thing. Keeping it accurate and trustworthy at scale is another. Three things tend to determine whether it holds up: governance, observability, and how you sequence the rollout.

Governance comes down to access scoping at the semantic layer. An agent with read access to your entire data warehouse is a security risk that scales with the size of that warehouse. When agents query through named semantic entities instead, access controls apply at the entity level. An agent configured for RevOps can see pipeline entities and nothing in HR or payroll, even if those tables sit in the same Snowflake account. RBAC at the entity level within the context graph keeps access controls consistent regardless of which AI tool connects through the MCP server.

Observability is the feedback loop that keeps the system honest over time. Thumbs-up/down feedback on agent responses is the primary signal for catching when entity definitions are wrong, incomplete, or ambiguous. When an analyst rates a response negatively and notes “this calculated churn wrong,” you know exactly which entity needs attention. A simple weekly process where a data engineer reviews low-rated responses, identifies the definition that caused the error, and updates the semantic layer is how the context graph gets better rather than drifting.

Rollout sequencing is where most teams trip up by trying to deploy agents across all domains at once. Start with one well-bounded domain where ground truth is independently verifiable. RevOps pipeline metrics are a good starting point because you can compare agent output directly against a known analyst answer and catch errors before they spread. Run that comparison for two weeks, validate accuracy, then expand by adding semantic entities for the next domain. The agent layer stays the same. The context graph grows.

Gartner’s enterprise AI adoption analysis projects that 33% of enterprise software will include agentic AI capabilities by 2028. Teams that invest in the semantic layer now will be able to extend agents into new domains without rebuilding from scratch each time. Teams that treat it as a problem for later will find their agent accuracy capped by whatever institutional knowledge never made it into documentation.

Audit Your Semantic Layer for Agent-Readiness

Before you start evaluating agent platforms, it’s worth assessing objectively where your semantic layer actually stands. Four questions that cut straight to it:

  1. Are your core business metrics defined in code with explicit formulas? A dbt metric definition or a LookML measure counts. A wiki page that explains what a metric means does not.
  2. Are entity relationships, like account to opportunity to ARR, documented with the join logic that actually implements them?
  3. Is there a single authoritative definition for ambiguous terms like “active user,” “churned customer,” or “qualified pipeline”?
  4. Does that definition point to a specific table and column, or does it only exist in a runbook somewhere?

If any of those answers is no, that’s your context debt. And it will show up as agent errors no matter which LLM you put in the driver’s seat.

The good news is you don’t have to solve all of it at once. Start with the metric your team argues about most, the one behind every “wait, which number are we using?” thread in Slack. Turn it into a proper semantic entity: write the business definition, nail down the SQL implementation, document its relationships to adjacent entities. Then test it. Can your agent answer three different versions of that question correctly, with its reasoning visible? If yes, you have your foundation.

That’s the approach that actually works in practice: one domain, one entity at a time, context graph growing steadily while the agent layer stays stable. It’s less exciting than a big-bang deployment and considerably more likely to produce a system people trust six months from now.

What does your current semantic layer actually cover? If you’ve been working through the context debt problem in your own stack, it’d be great to hear what’s worked.

Why MTP doesn’t speed up your llama.cpp inference (and how to actually fix it)

Last week, I spent two days banging my head against a wall. I had just spun up a fresh llama.cpp build with multi-token prediction (MTP) support, loaded a quantized Qwen3 model, and ran my benchmark suite expecting that sweet 2-3x speedup everyone keeps talking about.

The result? Roughly the same tokens per second. Sometimes slower. After a lot of profiling, I figured out what was happening — and it turns out the issue is more common than the celebratory benchmark posts suggest.

This post is for anyone who’s enabled MTP, expected a speedup, and got nothing.

What MTP actually does (the short version)

Multi-token prediction is a form of speculative decoding baked into the model itself. Instead of running a separate, smaller draft model to guess the next few tokens, the main model emits multiple candidate tokens per forward pass. The verifier (usually the same model with a slightly different head) accepts or rejects them in one shot.

The theory is simple. If acceptance rate is high, you get 2-3 tokens per forward pass instead of one, with roughly the same latency per pass. In practice, MTP can make things worse if any of three things go wrong.

The three reasons MTP fails to speed things up

Here are the actual root causes I hit, in order of frequency:

1. Low acceptance rate

This is the big one. MTP only helps if predictions are accepted. If your acceptance rate is below ~60%, you’re paying the extra compute cost of generating drafts without getting tokens back. Wall-clock time goes up.

I see this most often when:

  • The prompt is unusual (specific code style, niche domain)
  • Temperature is too high (anything above ~0.7 starts hurting)
  • The model was quantized aggressively and the MTP head suffered more than the main weights

2. KV cache thrashing

When you generate multiple candidates per step, you churn the KV cache more aggressively. On consumer GPUs with limited VRAM, this can spill into slower memory or cause re-allocation. The forward pass speedup gets eaten by memory stalls.

3. CUDA graph capture failures

This one bit me hard. llama.cpp tries to capture CUDA graphs for the inference loop. If MTP introduces dynamic shapes (variable number of accepted tokens per step), the graph gets re-captured every step. You lose the performance win of graphs entirely, and the per-step overhead actually goes up.

Step-by-step: diagnosing your setup

Here’s the order I work through now whenever MTP doesn’t seem to help.

Step 1: Measure the actual acceptance rate

llama.cpp surfaces speculation metrics with verbose logging. Build with CUDA support and run with -v:

# Build llama.cpp with CUDA support
cmake -B build -DGGML_CUDA=ON
cmake --build build --config Release -j

# Run with verbose stats so we can see acceptance numbers
./build/bin/llama-cli 
  -m models/qwen3-quantized.gguf 
  -p "Write a Python function for binary search" 
  --n-predict 256 
  -ngl 99 
  -v 2>&1 | tee run.log

Then grep the log for the speculation stats. You’re looking for an n_accept ratio. Below 0.6 means MTP is actively hurting throughput on your workload.

Step 2: Check VRAM headroom

If acceptance is fine but throughput is still bad, you’re probably memory-bound. Watch VRAM usage during inference in a separate terminal:

# Poll memory and GPU utilization once per second
nvidia-smi --query-gpu=memory.used,memory.total,utilization.gpu 
  --format=csv -l 1

If you’re sitting at >95% VRAM utilization while running, MTP’s extra KV cache pressure is pushing you over the edge. The fix is usually to reduce context length, drop to a more aggressive quant (Q4_K_M instead of Q5_K_M), or shorten the draft window.

Step 3: Disable CUDA graphs as a control

To check whether graph re-capture is killing you, force graphs off and re-run:

# Disable CUDA graphs to test if they're being re-captured each step
GGML_CUDA_DISABLE_GRAPHS=1 ./build/bin/llama-cli 
  -m models/qwen3-quantized.gguf 
  -p "Write a Python function for binary search" 
  --n-predict 256 
  -ngl 99

If throughput is roughly the same with graphs disabled, capture isn’t your problem. If throughput goes up with this flag set, that’s the smoking gun — graphs were being re-captured every step under MTP and the overhead was worse than not using them at all.

The actual fix

Once you’ve identified which of the three issues you’re hitting, the fix is usually simple:

  • Low acceptance — shorten the draft window. Most MTP implementations let you set a draft length of 1-4 tokens. Dropping from 4 to 2 often pushes acceptance above 70% because the model has to commit to fewer guesses in a row.
  • VRAM pressure — reduce context length or quantize more aggressively. KV cache size scales linearly with context, so cutting --ctx-size in half buys you real headroom.
  • Graph capture churn — pull the latest llama.cpp. The speculation code path changes frequently and padded graph capture has improved a lot recently.

Here’s the config that finally worked for me on a quantized Qwen3 model with around 24 GB of VRAM available:

# Final working config — moderate draft length, conservative context
./build/bin/llama-cli 
  -m models/qwen3-quantized.gguf 
  -p "$PROMPT" 
  --n-predict 512 
  --ctx-size 8192 
  --draft-max 2 
  --draft-min 1 
  -ngl 99

That gave me roughly 1.7x throughput over the no-MTP baseline on my workload. Not the magical 3x some posts claim, but a real, repeatable win that I could ship.

Prevention tips

A few things I now do by default whenever I touch MTP:

  • Always benchmark with and without MTP. Don’t trust that it’s helping just because it’s enabled. Run both, measure both, save the numbers.
  • Pin your llama.cpp version. The MTP code path changes frequently. A config that works today can regress between commits.
  • Match quantization to the head carefully. Some MTP heads are sensitive to aggressive quantization. If acceptance rate suddenly tanks after a re-quant, that’s usually why.
  • Log acceptance rate as a metric, not just throughput. Throughput tells you the symptom; acceptance rate tells you the cause. When you can see both side by side, regressions become obvious.

The honest takeaway is that MTP is a real win when the conditions line up, but it isn’t free. If you’ve enabled it and gotten nothing, you’re not doing it wrong — you’ve just hit one of the failure modes nobody talks about in the benchmark threads. Walk the three steps above and you’ll usually find the culprit within an hour.

Longest Substring Without Repeating Characters — LeetCode #3 (Medium)

The Problem

Given a string s, return the length of the longest substring that contains no duplicate characters.

Example

Input: s = "abcabcbb"

Output: 3

The answer is “abc”, with the length of 3. Note that
“bca”
and
“cab”
are also correct answers.

Constraints

  • 0 <= s.length <= 5 * 10^4
  • s consists of English letters, digits, symbols and spaces

The Key Insight

Here’s what we’re throwing away. When we hit the duplicate ‘a’, we already knew the window held ‘a’, ‘b’, ‘c’. We had that information. What if instead of restarting, we just nudged the left edge forward until the duplicate was gone?

That’s the sliding window. A left pointer and a right pointer. Right grows the window, left shrinks it. We need instant answers: is this character already in the window? That’s a set. Think of a guest list, names only, no duplicates.

You might think to add the character first, then check. Don’t. Check first, then add, or you’d match a character against itself. The rule: while right’s character is in the set, remove from the left and slide left forward. Then add right’s character.

Why It Works

Notice what happened. Both pointers only ever moved right. Every character entered the set once and left once. No backtracking.

Walking Through It

Left and right both start at zero. ‘a’ is not in the empty set. We add it. Window is one wide. Max is one. Right moves to one. ‘b’ is not in the set. Add it. Window spans zero to one. Max is two.

Right to two. ‘c’ is not in the set. Add it. Window is three wide. Max becomes three. Right to three. That’s ‘a’ again. ‘a’ is already in the set. Duplicate.

Remove ‘a’ at index zero, slide left to one. ‘a’ is out of the set. Add the new ‘a’. Window spans one to three. Right to four. ‘b’ is in the set. Remove ‘b’ at left, slide to two. Add new ‘b’. Window is two to four. Max still three.

Right to five. ‘c’ repeats. Remove ‘c’ at left, slide to three. Add ‘c’. Window three to five. Max still three. Right to six. ‘b’ is in the set. Remove ‘a’ at left, slide. ‘b’ is still there. Remove ‘b’ too, slide again.

Now ‘b’ is clear. Add it. Window is just two wide. Right finishes at seven the same way. Max never exceeded three.

Complexity

Each character enters the window once and leaves once. The set holds only the current window. Time and memory both grow with the string’s length.

The Code

OK, same logic in Python. We initialize the set, the left pointer, and the running max. The outer loop walks right one step at a time through every character.

The inner while keeps removing from the left until the repeat is out of the set. Then we add the new character and check if this window beats our best.

Hand back the best length we found.

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        seen = set()
        left = 0
        max_len = 0
        for right in range(len(s)):
            while s[right] in seen:
                seen.remove(s[left])
                left += 1
            seen.add(s[right])
            max_len = max(max_len, right - left + 1)
        return max_len

Wrap-up

And that’s the pattern. Grow until you can’t, shrink until you can. You’ll see this shape again.

📺 Watch the full walkthrough on YouTube: https://youtu.be/dZ395_AbzcA