DEV Community

Cliff K
Cliff K

Posted on AI-assisted

typeghast: a typing game haunted by your coding agent's context window

Hacktoberfest Open-Source AI Challenge: Ghost in the Machine Submission πŸ‘»πŸŒΌ

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine

What I Built

There's a dead stretch in every coding-agent session. You hand over a task, the agent starts reading files and running tests, and you sit there watching it scroll. Meanwhile, out of sight, its context window fills up with every file it read, every log it printed, every plan it revised. Past a certain point the agent starts to forget and to contradict itself. People call it context rot, and Context Rot: Why Your AI Coding Agent Gets Dumber Mid-Session by enjoy_kumawat describes it well.

typeghast turns that dead stretch into a typing game, and turns context rot into actual ghosts.

typeghast: typing what the agent just did while ghosts named after its context swarm the text, until a compact exorcises them

You run it in a split pane next to your agent. It reads the agent's session as it happens and:

  • gives you the agent's work to type. A local Gemma model turns what the agent just did into short changelog lines: "Implemented a cost pumpkin in the header for tracked sessions." So you learn what changed while you wait, instead of finding out from the diff afterwards.
  • fills the screen with ghosts as the context fills up. They drift over the words and hide letters. Some hunt your cursor. Near the peak they turn a rotten green.
  • names every ghost after something haunting the context: ci logs, webhook retries, pagination test. They're the agent's past, still hanging around in the window, taking up space.
  • exorcises them when you /compact or /clear. The context drops and the ghosts flee.
  • pays you to type through it. The score multiplier climbs to Γ—3 at the peak, and a πŸŽƒ pumpkin in the corner shows what the session would have cost at API prices.

Haunted mode: ghosts named after the agent's context drift over the line being typed, with the context meter and cost pumpkin in the header

That's the theme as I read it: a ghost in the machine that is literally the machine's own past, and a small ritual for laying it to rest.

There's also a normal mode with no agent: you type today's top headlines from DEV, Lobsters and Hacker News. Type a headline without a mistake and its link unlocks.

Normal mode: typing a trending DEV headline, with the unlocked link shown under the text

Demo

go install github.com/cliffkoome/typeghast@latest
typeghast --demo
Enter fullscreen mode Exit fullscreen mode

--demo needs nothing else. It plays a made-up agent session, and you control the context with PgUp and PgDn. Home or ctrl+l compacts and triggers the exorcism. To play it for real, run plain typeghast while a Claude Code session is working, and it finds the session on its own, or lets you pick one if several are running.

Prebuilt binaries for Linux, macOS and Windows are on the v0.1.0 release.

Code

GitHub logo cliffkoome / typeghast

A terminal typing game haunted by context rot

typeghast

A terminal typing game haunted by your coding agent's context window.

πŸ“– Read the story behind it on DEV how it reads the agent's context, and how Gemma 4 went from 90 seconds per batch to a few seconds.

typeghast: typing what the agent just did while ghosts named after its context swarm the text, until a compact exorcises them

You kick off a coding agent, then wait. typeghast gives you something to do in that terminal while it works, and it plays off what the agent is doing:

  • You type what your agent just did. A local open-weight model reads the agent's session as it happens and writes short changelog lines for you to type: "Added retries with backoff to the webhook client so flaky deliveries recover."
  • Ghosts are context rot. The fuller the agent's context window gets, the more ghosts drift over the words and hide letters. Some hunt your cursor and as the haunting nears its peak they turn a rotten green. Each one is named…

Go, Bubble Tea and Lip Gloss, MIT licensed. It's a single binary with no runtime dependencies.

How I Built It

Reading the agent's mind (well, its transcript)

Claude Code writes each session to a JSONL file under ~/.claude/projects/. typeghast tails that file. Each reply records its token usage, so the context size is simple to get: input + cache reads + cache writes + output, taken from the latest reply.

How full is "full"? A fixed number of tokens would be wrong for most models, so the haunting peaks at a fraction (default 40%) of the model's real context window. Window sizes and prices come from LiteLLM's open model catalog.

Two things in the transcript caught me out:

  • Every reply is logged several times, once per content block, up to four copies. Add up usage naively and the cost pumpkin overcharges you up to 4Γ—. Usage is now counted once per message id.
  • Subagent turns are in the same file. They cost money, so they count toward the pumpkin. But they run in their own context, so they must not count toward the ghosts.

