Releases: suwonleee/llmwiki
Release list
v0.9.3 — filed where the work happened
One silent failure mode, closed: a session's record landing in the wrong wiki.
The problem
Capture buckets a session by its cwd — the right key, until the day it isn't. A session started from an enrolled home directory (or any ancestor repository) buckets home while every edit goes to another repo. The close-out guidance said "check cwd= on the extract header," but the bucket and the cwd agree by construction, so the one field being checked structurally cannot detect the mismatch. The result was silent misfiling: the record ends up in a wiki no future session of the right project will ever be shown.
Found the honest way — the day this engine's own release session ran from ~ and produced three releases in ~/llmwiki-runtime, and nothing in its extract said so.
The fix: a deterministic signal in the extract header
The extract now resolves every mutation tool call (Edit/Write/MultiEdit/NotebookEdit file paths) to its nearest git root and reports where the work actually landed:
# cwd=/home/you session=… users=12 assistants=34
# touched: /home/you/project-b(87) · /home/you(7)
# ⚠ route: 87 of this segment's file mutations happened under /home/you/project-b, not /home/you —
# file the session into THAT repo's wiki, then advance the watermark where the transcript is queued.
- reads are deliberately excluded — investigation roams across repos; where you changed files is where the record belongs
- the advisory appears only on a mismatch; same-repo sessions and chat-only sessions stay silent
/wiki-savenow defines its target as the repo the session's work concerned (normally cwd), with the cross-filing procedure;/wiki-deep's scope gate reads the new lines and chooses: file into the right repo you maintain / skip genuinely foreign work / keep chat-only sessions in their bucket- verified live on the session that exposed the problem: its home-bucketed extract now prints
touched: …llmwiki-runtime(87)and routes the filing correctly
Also in this release
benchgrew a passive-delivery report — cold-start and per-turn reach, silence rate, injected bytes: the instrumentation behind the wiki-vs-no-wiki A/B numbers- one practical line in the READMEs: start your agent from the project directory — the flag exists for the sessions that didn't
Upgrading: git pull && ./setup.sh (the update notice from v0.9.2 will have told you). Coming from v0.8.x, the enrollment step in v0.9.0 still applies.
Verified with 777 tests, tsc --noEmit, and a live run against the very session that motivated the feature.
v0.9.2 — write at close-out, know when to update
Two behavior changes, both about timing — when the wiki gets written, and when you learn a new version exists.
Wiki authoring moves to the session close-out
The injected operating rules told a session what to record but never when. Long work sessions therefore kept writing wiki pages mid-task: meta-work spending the context the actual task needed, producing records made before the session's whole picture existed. Rule 3 now opens with the timing contract:
- mid-session, the session does not create or edit wiki pages — capture already preserves the transcript, so deferring loses nothing
/wiki-savefiles the whole session at the end, including the decision from its first hour that was deliberately left unwritten at the time- the one exception: you explicitly ask to record something now (any
/wiki-*command counts)
This costs +317 bytes of cold-start rules per session, against thousands of tokens per mid-session page write. Verified behaviorally on live sessions: a decision-bearing prompt now answers "I'll file this at /wiki-save", and an explicit "record this now" still records.
The engine tells you when a newer version exists — and never updates itself
You no longer have to watch the Releases page. The daemon asks origin once a day — a git fetch plus an object read of origin/main's package.json; nothing is checked out and nothing runs — and your next session then starts with one line:
[llmwiki] engine update available: v0.9.1 → v0.9.2 — apply when you choose: cd ~/llmwiki && git pull && ./setup.sh (the engine never updates itself)
Applying stays your act, deliberately. Hooks execute this engine's code inside every session, so auto-applying whatever origin/main currently says would turn one compromised account into code execution on every consumer machine at their next session start. A clone is not consent (v0.9.0) — and pulling new code is a fresh act of consent.
Details that make the line trustworthy:
- it compares origin's recorded version against your clone's live
package.json, so it disappears the moment you pull — no daily lag - version strings from the remote are treated as untrusted input: only a plain
x.y.zsurvives, re-validated on every read; a tampered state file reads as nothing llmwiki doctorgrew an[update]section; an available update never fails doctor — being behind is information, not a defect- the check runs from the daemon only (session start does zero network and stays offline-safe), and the record is an owned state file removed by
./setup.sh --uninstall --purge-data
Upgrading: git pull && ./setup.sh — one last manual look at the Releases page; after this version the engine tells you. Coming from v0.8.x, the required enrollment step in v0.9.0 still applies.
Verified with 765 tests, tsc --noEmit, live-session behavioral probes for the authoring rule, and real-hook renders of the update notice in both rule languages.
v0.9.1 — the first five minutes
Two lines an adopter reads in the first five minutes, both of which said something untrue. Found by installing the published clone in a clean room — isolated HOME, inert supervisor shims — and following the README as written.
doctor denied an enrollment that had just succeeded
The enrollment inventory is drawn only from repositories the capture queue has already seen; doctor never scans your machine, by design. But the queue file exists from the first sweep onward, including sweeps that captured nothing — so a fresh install answered ✅ enrolled repositories: 0 seconds after init printed automatic integration enabled for this worktree. In the clean room: init succeeded, cold start injected 2,833 bytes into the session, and doctor still said 0.
It now reports what it actually knows — no repository captured yet — and names the command that can answer immediately, llmwiki status <repo>.
The README told Claude-only users to run a command they do not have
The llmwiki launcher is installed by the Codex and OpenCode wiring. All four READMEs nonetheless gave llmwiki init /absolute/path as the one activation step, so a Claude-only install meets command not found at exactly that point. Each README now also gives the direct form:
bun ~/llmwiki/src/cli.ts init /absolute/path/to/my-project # status and disable likewise(The agent contract in setup_text.md already handled this correctly — only the human-facing docs were wrong.)
Upgrading: git pull && ./setup.sh. If you are coming from v0.8.x, the required enrollment step in v0.9.0 still applies.
Verified with 754 tests, tsc --noEmit, and a clean-room install of the published clone: clone → setup → enrollment gate → init → cold-start and per-turn injection → capture sweep → uninstall.
v0.9.0 — a clone is not consent
Two things changed here: when llmwiki runs at all, and whether capture told the truth about what it captured. The first is a required upgrade step — see the bottom.
A clone was enough to make llmwiki read a repository
"Is this repo mine?" used to be answered by the presence of docs/wiki/ — a directory that arrives with any git clone. So cloning a stranger's project was enough to make the installed hooks read its Markdown into your model's context and queue that session's transcript. Nobody made that decision; it came in the tarball.
Activation is now one explicit step per repository:
llmwiki init /path/to/my-project # scaffolds the wiki, records the marker
llmwiki status /path/to/my-project # is it on, and why not
llmwiki disable /path/to/my-project # off again; your wiki files are untouchedThe marker lives in that worktree's .git/llmwiki/, where a clone cannot deliver it and no commit can carry it. It is per-worktree, so trusting one linked worktree never trusts its siblings, and it records the path it was written for — a copied or moved repository fails closed rather than following you. Until init runs, a repository produces exactly zero bytes of cold-start context, no per-turn injection, and no captured sessions. After it runs, there are no further prompts, ever.
99% of Claude sessions never reached the wiki
The router identified a transcript record by reading its type field first. Real records often carry their content before their type, and those were dropped — silently, since the daemon reported healthy and the queue kept filling. The loop looked alive while almost nothing reached the wiki. Routing now reads the whole record, and the shapes that broke it are pinned by fixtures derived from real transcripts (keys, nesting and types preserved, every value replaced).
Codex threw hook output away without saying so
Plain-text hook output was accepted by the hook and discarded by the harness — no error on either side. Both ends now agree on a JSON envelope. OpenCode and Claude were unaffected but share the same path.
Capture could die quietly
llmwiki doctorgrew a[capture]section: enrolled repositories, queue health, and the repositories that hold a wiki but are not enrolled- when the harness states the session id, that now wins over inferring it from a filename
- an older llmwiki's leftover logs could make the engine refuse its own state directory
- capture buckets normalise to the same wiki root that reads use, NFC/NFD filename splits included
- hooks bind to the repository the session is actually working in, ending a read-here/write-there split
- the daemon re-installs read hooks that a live session removed
Retrieval narrowed the further you got from English
Two thresholds were counted in characters, which is a different bar in every language — the same sentence is far fewer characters in Korean or Japanese than in English, so non-Latin sessions quietly fell under the floor. On one corpus a Korean session's recall went from 1 of 7 prompts to 5 of 7.
Install and retention now report what happened, not what was attempted
daemon/install.sh verifies the service with launchctl list instead of assuming the load worked. Retention warnings count only sessions that can still be saved, the backlog shows the time left before the harness deletes a transcript, and a ledger records what expired. Synthesis lines — compact summaries, sidechains — no longer enter the pool of "things the human said".
Uninstall
./setup.sh --uninstall # every hook, plugin, command, launcher and service llmwiki installed
./setup.sh --uninstall --purge-data # …and its local runtime state as wellRemoval is by ownership, not by restoring a backup: entries llmwiki installed are removed and everything else in your harness configuration is left exactly as it is. Run it from the installed clone before you move or delete that clone. Your wikis are never touched — docs/wiki/ is ordinary Markdown in your own repositories.
Upgrading — one required step. Enrollment is new, and no repository is grandfathered in: after
git pull && ./setup.shevery project you were already using goes inert until you turn it on once:
llmwiki init /path/to/that-projectllmwiki doctor lists exactly which ones need it ("N repositor(ies) hold a wiki but are not enrolled"), and setup.sh runs doctor for you at the end. Nothing under docs/wiki/ is touched either way.
Verified with 752 tests, tsc --noEmit, and a clean-clone consumer run (clone → bun install → bun test → setup.sh --dry-run).
v0.8.2 — every script priced at its real token rate
v0.8.0 moved CJK onto a real token rate. It left every other non-Latin script on the English one, which is the same defect with a smaller multiplier — a Russian, Greek, Arabic, Hebrew or Hindi chunk still ran about twice its intended size, and a Thai chunk three to four times.
Chunk size is a budget in tokens, so mispricing a script means its readers pay that multiple in context on every retrieved hit — permanently, for writing in their own language.
Same sentence, estimated tokens
| characters | before | after | |
|---|---|---|---|
| English | 50 | 12 | 12 |
| Korean | 29 | 25 | 25 |
| Japanese | 31 | 31 | 31 |
| Russian | 55 | 13 | 26 |
| Thai | 56 | 14 | 56 |
| Hindi | 49 | 12 | 22 |
| Arabic | 47 | 11 | 21 |
| Greek | 46 | 11 | 21 |
Three rates
- dense and unspaced (Han, kana, Hangul, Thai, Lao, Khmer, Myanmar) — about one token per character
- other non-ASCII (Cyrillic, Greek, Arabic, Hebrew, Indic, accented Latin) — about two characters per token
- ASCII — four characters per token
Hardcoded code-point ranges are gone in favour of Script_Extensions, so a character like the prolonged-sound mark ー (which is Script=Common) is no longer missed.
Upgrading: chunk boundaries changed, so an existing index invalidates and rebuilds itself on first open. Nothing under docs/wiki is touched. Measured on a 245-page wiki: 0.70s.
590 tests, tsc --noEmit clean.
v0.8.1 — per-turn injection works in every writing system
v0.8.0 fixed search, dates, links and chunking — but its verification was CJK-and-Latin shaped, and it never checked the per-turn injection path. Measured properly, that path worked in three of nine languages.
What was broken
Injection runs off terms pulled out of your prompt, and the two patterns doing the pulling recognised ASCII and CJK only.
- Cyrillic, Thai, Devanagari, Arabic, Greek, Hebrew produced no terms at all. Not degraded — the feature never fired once.
- Japanese and Chinese failed the other way: with no spaces to break on, an entire clause became one term, and an entire clause is a literal substring no page contains.
- Vietnamese produced one term: the ASCII pattern cannot start on a letter with a diacritic, so nearly every syllable was dropped. The same fragmentation quietly cost German, French, Spanish, Portuguese, Turkish and Polish their accented words (
überhauptarrived asberhaupt).
Measured, before and after
Fair fixtures — the page and the prompt share the same content words in each language:
| en | ko | ja | zh | ru | th | hi | ar | vi | |
|---|---|---|---|---|---|---|---|---|---|
| before | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| after | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
All eight unrelated control prompts stay silent, before and after — recall improved without spending precision.
How
- a word pattern for every script, with
\p{M}keeping combining marks attached - runs in scripts written without spaces (Han, Kana, Thai, Lao, Khmer, Myanmar) sliced into word-sized windows.
Script_Extensions, notScript— the prolonged-sound markーendingマイグレーションisScript=Commonand split the run down the middle - floors kept honest: 4 characters for Latin, 3 for a dense script, so the
the/and/forclass still stays out without a stopword list
588 tests, tsc --noEmit clean. No index migration — this release changes only how a prompt is read.
v0.8.0 — international usability
Five things that made llmwiki quietly worse for anyone outside an English, UTC, single-machine setup. All five failed silently — no error, no warning, just a slightly worse wiki.
A query shorter than three characters came back empty
The FTS index uses SQLite's trigram tokenizer, which cannot match a term below three characters — and the term was still quoted into the implicit AND, so one short word emptied the whole query. That is most ordinary CJK words: 언어 · 세션 · 보안 · 言語 · 语言 · 設定. Short terms are now dropped from the MATCH, and a bounded substring scan answers when MATCH finds nothing.
Dates followed UTC, not the reader's calendar
A page filed at 08:00 in Seoul was dated yesterday. In New Zealand that covered most of a working day; in California every evening was dated tomorrow. Calendar dates now come from the reader's own calendar; machine timestamps stay UTC.
CJK wiki links broke across checkouts
The same Korean, Japanese or Chinese filename exists as NFC or NFD depending on which tool wrote it — Finder, unzip and iCloud/Dropbox decompose; keyboards and git compose. A Mac and a Linux checkout of one wiki therefore disagreed on the name, the link produced no edge, and the page read as an orphan. Link keys are now normalized.
A [[wikilink]] pointing at nothing was never reported
The dangling-link rule only inspected markdown [text](path) links, so the wiki's primary linking idiom was the unchecked one. Now reported as a warning, not an error — linking forward to a page you have yet to write is normal use and must not block a close-out.
Token estimation was English-only
Chunk size was set by characters ÷ 4. English does run about four characters per token; CJK runs about one — so CJK chunks were three to four times larger in real tokens, and every retrieved hit cost several times more context. Each script is now counted at its own rate.
Upgrading: chunk boundaries changed, so an existing index invalidates and rebuilds itself on first open. The index is derived state — nothing under docs/wiki is touched. Measured on a 245-page wiki: 0.67s.
Verified with 581 tests, tsc --noEmit, and a ko/ja/zh/en smoke run.
v0.7.3 — the language check, measured and trimmed
Patch. Someone asked whether the "unset means your session's language" chain was too slow, so it got measured instead of defended.
Measured (no config, no env — when detection actually runs)
| Path | Before | After |
|---|---|---|
| wiki pages: 10 / 100 / 500 / 2000 | 0.40 / 0.63 / 1.77 / 7.4 ms | unchanged |
| transcripts: 0.2 / 2 / 8 MB (only when no page exists yet) | 2 / 16 / 55 ms | 2.0 / 2.4 / 5.1 ms |
For scale, llmwiki context takes about 90 ms end to end and overview --normalize about 30 ms, most of it process startup — the language check is 0.5–2 ms of that.
Changes
- A transcript is read from its last 256 KB. Identifying a language never needed a whole session, and the tail is the most recent part anyway. A truncated first line simply fails to parse and is skipped, which the extractor already did.
- The answer is memoized per repository, per process. One close-out normalizes the overview, refreshes the gap queue and appends the log; there is no reason to re-scan for each.
- Cold-start and the per-turn header now use the same answer as the writers. They consulted only the config before, so a Korean wiki with no config got Korean pages and English operating rules.
What a user sees
One rule: set nothing and it follows your language. Of the five resolution steps, three (environment, config, cache) are constant time, and the two that touch disk are bounded and cached.
Update
git pull && ./setup.sh548 tests passing via bun test, tsc --noEmit clean.
v0.7.2 — the language sample is really the recent pages
Patch on v0.7.1's session-language default.
Fixes
- Detection now samples the pages it claimed to. The code stored a placeholder in place of each page's modification time and then took a tail slice, so the "newest twelve pages" were really "twelve in directory order". A wiki that ran in English for a long time and recently switched to Korean stayed English, outvoted by its own history. It now sorts by real mtime — and sampling the newest pages keeps the check cheap on a large wiki.
How it decides, stated plainly
- One vote per document, the most-used language wins. Inside a document the script counts decide, so "the language used most, ignoring code" holds at both levels.
- Order is unchanged:
LLMWIKI_LANG→ an explicit[wiki] lang→ the wiki's recent content pages → your own utterances in this repo's captured transcripts → English. - Your pages are unaffected as always: page prose follows your conversation.
Update
git pull && ./setup.sh548 tests passing via bun test, tsc --noEmit clean. Verified on a mixed wiki (two Korean pages, one English → Korean) and on a switched wiki (fourteen old English pages, six recent Korean → Korean).
v0.7.1 — leaving the language unset now means your session's language
Follow-up to v0.7.0. Setting a language is now the exception, not the requirement.
Fixes
-
An unset
[wiki] langfollows your session instead of defaulting to English. Measured on a wiki written entirely in Korean, with no config and no environment variable: the engine wrote an English pointer line into it. Nobody who configured nothing asked for that.The engine is a CLI and cannot read your conversation, so it reads what the session left behind, in order:
- the wiki's own content pages — written by earlier sessions in their own language. Reading these first also makes the answer sticky: a wiki never flips language between runs, which matters when one teammate types Korean and another English.
- your own utterances in this repo's captured transcripts — all a brand-new wiki has to go on.
- English, as the last resort rather than the default.
Detection reads prose and never code: frontmatter, fenced and inline code, URLs, link targets, footnote definitions and path- or identifier-shaped ASCII are stripped first, because pages keep code terms verbatim by contract. A Korean sentence full of English API names is still Korean.
Files the engine itself wrote — the skeleton, the log, the ledgers, the cold index — never vote, so an English skeleton seeded on day one cannot lock a Korean team into English forever.
-
Priority is unchanged and explicit still wins:
LLMWIKI_LANG→[wiki] lang→ detected → English.lang = "auto"is accepted as "follow the session", and pinning a code is still the right move on a team whose members type different languages.
Unchanged
Your pages were never affected by this setting and still are not: page prose follows the language of your conversation, and nothing is translated for you.
Update
git pull && ./setup.sh547 tests passing via bun test (8 new), tsc --noEmit clean. Verified end to end: a Korean wiki gets the Korean pointer, an English wiki stays English, a fresh repo with only Korean utterances seeds a Korean skeleton, and a repo with no evidence stays English.