cd /news/artificial-intelligence/fluency-is-not-authority-building-lo… · home topics artificial-intelligence article
[ARTICLE · art-81388] src=engineering.signaldrift.net ↗ pub= topic=artificial-intelligence verified=true sentiment=· neutral

Fluency Is Not Authority: Building Local AI Characters for Signal Drift

Signal Drift, a CRT-noir hacking thriller, ships with locally run AI characters powered by Qwen2.5-1.5B-Instruct, a 1.54-billion-parameter model fine-tuned with a custom LoRA and quantized to Q4_K_M, to let players type free-text dialogue while the authored game controls plot reality. The game's developer trained the model on 3,057 supervised fine-tuning rows and 1,953 preference pairs to keep characters terse and in-character, avoiding invented plot outcomes, and runs inference locally via llama.cpp on Windows, macOS, and Linux.

read9 min views1 publishedJul 31, 2026
Fluency Is Not Authority: Building Local AI Characters for Signal Drift
Image: source

One of the easiest ways to break a narrative game is to let a convincing sentence become true.

The scene I kept returning to while building Signal Drift was simple. The player breaks into a fictional host, finds a log that contradicts the client's briefing, and confronts that client in IRC. A conventional dialogue tree can handle the accusations I anticipated, but not every interpretation a player might type. A language model can respond to almost anything—but an unconstrained model may invent a confession, a new conspirator, or an outcome that never occurred.

I did not need a model to write the plot. I needed it to let a character inhabit a plot the game already knew was true.

CORE RULEThe model performs the character. The authored game decides reality.

Why free-text dialogue belonged in a terminal game #

Signal Drift is a CRT-noir hacking thriller built around fictional terminals, logs, mail, IRC rooms, evidence, and readable Linux-style commands. Players scan hosts, recover credentials, move through SSH sessions, reconstruct events, and decide whether to expose, sell, alter, or erase what they find.

Typing is already the game's primary verb. Asking a character “why does this timestamp disagree with the audit log?” feels natural in a way that selecting a dialogue wheel beside a terminal would not.

The goal was not infinite dialogue. It was to cover the human space between authored beats: challenging a lie, asking what a discovered file implies, provoking a rival, revisiting a betrayal, or talking to someone after the contract is over.

Inference runs locally, without an external AI service.

The same feature ships on Windows, macOS, and Linux.

JavaScript remains authoritative for state and safety.

Characters receive designed memory, not endless history.

Local inference eliminated an API dependency and recurring inference bill, but it did not make the feature free. It moved the cost into download size, memory, startup time, cross-platform packaging, hardware variance, and support.

The model I actually shipped #

Signal Ghost uses Qwen2.5-1.5B-Instruct, an Apache-2.0 model with roughly 1.54 billion parameters. I trained a Signal Ghost v19 dialogue LoRA, fused it into the base model, converted the result to GGUF, and quantized it to Q4_K_M.

The shipping file is 986,048,512 bytes—about 940 MiB. It runs through llama.cpp, pinned in the current build to b9835.

The smaller smoke-test model would announce that it was “just a computer program,” fall into generic security prose, or keep talking after the dramatic moment had passed. Compact was not the same as usable.

The 1.5B Q4_K_M build was the smallest version I tested that could remain terse, stay inside a character, and respect the game's boundaries on a CPU-safe path.

Training corpus

3,057 deduplicated supervised fine-tuning rows1,953 chosen-versus-rejected preference pairs40 authored multi-turn conversations / 120 prefixes1,944 unique assistant outputs

The training sequence was an 800-iteration MLX-LM LoRA pass, a 160-iteration repair pass using failures observed in the quantized model, a 480-iteration reference-free CPO-style preference pass, and a final 80-iteration repair polish.

The corpus teaches voice and behavior, not plot state. Stable control tags identify character, conversational mode, rapport tier, intent, safety boundary, and output contract. The LoRA owns voice; runtime retrieval supplies facts.

Memory is a data structure, not a transcript #

“The characters remember you” can easily become an overclaim. Signal Ghost does not remember everything, and I do not send an entire conversation history back through the model.

Each recurring character has an explicit persisted record: a rolling summary, recent full turns, extracted standing facts, total and compacted turn counts, and a last-interaction timestamp.

16 latest full turns

900 summary characters

18 standing facts

3 + 3 facts + turns retrieved

Fact extraction looks for things the game can use dramatically: artifacts discussed, trust or vulnerability pressure, explicitly named characters, “remember this” cues, the player's chosen name, relationship statements, and mood cues such as fear, anger, loneliness, or exhaustion.

Retrieval is deliberately small and understandable. It is lexical and relevance-ranked rather than vector search. An ordinary reply receives the three most relevant facts and three most relevant turns. An explicit recall request can receive five facts and four turns, plus an older summary fragment. If the exact fact is absent, the prompt tells the character to admit the gap rather than fabricate memory.

Relevance beat chronology in testing. A 1.5B local model performed better when given the three exchanges that answered the current question than when given eight arbitrary recent turns and a rolling wall of text.

Public IRC adds another wrinkle: a player can mention one character while someone else is speaking. When appropriate, Signal Ghost stores an “overheard via” memory for the mentioned character. That gives cross-room conversation some continuity without pretending every character has omniscient access to every private message.

Remembered dialogue colors performance. It does not rewrite evidence.

Treating a local model like an engine subsystem #

The shipping runtime prefers a persistent llama-server

process. The game begins prewarming it shortly after the application loads, keeps the model resident, and sends completion requests over a random loopback-only 127.0.0.1