The narrator: Gemma 4, on my own GPU

Every few actions, typeghast sends a batch of them (files edited, commands run, what the agent said) to a model running locally in Ollama. The model returns JSON: 2 to 4 sentences to type, plus 1 to 3 names for the ghosts. If Gemma is installed it's picked automatically.

Here's real output from the session where I built the cost pumpkin:

Added tests for deduplicated spend and pricing logic.
Implemented a cost pumpkin in the header for tracked sessions.
Rendered a haunted frame from the session transcript.

ghosts: cost pumpkin, haunted frame, status.md

The part where Gemma 4 was unusable, and then wasn't

I compared Gemma 4 12B with Qwen 2.5 7B on an AMD Radeon RX 6600. Both got the same four batches of real events from one of my sessions. Gemma's first run was a disaster:

  • about 90 seconds per batch
  • 2 of 4 batches came back with no JSON at all

ollama show gemma4:12b gave it away: Gemma 4 has thinking on by default. It was reasoning at length before every answer, and sometimes it ran out before it ever wrote the JSON. A typing game can't wait 90 seconds for its next sentence.

The fix was one field in the request:

body, _ := json.Marshal(map[string]any{
    "model":  n.Model,
    "stream": false,
    "format": "json",
    // Thinking models like Gemma 4 otherwise reason for minutes before
    // answering, and sometimes run out before writing the JSON.
    "think": false,
    // Narration comes in bursts; don't reload the model between them.
    "keep_alive": "30m",
    // ...
})
Enter fullscreen mode Exit fullscreen mode
Gemma 4 12B Qwen 2.5 7B
First call (loads the model) 9 s 5.4 s
Later calls 3.3–6.1 s 1.5–1.8 s
Valid JSON 4/4 (was 2/4) 4/4
Sentences clean changelog lines, consistent tense choppier, mixed tenses

Qwen is faster, but Gemma writes the better lines, and in play the speed doesn't matter. Narration runs in the background while you type the lines already queued, so you never see the wait. Gemma stays the default.

It turns out think: false matters for more than speed. Ollama skips your JSON schema when a thinking model answers without thinking by homelabpm explains why. On the same Ollama version I'm running, a thinking model only gets the format enforced after its thinking block. With thinking off, the format applies from the very first token. typeghast also checks every reply and drops anything malformed.

The comparison turned up one more bug. My prompt included example ghost names, and Qwen copied them word for word in 3 of 4 batches. Whatever the agent had actually done, the ghosts came back as model.go rewrite, flaky test logs and old todo list. I replaced the examples with an instruction: take each name from the actions above, never invent one. After that, both models named real things every time.

Making typing feel good

A typing game lives or dies on feel, so:

  • Correct words flash gold.
  • Speed lines stream under the letters you just typed once you pass 70 WPM. They grow longer and hotter (yellow, orange, red) up to 130, and they wrap back to the line above, so they don't vanish when you start a new line.
  • The screen shakes, but only on big moments: a perfect sentence, an unlocked link, every 25-word streak, an exorcism. Shaking on every word made the text jump while you were reading it, so I didn't.

Why Open Matters Here

Agent transcripts are private by nature. They hold your source code, your file paths, your shell commands, sometimes a secret printed in a log. Sending all of that to someone else's API to power a typing game would be absurd. With an open-weight model in Ollama, the transcript is read from your disk and summarised on your GPU, and none of it leaves your machine.

It's free, so it can run constantly. The narrator fires every few actions for as long as the agent works, all day if you let it. Each call is cheap, but metered calls add up. Locally the marginal cost is zero, so the game can narrate everything instead of rationing calls.

The narrator works offline. No API key, no account, no rate limit, nothing to sign up for.

I could see inside it and fix it. The Gemma problem was solvable because everything was open: I could read the model's capabilities, toggle its thinking mode, and compare it head to head with another open model on my own data, in an afternoon. With a closed endpoint I'd have been guessing at a black box.

Nothing is locked in. --model takes any Ollama model, and with no model installed typeghast falls back to the agent's own words. The game still works for everyone. The open model is what makes it good.

Prize Categories

  • Best Use of Gemma. Gemma 4 12B is the default narrator. It writes every sentence you type and names every ghost, entirely on-device, and this post documents how I got it from about 90 seconds per batch to 3–6 seconds.

Top comments (0)