TextGen 5.0 — Custom Build
A local-LLM desktop build based on oobabooga/textgen, with a purpose-built memory, voice and image layer bolted into the UI. This page is the feature comparison against the public upstream release, plus the demo order for the walkthrough. Ready-made sample outputs for the camera are on the companion showcase page.
What's on this page
- The short version
- Side-by-side comparison
- The custom extension suite
- Extension framework changes
- Engine & model-management work
- Reliability fixes & reply hygiene
- Performance engineering
- What came from upstream (not our work)
- Portable setup, rollback & tooling
- Recent work timeline
- Demo order for the video
- Credits & licence
1.The short version
Four things to say out loud, then the detail is just backup.
- It's the real app, not a replacement. Same codebase and everything upstream ships — multiple backends, OpenAI/Anthropic-compatible API, tool calling & MCP, vision, file attachments, LoRA training, notebook, themes — running on a newer snapshot than the public v4.9 release.
- Memory is the headline. Upstream ships no built-in persistent memory for characters; that's left to community extensions with their own dependencies. This build has a purpose-built memory layer with no heavy packages: per-character JSON store, keyword and semantic retrieval, cross-session carry-over, persistent mood/relationship/style state, a validated rolling summary, and a session diary.
- Voice and images are local and wired into the chat box. Kokoro-82M text-to-speech through a portable Node runtime, and Stable Diffusion through
stable-diffusion.cppon Vulkan — neither needs PyTorch, and both are reachable from a command or from the AI's own reply. - Plus engine work upstream hasn't released: llama.cpp b11130 with the new
--load-modeflag surface, MTP/draft heads filtered out of the Model dropdown, and hardening so one broken extension can't take the app down.
Framing that keeps it honest on camera: this is a personal build that stands on upstream's work. Upstream provides the app, the loaders, the API and the UI. What's custom is the extension layer, the engine currency, and the integration work that makes those pieces behave like one product.
2.Side-by-side comparison
Upstream column = what the public v4.9 release and its docs describe. Right column = this build.
| Capability | Upstream TextGen (v4.9) | This build |
|---|---|---|
| Core app: chat / notebook / parameters / model / session tabs | Shipped | Inherited — on a newer development snapshot |
| Backends (llama.cpp, ik_llama.cpp, Transformers, ExLlamaV3, TensorRT-LLM) | Shipped | Inherited (llama.cpp binaries updated locally) |
| OpenAI/Anthropic-compatible API, tool calling, MCP servers | Shipped | Inherited |
| Vision, file attachments, LoRA training, themes, LaTeX | Shipped | Inherited |
| Persistent memory per character | Not built in — community extensions, extra dependencies | Custom layer JSON store, no heavy packages |
| Memory retrieval | — | Keyword and semantic (local bge-small-en-v1.5 embeddings) |
| Cross-session continuity | — | Carry-over of the previous session's tail + greeting variation |
| Persistent emotional & voice state | — | Mood / energy / patience, relationship score, writing-style profile |
| Rolling conversation summary | — | Background thread, validated, injected as older-context |
| Summarise-on-command | — | /sum, /sum5…/sum50, /sumall with a fixed output contract |
| Session diary | — | /diary writes a dated journal entry |
| Workspace files (browse, edit, attach) | Chat attachments only — no shared workspace panel | A panel: folder setting, file list, editor with per-save backups and undo, search, stats, and attach-a-file-to-your-next-message |
| Verbatim recall of past messages | — | Tag-driven recall from timestamped logs, human-readable times |
| Time / weekday / time-of-day in the prompt | Not in core | Two tiers: time of day in words every turn, exact clock/date only when asked, with leak-protection in replies |
| Reminders | — | Weekly / monthly / yearly / one-off, throttled, on-off switch |
| Text-to-speech | Silero / Coqui extensions (PyTorch) | Kokoro-82M Q8 via portable Node — no PyTorch, per-character voices, play button in the composer |
| Hands-free voice loop | Whisper STT extension (dictation) | Mic → Whisper → local TTS → speaker, from one panel (early stage) |
| Image generation | Diffusers tab (PyTorch, quantised, gallery) | stable-diffusion.cpp on Vulkan, models bundled, plus in-chat /image and AI-triggered images |
| Extension management UI | Extension list / accordion area; every extension toggleable | Dedicated Extensions top-level tab; core panels always on and hidden from the disable list; panels start collapsed |
| Behaviour when an extension has a missing dependency | Can abort startup in the snapshot this build started from | Logs the error and skips that extension — the app keeps starting |
| llama.cpp build & flag surface | Whatever the release bundles | b11130 (0.4.1-dev, Vulkan); UI's mmap/mlock boxes translated to --load-mode, with a fallback for older builds |
| Model dropdown hygiene | Draft/MTP heads can appear as loadable models | Detected from GGUF metadata and hidden; still selectable as model-draft |
| Web search trigger | Literal phrases ("search for", "look up") | Also fires on natural wording ("check the web for…", "anything online about…") |
| Reply hygiene | — | Strips echoed context blocks, stray HTML and copied prompt scaffolding; keeps paragraph breaks |
| Download vetting | — | A local gguf_check tool reads GGUF metadata before a file is kept |
Note on the "—" rows: those are features the core app genuinely doesn't provide, which is why they became extensions here. It isn't a knock — upstream deliberately keeps the core lean and points people at the community extensions directory.
3.The custom extension suite
Eight extensions, all in user_data/extensions/, all written for this build. Commands marked "no model in the loop" return their result directly, so they work reliably even with a small or thinking-disabled model.
Long-Term Memoryflagship
What it is: persistent, per-character memory plus realistic continuity between sessions. Pure Python — no ChromaDB, no embeddings framework, no accounts.
- Per-character JSON store that survives restarts; memories persist until deleted.
- Keyword triggers — the memory surfaces when its keyword appears.
- Semantic retrieval — an optional local embedding model (
bge-small-en-v1.5, run by llama.cpp) matches by meaning, so a memory can surface without the exact word. - Session carry-over — the tail of the previous chat is injected into a new session, with a "last chat was X ago" note and a nudge not to repeat a stock opener.
- Persistent state — mood (score, energy, patience, time decay), relationship score, and a writing-style profile, all injected as context and carried across sessions.
- Rolling summary — generated in a background thread so it never slows a reply, and validated before use.
/compact— folds the older part of a long chat into that summary and leaves it out of what the model sees, so a long session keeps its thread instead of hitting TextGen's silent truncation./compact offundoes it. Chat, log, export and recall keep every message.- Session diary —
/diarywrites a dated entry built from the session;/diary readshows it. - Shared user profile — facts known to every character, with multi-user support.
/fileworkspace — direct file read/write/edit/search/undo in a shared folder, no model involved.- AI self-save — the model can emit a save tag to record durable facts itself (separate store, duplicate-checked).
- State card, chat export, and a reply-hygiene pass over everything it injects.
/remember a fact, then mention its keyword in normal chat and watch it surface. Then /file write notes.txt | … and /file read notes.txt.File Workspaceown panel
What it is: a browser and editor for the /file workspace — set the folder, list files, open one, edit, save, search, and hand a file to the model with your next message.
- No engine of its own. It drives the same functions the AI's
/filecommands use, so the panel and the model can never disagree about the folder, the backups or the wording of a result. - Folder anywhere. Set the workspace to any path — the status line says whether it exists and suggests one that fits this machine if the configured drive isn't there.
- Edit with a safety net. Every save takes a backup, so Undo last change restores the previous version; click again to step further back.
- Attach a file to your next message and it travels inside that message — the model reads it, and your own bubble shows
📎 Attached: notes.txt (66 bytes). Capped at 4000 characters per file. - Your work and the AI's work are the same files, in one shared folder every character can use.
Summariser/sum
Turns a bare command into a structured chat summary: /sum, /sum5, /sum10, /sum20, /sum50, /sumall.
- Fixed output contract: one
[Summary]section, then up to five verbatim[Keywords]. - Written in the assistant's own first person — "we discussed", not third-person narration.
- Three hooks keep the model honest: an explicit task in the message, a prefilled
[Summary]header so it can't open with a greeting, and an output pass that trims anything before the header and caps the keyword list.
/sum20 in a long chat and point at the clean header and five keywords — no preamble, no keyword sprawl.Log Recallverbatim
Accurate recall of what was actually said earlier, straight from timestamped chat logs — no paraphrasing, no guessing.
- Tag-driven: last N messages, everything at/before a time, or everything between two times.
- Returns the literal log lines, formatted with human-readable times.
- Per-character log folders, so recall stays scoped to that conversation.
Time & Reminderscontext
Injects real local time into every message, and delivers date-aware reminders.
- Time block each turn: UTC + local time, date, offset, time-of-day, weekday — with daylight-saving handled.
- Four reminder types: weekly, monthly, yearly, one-off, stored in a simple
.ini. - Throttled delivery (default: once every 20 prompts) so it never spams; delivered as a separate assistant-side message.
- On/off switch in the panel, plus a debug log file for verifying matches.
- Cleans leaked time macros out of replies, and preserves paragraph breaks while doing it.
Text-to-SpeechKokoro
Fully offline neural speech using Kokoro-82M (Q8) through a portable Node runtime. No PyTorch, no API, no internet at runtime.
- Play button in the chat composer next to the attach icon, plus "Speak last reply" in the panel.
- Voice picker, speech-speed slider, and an optional pause after commas for natural pacing.
- Per-character voices stored in a small JSON file, so the play button uses the right voice automatically.
- Strips thinking blocks, leaked macros and HTML entities before speaking, so it never reads markup out loud.
Voice Loopearly stage
Local speak-and-react pipeline in one panel: microphone → Whisper speech-to-text → local TTS → speaker. No cloud service, no external app.
- Start/stop panel for a hands-free conversation loop.
- Uses components already in the app (Whisper, audio device access) plus the Kokoro voice worker.
Image Generationlocal · Vulkan
Stable Diffusion inside the app via stable-diffusion.cpp on Vulkan. No PyTorch/diffusers stack, no external SD web UI, models bundled with the install.
- Panel with model picker, prompt + negative prompt, size, steps, CFG and seed.
/image <prompt>in the chat box drops the picture into the conversation.- The AI can draw by emitting an image tag in its reply — the tag is replaced with the finished image.
- Per-model presets (ideal steps/CFG/negative prompt applied on selection); drop a new checkpoint in the models folder and it appears in the dropdown.
- VAE tiling on by default, so 1024×1024 fits on a 16 GB card.
/image a scene, then ask the AI for a picture and show the tag being replaced by the render.4.Extension framework changes
Small code changes, big effect on how the app feels to use — and they're the reason the seven extensions read as one product instead of seven add-ons.
| Change | Why it matters |
|---|---|
| Dedicated Extensions tab | Panels moved out from under the chat box into their own top-level tab, next to Chat and Model. The chat column stays clean; everything configurable lives in one place. |
| Core extensions always loaded | The memory, voice, image and reminder panels can't be accidentally disabled from the extension list — previously a stray click could break the UI mid-session. |
| Panels start collapsed | The tab opens as a tidy list of headings instead of a wall of open controls. |
| Resilient loader | A missing dependency now logs an error and skips that extension. Before, one broken extension (say, one wanting PyTorch) could stop the whole server from starting. |
| Settings persistence | Extension settings — including a custom folder path used by the file commands — save into the app's settings file and come back after a restart. |
5.Engine & model-management work
llama.cpp b11377, with the old UI boxes still meaning the right thing
The bundled llama.cpp has been updated twice — to b11130 (0.4.1-dev) in September and to b11377
(0.5.0-dev) on 4 October, both Vulkan builds. The newer one brings vectorised Vulkan loads with an fp16 dot
product, and probabilistic draft sampling for speculative decoding. That update removed the old
--mmap / --mlock / --direct-io flags in favour of a single --load-mode.
Rather than break the two checkboxes in the Model tab, the loader translates them:
neither box -> no flag (auto)
no-mmap -> --load-mode none
mlock -> --load-mode mmap+mlock
both -> --load-mode mlock
older binary -> falls back to --no-mmap / --mlock (probed once per binary, cached)
extra flags -> an explicit --load-mode wins; the translated one is suppressed
- The server's capabilities are probed once per binary path and cached, so an older build, a backup folder or a different fork keeps getting the flags it understands.
- The load log now reports the mode it chose, so a load line tells you which path was taken.
- The previous binary version is kept in a backup folder for instant rollback.
The llama.cpp build is visible in the Model tab
A small grey line under the model loader reads, for example, llama.cpp b11377 (0.5.0-dev). It is
read from the binary itself once per session, so a screenshot is self-describing and a bug report needs no
guessing about which build produced it.
Two ways a draft setting used to kill a load — both now caught
- A draft spec type with no draft model. The two settings live in different boxes and are applied separately, so one could be set while the other was empty — llama.cpp then read it as self-speculation against the main model and exited. The loader now says so plainly and starts without speculative decoding.
- A draft model that is the model itself. Easy to do by picking the wrong entry in the dropdown, and also possible when a second saved entry for the same model (written under a differently-cased name) quietly replaces the correct one. Treated as no draft, with the reason written into the log.
- The load line now reports
spec_typeandmodel_draftalongside gpu-layers, context and cache type, because the flags that cannot start a server were never the ones being printed.
A second model for general-purpose work
Alongside the Gemma 4 chat models there is now Qwen3.8-27B (GSQ-RCO, IQ3_S) with its vision projector
and its own instruction template: 256k trained context, thinking by default, and around 36 tok/s at a q8_0 KV
cache on this card. It is set up for document, code and analysis work rather than persona chat, which is why
spec_type is none — its MTP layer is built into the model rather than supplied as a
separate head.
Draft / MTP heads kept out of the Model dropdown
A speculative-decoding head (mtp-*.gguf, or a file whose GGUF metadata declares an
*-assistant architecture) is not a chat model on its own, but it used to show up in the model
list as if it were. Detection is metadata-first — the architecture is read straight out of the GGUF
header, so renaming the file doesn't fool it — with a filename rule as a fallback.
- Applied to the Model dropdown only: the list is filtered, so you can't pick a head by mistake.
- The right-hand model-draft dropdown is deliberately left unfiltered, because that's where a head is actually used.
- The API's model list and the Training tab also keep the full list, and a head that's currently loaded is never hidden from its own dropdown.
- The same rule lives in the download-vetting tool, so a head is flagged before you decide to keep it.
Model-tab help text corrected
Some KV-cache types are ExLlamaV3-only and silently ignored on llama.cpp — the tooltip now says so instead of implying they apply to every backend.
6.Reliability fixes & reply hygiene
| Fix | What it solved |
|---|---|
| Chat-style crash guard | A reset setting could leave the chat style empty and crash rendering; it now falls back to plain rendering. |
| Web search on natural wording | The trigger only recognised literal phrases like "search for" or "look up". It now also fires on "check the web for…", "anything online about…", "what does the internet say…" and similar. |
| Memory during tool turns | When the app re-enters generation (tool hops, regenerations) it passes empty user text, which meant memory matching returned nothing. It now falls back to the last real user message, so relevant memories still surface. |
| Reminders can be muted | Added an on/off switch and a settings flag, so reminders can be paused from the panel without disabling the extension or restarting. |
| Echoed context blocks | Models sometimes parrot the injected context back as dialogue. Those blocks are now stripped from replies, including malformed echoes, and a reply left as a bare status token collapses to nothing instead of being shown. |
| Copied prompt scaffolding | Replies occasionally opened with a copy of the injected clock or the model's own speaker label. The injection itself was reshaped to remove the cue (labelled context block, trailing the message, local values first instead of UTC), replies are cleaned before being stored for session carry-over so the old style can't be re-taught, and the opening is cleaned on the way out: a dateline stamp is dropped, a stamp woven into a sentence is repaired to the local clock, and the character files carry an explicit rule not to do it at all. |
| Paragraph breaks preserved | A whitespace tidy-up in the time-macro cleaner was collapsing blank lines, so replies arrived as one solid block. It now only collapses spaces and tabs. |
| Rolling summary validation | The background summariser could degenerate into a repetition loop or a copy of the transcript, and that text then sat in every prompt. Generations are now checked before being stored or injected; a bad one is discarded and retried, and an unusable stored summary is never injected. |
| Scaffolding that re-taught itself | The model reads its own earlier replies back in the prompt, so a dateline it copied once stayed in context and kept being copied — cleaning the visible reply never fixed that, because the prompt is built from the copy it never touched. The prompt is now assembled from a cleaned copy of the history: leading scaffolding is stripped from the model's own replies and the exact clock/date fields are dropped from older user turns, so the habit can't feed itself. The transcript, the log, the export and verbatim recall keep every message. |
| It flashed before it was cleaned | The clean-up pass ran only when a turn finished, so whatever the model wrote at the start of a reply was on screen raw until then. A new per-chunk display pass now filters the streamed text itself — a dateline, a speaker label or a bare 1. marker never reaches the screen at all. Only the display copy is filtered, so the log still holds exactly what the model wrote. |
| Status lines read like system text | The four guard notices — the ones that appear instead of a reply when a generation comes back empty or repeats itself — dropped their brackets and now read as plain sentences. A lone 1. at the start of a reply is dropped too, but only when there is no second item, so a genuine numbered list still reads as one. |
| Replies that vanished for saying "thought" | A reply that began "though" or contained the word thought at the start of a line was being treated as an unfinished reasoning tag and hidden — the turn looked empty. Found in upstream's own pull-request list, taken from it, and verified against their test cases. |
| A fresh chat inheriting an old conversation | The rolling summary was stored per character, so a new chat opened with yesterday's summary labelled "this conversation" — and the model carried on yesterday's subject instead of reading today's message. Summary and compaction are now scoped to the chat they came from. |
| A summary made of the assistant's own instructions | Talking to a persona about its card could end up summarised as conversation, then injected into every later prompt. The summariser is now told not to record configuration talk, and a filter drops those lines if it does — without eating legitimate lines about this build, which the persona does discuss. |
| Requests that could wait forever | Every call to the local server was issued with no timeout at all: a wedged model server meant the interface never came back. Connect and read timeouts are now set throughout, and a stream that stops sending data ends with one readable line and keeps the partial reply. |
7.Performance engineering
The interesting one, and the thing most people building memory extensions get wrong.
Where you inject context decides whether the model can cache the prompt.
Putting changing blocks (memory matches, mood, summary) ahead of the character definition means the start of the prompt changes on every turn, so the engine re-evaluates the whole context for each reply — measured at roughly 16 seconds with a 64k window around 30% full.
Injecting those blocks into the newest user turn instead keeps the character definition and the conversation history a stable, cacheable prefix, so only the new text is evaluated. Same information, a fraction of the work — and it's why the extension defaults to that position.
- Summaries are generated in a background thread, so a long chat never stalls a live reply.
- Commands are answered without invoking the model at all (no tokens, no wait, works with thinking disabled).
- Embedding-based memory retrieval runs against a small local model served by llama.cpp — no Python ML stack loaded into the app process.
8.What came from upstream (not our work)
Worth stating on camera — it makes the comparison credible and gives credit where it's due.
- The application itself: the Gradio UI, chat/notebook/parameters/model/session tabs, Jinja2 prompt templates, character handling, streaming.
- All the loaders and backend switching, model metadata handling, and the portable packaging approach.
- The OpenAI/Anthropic-compatible API, tool calling (including MCP servers over HTTP and stdio), and the tool-parsing layer.
- Vision/multimodal input, text/PDF/docx attachments, gallery, character bias, autosave.
- LoRA training and evaluation, LaTeX rendering, syntax highlighting, themes, and the built-in docs.
- Web search plumbing, reasoning/thinking-channel handling, and the spec-decoding support the newer llama.cpp builds rely on.
- An image-generation tab (diffusers-based) — this build's image panel is a different engine wired into the same idea.
9.Portable setup, rollback & tooling
| Item | Detail |
|---|---|
| One-double-click launch | A launcher starts the local server and opens the desktop window. No Python environment to set up, no manual dependency install. |
| Everything self-contained | Models, the image-generation engine and checkpoints, the speech runtime, the embedding model and the extensions all live inside the install folder (~20 GB), so it can be moved or copied as-is. |
| Rollback paths | The install is version-controlled, the previous llama.cpp binaries are kept in a backup folder, and a clean unversioned copy of the build exists as a deployment snapshot. |
| Download vetting tool | A small script reads a GGUF's metadata before you keep it: size, quantisation, architecture, the configured context length, chat-template revision, token flags the engine would silently correct, KV-cache cost at that context, whether a vision projector is set — and it flags draft/MTP heads as not-a-chat-model. |
| Notes renderer | A script turns the plain-text notes into a styled, self-contained HTML page (this page follows the same house style). |
| Written record | A changelog documents every change with its reason and the files touched — so customisations can be re-applied after an upstream update. |
10.Recent work timeline
| Date | Work |
|---|---|
| 20 Sep | Model / KV-cache / performance study; cache-type trap identified; rules set for vetting future downloads; llama.cpp update assessed and deliberately deferred. |
| 22 Sep | Extension-framework restructuring (Extensions tab, core extensions, collapsed panels); loader resilience; chat-style crash fix; web-search triggers broadened; memory during tool turns fixed; reminder on/off switch; replaced the bundled picture extension with the local Vulkan one; persona rules for saving memory. |
| 23 Sep | llama.cpp updated to b11130 and the loader taught the new --load-mode surface; draft/MTP heads hidden from the Model dropdown; vetting tool updated; docs and fallback copy brought back in step. |
| 24 Sep | Memory reliability pass (summary validation, prompt rebuild, never inject a bad summary); summariser output contract enforced end to end; reply hygiene (scaffolding, echoed blocks, paragraph preservation); session diary rebuilt into a real entry; character files given the no-timestamp/no-self-label rule. |
| 26–27 Sep | Per-chunk display filtering (a dateline or label no longer flashes before it is cleaned); notices rewritten as plain sentences; /table added; the command reference sheet published as HTML and PDF; upstream's open pull-request list used as a bug list — four live bugs found and fixed, including the disappearing-reply one. |
| 28–29 Sep | The rolling summary stopped being thrown away (the model opened its thinking channel on every summary call, which the guard read as leaked markup); a cooldown added for a failed background job; three personas compared and the answer-shape block added after card rules proved unreliable. |
| 30 Sep – 2 Oct | Persona work: Wire Test reduced to a core and rebuilt block by block until the behaviour was reliable, then given the grounded traits it asked for; the persona renamed Wire Grounded with its memories, mood, diary and transcripts moved across. |
| 3–4 Oct | llama.cpp updated to b11377 and the build made visible in the Model tab; two draft-configuration traps turned into warnings instead of failed loads; the summariser stopped recording configuration talk; the clean fallback copy refreshed with every modified build file. |
11.Demo order for the video
A sequence that shows the strongest things first and never leaves the chat box. If you'd rather not open a live chat at all, the sample output page already contains the finished version of every one of these moments.
- Launch — double-click, window opens. Say the number: no install, no Python, no keys.
- Memory basics —
/remembera fact, then mention its keyword in normal conversation and show it surfacing. Follow with/memories. - Commands without a model —
/file write notes.txt | …then/file read notes.txt. Point out these return instantly and can't be hallucinated. - Summarise —
/sum20in a long chat: clean[Summary]+ five keywords, first person, no preamble. - Continuity state —
/mood,/style,/relation: state that persists across sessions. - Diary —
/diarythen/diary read: a dated entry written from the session itself. - Verbatim recall — ask what was said earlier and show the log lines with real timestamps.
- Voice — Speak last reply; change character; play again to show the voice follows. Mention the mic loop panel as next.
- Images —
/imagea scene; then ask the AI for a picture and show the tag replaced by the render. - Extensions tab — show the tidy collapsed panels and that core panels can't be disabled by accident.
- Model tab — show the draft head is absent from the model list but present in the draft list; point at the small
llama.cpp b11377line under the loader, and at the load line reporting its load mode, spec type and draft model. - Wrap — the honesty line: same app as upstream, newer engine, plus the memory/voice/image layer; credit upstream by name.
If asked "why not just use upstream?" — because the pieces people actually want for a chat companion (memory that persists, a summary command, verbatim recall, reminders, local voice, local image generation) are not in the core app, and the community ones each bring their own heavy dependencies. Here they're one install, offline, and they share state instead of fighting over the prompt.
12.Credits & licence
- Upstream project: oobabooga/textgen — the application this build is based on. Stars, docs and the community extensions directory all live there.
- Licence: AGPL-3.0, inherited from upstream. Modifications are distributed under the same licence.
- Third-party components used by the extensions: llama.cpp (and its embedding server), stable-diffusion.cpp for image generation, Kokoro-82M via kokoro-js for speech, Whisper for speech-to-text, and
bge-small-en-v1.5for local embeddings. Each keeps its own licence; none of them phone home at runtime. - No telemetry, no external requests in normal use — web search is opt-in per message and only fetches when asked.
Built as a personal project on top of upstream's work. Upstream feature claims here are based on the public v4.9 release notes and the project's own documentation; everything in the right-hand column is present in this install and was verified by running it.