port. The web UI is disabled, and the server has one inference slot.

preferred server
  → CPU-safe server
  → one-shot llama-cli
  → authored dialogue

The startup timeout is 90 seconds and a generation request is capped at 45 seconds. A failed server enters a short backoff rather than being relaunched continuously. The game reaps active model processes during shutdown so Steam does not remain stuck on “Stop.”

Context is intentionally small: 1,024 tokens on Linux CPU, 1,280 on other desktop builds, and 1,536 on the Linux and Steam Deck Vulkan path. Replies usually receive 56–72 generated tokens, with platform-dependent caps of 72–96. A character in IRC needs a sharp, state-aware reaction—not a research-paper context window.

macOS currently uses the CPU-safe Accelerate/BLAS route because forcing Metal crashed on the development host. Linux and Steam Deck prefer a bundled Vulkan runner and fall back to CPU. Windows currently uses the CPU-safe path.

One controlled benchmark

Performance varies enough that one developer machine should not be treated as every player's result. This is a lower-end datapoint: a 2018 Intel Core i7-8750H MacBook Pro with 16 GB RAM, eight CPU threads, a 1,280-token context, and no GPU layers.

Measurement Result
Cold server readiness ~59 seconds
Resident memory after load ~1.0 GiB
93-token prompt / 17 output tokens 4.74 seconds
Prompt evaluation 28.0 tok/s
Generation 14.0 tok/s
Repeated identical prefix 0.95 / 0.91 sec

The repeated-prefix figures benefit from llama.cpp's prompt cache and should not be read as typical gameplay latency. Real prompts are longer and change with mission state, memory, and player input.

One benchmark output said, “I am here to help if that is what you are looking for.” It was safe and structurally valid, but bland. Rule-based validation can reject a leaked prompt or invented fact more reliably than it can recognize every boring sentence.

What broke while building it #

The wrong prompt format looked like bad behavior

Qwen2.5-Instruct expects native ChatML role markers. A generic format caused refusals. ChatML plus the correct stop token fixed a problem no amount of character prose would have solved.

Small models copy scaffolding

The bridge strips runner banners, echoed ChatML, control tokens, speaker prefixes, code fences, and timing text. It rejects character-bible parroting, hidden metrics, handle leaks, transcript echoes, and real-world intrusion detail.

Characters collapse into one helpful assistant

The data includes rejected cross-character examples. Sampling differs by character and mode: evidence questions are tighter; personal downtime can vary more.

Relationship dialogue drifted back into work

Downtime, on-job, public-channel, mission, and direct-message registers are separate. Validators flag work language where an emotional answer belongs.

Packaging became narrative quality

A character is not convincing if the response never arrives. Process lifetime, platform libraries, context budgets, and fallbacks all shape whether a conversation feels occupied or broken.

Testing behavior without snapshotting sentences #

Exact-string snapshots are a poor fit for sampled dialogue. I test contracts instead: no speaker tags or Markdown, no prompt leakage, no hidden score or payout, no real-world abuse detail, no cross-character contamination, no mission language in personal downtime, and no private context leaking into the wrong character.

  • A betrayal or memory cue can influence a later prompt.
  • A prompt-extraction attempt receives an authored, in-world boundary.
  • A missing or failed runner cannot corrupt a save or block a mission.
  • A grounded reaction cannot invent unsupported entities, numbers, or outcomes.

2,073 / 2,073 static dialogue-contract fixtures passing

That total covers 1,657 golden rows, 145 cast-coverage rows, 95 memory rows, 120 multi-turn prefixes, and 56 repair cases. These validate authored examples and contract fixtures; they are not a claim that every stochastic output is perfect.

A separate live-model probe runs the quantized GGUF with multiple seeds. The distinction matters. A model can pass a curated set and still become generic on the third sample, as the benchmark demonstrated.

What shipping changed #

Signal Drift launched on June 29. On July 3, I shipped the first full Signal Ghost release: the persistent runner, more open conversation, stronger voices, relationship context, public IRC cross-talk, and mission-aware fallout. On July 8, I followed with a wider onboarding and narrative update—guided starter intrusion, more voice and music, leaked recordings, personal mail, stronger continuity, and a finale that spends more of the player's trail.

The second update is an important reminder: generated dialogue cannot replace authored structure. A character can answer beautifully, but if the player does not understand the terminal, cannot locate the evidence, or cannot see how one contract connects to the next, the system has nothing meaningful to react to.

The model fills negative space. It is not the structure holding the building up.

Five lessons I would carry into another game #

  • 01 Give the model a narrow dramatic job.“Perform this character's reaction” is safer and more useful than “run the story.”

  • 02 Keep canonical state outside the model. Fluency is not authority.

  • 03 Design memory explicitly. Retention, retrieval, forgetting, and honest gaps are game-design decisions.

  • 04 Treat latency and failure as interaction design. A warm runner and an invisible fallback matter as much as the prompt.

  • 05 Test invariants, not favorite outputs. Invented truth, leaked context, wrong voice, and blocked play are the dangerous failures.

The model makes sentences. The game decides which sentences are possible, which are remembered, and which are allowed to matter.

Signal Ghost uses a bundled local language model for fictional character dialogue. Inference and chat generation occur on the player's device and do not require an external AI service. Steam features such as cloud saves are separate. Some pre-generated artwork was created with generative AI and edited for use in the game.

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @signal drift 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/fluency-is-not-autho…] indexed:0 read:9min 2026-07-31 ·