Release pace. 1.1.x was held to fixes from 2026-10-15; 1.2.0 shipped on 2026-10-19, ahead of the planned 2026-11-12, at the maintainer’s call; 1.3.0 on 2026-11-09. The early weeks shipped many versions a day; that pace was development, not a support promise.
Minor releases (new features) at most once a week; patch releases (fixes, docs, benchmark updates) when ready. 1.0.0 to
1.0.2 went out on one day: 1.0.0 at the maintainer’s call, 1.0.1 and 1.0.2 as fixes from outside reviews. From 1.0.0 the version follows
semantic versioning; what counts as the stable surface is listed in docs/DETAILS.md. Each entry starts with what you will
notice, then the details.
Nothing yet.
What you will notice
90 系统文件/自动化/记忆变更记录.md; in a vault that is a git
repository each run is also one commit, and import_sessions.py --undo <run> takes it back.
KROUTER_INJECT=confirmed keeps the 1.2 behaviour.extras/host-daily-evolution/intraday_capture.py).#nocapture / #不记 in a question, or a whole folder with --exclude-root.install.sh) now records where the vault is, so the hooks work there too.Details
1 : 3.3, 6230 / N70 / N73, 1.2MB / 498) and a trailing _ (session_item_) no longer fail a verbatim
answer: on the author’s vault 6 of the 8 cards the checkup listed as unfaithful were this; the other 2 rewrote a
date (08-16 → 8/16) and are still refused.type: daily-log page already lists, or a
day its collector covered, is linked to that log (covered_by) or, with --overlap skip, not written. A fork or
resume that copied another session’s turns no longer writes them twice. Session-end distilling sends only the
turns added since the last digest, waits for a running import instead of sending the same session twice, and uses
the lane named at consent. docs/AUTO_CAPTURE.md.bench/decision_capture/): 14 fixed sessions, recall and false-extraction rate scored
by rule, no judge model.import_sessions.py --intraday
imports and distills today and yesterday with the lane recorded at consent, and does nothing before it;
extras/host-daily-evolution/intraday_capture.py runs it at fixed wall-clock slots (default 12:10 and 18:10 in
Asia/Shanghai) from an hourly timer, catches up the latest missed slot after sleep and tries a failed slot once
more, then leaves it to the nightly job. session_hook.py prompt (UserPromptSubmit) writes a typed correction
(“改成”, “以后一律”, “锁死”, “from now on”) to the day’s 纠正候选 page as a provisional candidate: local pattern
match, redacted, no model call; rhetorical questions (“不是吗”, “对不对”) and KRouter’s own model calls
(KROUTER_IN_DISTILL) are not captured. A session with #不记 (or #nocapture) in a question, or under a root
on --exclude-root, is neither imported nor distilled, and a page already written for it is removed.KROUTER_INJECT=confirmed keeps the old
gate). Correction candidates are settled by the next distill of their session (成立 with the matching line, or
不成立). Each run that changes something makes one git commit of exactly its files ([krouter-run <id>]) and one
entry in 90 系统文件/自动化/记忆变更记录.md (added lines, replaced decisions, source session, verdicts);
import_sessions.py --undo <id> reverts the run and keeps its record.install.sh records the vault path for hooks and the MCP server. They do not inherit OBSIDIAN_VAULT from
the host app; bootstrap already wrote ~/.config/krouter/vault, install.sh did not, so after a manual install
the correction hook and the session-end import found no vault and did nothing, silently. An existing record is
left alone.docs/DENSE-OPTIN.md) are merged; nothing
in the product changed.backup.py init --dir <folder> turns on a nightly, encrypted archive of the whole vault
(its .git included, editor state and caches left out) in a folder that syncs elsewhere (iCloud Drive, OneDrive,
Dropbox, a Netdisk sync folder, a USB disk; several, separated by |). AES-256 via openssl with an HMAC; each
archive is restored into a temporary folder (page count, git fsck) before it counts; the newest 7, plus one per
week for 4 weeks and one per month for 6 months, are kept. The passphrase lives in the keys file, outside the vault;
status asks the agent to tell you while it is not saved anywhere else, or when no backup has restored cleanly for
three days. KRouter talks to no cloud. Restoring needs only openssl and tar. DETAILS → Backing up your vault.
Empty folders and symbolic links come back as they were; what a link points to outside the vault is not in the
archive, and each run says how many such links there are (links_outside).What you will notice
claude nightly writer can no longer read files outside the vault. Up to 1.2.0 its Grep and
Glob were allowed with no path, so it could print lines from any file in the account, the keys file included. Only
a nightly job on the claude lane (installed by install.sh, --ask or --timer) was affected; upgrade by
running the install line again.Details
install.sh or --ask /
--timer on the claude lane; the API-key, grok and Codex lanes, and the model steps (no tools at all), are not
affected. Found by an outside review.first_run.sh ends by pointing at bootstrap.py --dry-run and the lookup-only install, not at install.sh (which
installs the nightly job).What you will notice
bootstrap.py --ask turns remembering on. A logged-in Codex, which can read the whole account, is no
longer picked as the nightly writer unless you name it.bench/memory_honesty/HELDOUT.md against 1.0.3.Details
bootstrap.py --ask puts the two questions it used to put at install (import recent
sessions? install the nightly job?), with the same preview of what is sent; --import, --import-distill and
--timer still say yes in advance. An upgrade where you said yes before (session import consent, or a nightly job
on disk) keeps it and is not told it is off.KROUTER_WRITER, KROUTER_LEARN_CLI) or set
KROUTER_ALLOW_CODEX=1; otherwise a logged-in grok or claude, both limited to the vault, is used instead, and a
Codex-only machine is told why distilling was skipped (codex:reads-account).Porsche, Seattle, iPad) that no page
contains refuses the question; sentence-initial capitals, two-letter words such as OK, and month and day names do
not count. Tuned on LongMemEval S’s 30 unanswerable questions (now a development set: refused 3 → 9, answerable
questions refused 12 → 14 of 470) and dev3 (one more neighbour refused, nothing lost). heldout3, run once: wrong
citations 26.6% → 26.1%, neighbours refused 46 → 47 of 80, answerable right 102 of 120 unchanged.low, the receipt carries a recall_note: line (only one word matched; say the vault may not cover the question),
and no generated answer is attached. Alias hints no longer come from scattered characters or question words
(“剪辑” was offered “剪藏原件”; “标题有什么讲究” was offered a page on 什么): a hint needs a shared two-character word.tests/test_stale_injection.py.docs/img/demo-content.gif, the same story as demo.gif for short-video work, recorded the same way; plugin,
skill and package text call Obsidian optional.bench/longmemeval/lme.py), 8 (answers over the
vault’s own git history) and 9 (written by people: corrections and their neighbours) registered before any run; set
7 run and amended (S’s unanswerable questions became a development set).What you will notice
90 系统文件/自动化/下意识触发词-自学习.psv if your recall got worse.KROUTER_ALLOW_UNCONFINED=1 in ~/.dsh-krouter-keys.env to keep using it. grok, Codex, claude and API keys are
unchanged.Details
package.json, plugin.json, extras/dsh/package.json,
the newest CHANGELOG heading and the status block do not name the same version, or when changes are still listed
under “Unreleased”. The status block now says it describes main and names the latest release.docs/STATUS.md, docs/STATUS.zh.md),
copied by scripts/sync_status.py; a test fails when a copy drifts.KROUTER_ALLOW_UNCONFINED=1 (environment or
keys file). A pinned KROUTER_WRITER or KROUTER_LEARN_CLI follows the same rule; the writer and host_action say
why one was skipped.03 资料与证据 (clipped, pasted text) are never candidates. The learner now sends,
per unanswered question, up to 4 candidate pages’ first 600 characters, redacted (SECURITY.md). learn_bench.py
measures this.bench/memory_honesty/human_set/ (instructions, template, checker) for a held-out set written by a
person, each thing asked twice in different words; learn_bench.py (“learns from use”: week 1 through the router
and one night of learning, week 2 scored before and after).PROTOCOL.md, first_run.sh, install.sh and the nightly writer’s
files said “timer on by default”; the one-line install asks first, and only the manual install.sh installs it.extras/hooks/exec-gate.py is described as an accident guard, not a security boundary.What you will notice
Details
~/.cursor/projects/*/agent-transcripts/) become
session pages like Claude Code and Codex ones: your questions (the <user_query> part), each turn’s closing answer,
redacted, on the day and minute Cursor stamped on the question. The project folder is recovered from Cursor’s
folder name by checking which path exists. KROUTER_CURSOR_PROJECTS points at another root. Closes #26.What you will notice
docs/FOR-AGENTS.md
gives an agent asked to recommend it the table, the limits and the risks in one place.gh attestation verify dsh-krouter-1.0.4.tgz --repo 398894496-arch/KRouter. SECURITY.md explains each flag a
static scanner raises.Details
--dry-run,
then install with the nightly job off), who it is and is not for, and a one-second way to rerun the numbers.
docs/FOR-AGENTS.md carries the third-bench table and the risks in one place; SECURITY.md maps each flag a static
scanner raises to its file.gh attestation verify), so a package can be tied to
the tagged source and the workflow that built it, not only to a checksum in the same release.floor.py and both adapters now run on all three vaults; the container runs offline.heldout3.jsonl.gz: 200 questions (80 neighbours, half
Chinese) on vault3/, 110 pages including 20 daily logs as distractors, with no dev set on that vault. run.py
gains --ci (bootstrap intervals), --vs (paired intervals), --vs-matched (equal coverage) and --curve.
Baseline: KRouter 1.0.3 is ahead of BM25 with the two rules at equal coverage on every measure, significant on
none. The decision rule for “surpasses BM25” and for a new default is in HELDOUT.md, written before any candidate.docs/BENCHMARK.md, “Tried and not shipped”). A
second dev set, questions_dev2.jsonl (50 questions, written by the maintainer), is added for tuning.vault2/ (60 pages, half Chinese), 50 dev
questions (questions_dev3.jsonl) and 50 held-out (heldout2.jsonl.gz, gzipped so it stays out of greps), each
with 20 neighbour questions whose missing term CI checks is on no page. First run, before any change: KRouter 33%
wrong citations on heldout2 (9 / 20 neighbours refused, 24 / 30 answerable right); BM25 with the two rules 23%
(15 / 20, 17 / 30); plain BM25 61%. run.py --set dev3|heldout2; the vault2 BM25 baselines search Chinese by
character pairs.What you will notice
docs/BENCHMARK.md now say so plainly: KRouter’s advantage on this bench is
those two rules on by default, plus receipts, not a better ranker. Reproduced, not taken on trust; KRouter was
not changed in response, because the held-out set is not a tuning target.Details
run.py --adapter bm25-floor; bench/memory_honesty/floor.py applies “skip superseded + dev-picked floor” to any
adapter output that carries scores; the basic-memory adapter now records scores.bench/memory_honesty/adapters/Dockerfile): basic-memory was re-run
there, with identical answers to the host run. A container does not share the host’s keychain or launchd.--reveal; no KRouter
change).What you will notice
docs/BENCHMARK.md and bench/memory_honesty/results/.change (only the reversal of an earlier decision came out as decision), so
the decisions page now takes decision, change, feature, bugfix and refactor. In the live run, the model
pass sorted them correctly: “any branch, manual upload → tags on main, CI publishes” was marked as replaced, and the
session-start hook brought exactly that correction into a new session in the same repository and nothing into the
other one. The decisions page’s how line is now a summary of the page; the line matching the question comes from
recall.bench/memory_honesty/heldout.jsonl: 9 corrections, 6 current, 10 neighbour questions on the same vault, written
by the outside reviewer and never used for tuning. run.py --set heldout prints aggregates only; --reveal prints
the misses and, followed by a ranking change, burns the set. Rules, the baseline at freeze and a log of who has
seen what: bench/memory_honesty/HELDOUT.md.Details
bench/memory_honesty/heldout.jsonl, 25 questions; run.py
--set heldout prints aggregates only, --reveal shows misses and burns the set; rules and log in HELDOUT.md.tests/test_memory_honesty_heldout.py pins a SHA-256 of the questions and of the bench vault (line endings
normalized), checks the set does not overlap the development questions, and checks only a sanity guard: fewer
wrong citations and at least as many refusals as plain full-text search. No floor on the held-out score.bench/memory_honesty/adapters/); tests/test_memory_honesty_competitors.py
re-grades the stored raw output and re-runs KRouter and BM25 against the published dev rows.Fixes from an outside review of 1.0.0.
What you will notice
beta-corp/api was given alpha/api’s decisions. Reproduced, then fixed. A repository is now its git top level (or its directory, outside git). The path is written into each imported page (project_root:), and only pages for this exact repository are brought in. claude-mem’s memory is split the same way, using the cwd it records. Pages imported by 1.0.0 have no path and are no longer brought in; re-run import_sessions.py --days 30 to backfill (existing digests are kept).status: active or confirmed: true), and from unconfirmed pages only the “A → B” corrections, marked unconfirmed. KROUTER_INJECT=all restores the previous behaviour. The text is in English unless the decisions are in Chinese.docs/BENCHMARK.md now lead with this, not with the 0% on the bench’s own questions, which is a development-set number.claude plugin update krouter@krouter, or Enable auto-update in /plugin → Marketplaces).1.0 means the surface agents and vaults depend on is now stable: the vault layout, the alias table, the receipt
fields, the routes, the MCP tools, the KROUTER_* variables and the importers’ page markers change only in a major
release, with a migration. Scoring may still be retuned in minor releases, always with before / after numbers on
the public benches. Nothing breaks from 0.13.0: same vault, same table, same receipts; re-run the install line to
upgrade.
What you will notice
status: candidate). With distilling
on, your model sorts each project’s decisions into the ones still in force and the ones a later decision replaced
(“原来 A → 改成 B”). In a real run it caught “manual tags from any branch → tags on main, CI publishes”. claude-mem
is opened read-only and never changed.KROUTER_INJECT=0 turns the second off. Tested end to end with Claude Code: a new
session answered “what did we decide about releases” with no tool call and named the page, and the session was in
the vault when it ended.Details
import_claude_mem.py reads both column generations of claude-mem’s schema (memory_session_id and the older
sdk_session_id; epoch milliseconds or seconds), as of claude-mem b3e036fa13. The tests build both from its
schema; it has not been run against a live claude-mem install. Pages carry source: krouter-claude-mem, and a page
without that marker is never touched.session_hook.py start|end, wired by hooks/hooks.json at the plugin root; import_sessions.py --file imports one
transcript.nightly.py gains import_claude_mem. It stays off until consent, and is also off when claude-mem is not
installed.What you will notice
krouter_ask, krouter_status) with each CLI it finds and records the vault path, so no one has to export
OBSIDIAN_VAULT first. Claude Code can also install it as a plugin:
/plugin install krouter --marketplace 398894496-arch/KRouter.docs/DETAILS.md; facts
for an agent deciding whether to recommend it moved to docs/FOR-AGENTS.md, phrased as
facts rather than instructions.docs/BENCHMARK.md): wrong-citation rate, refusals on
neighbour questions, corrections followed. Plain full-text search that always answers: 63% wrong citations.
KRouter 0.12.1, first run: 22%. 0.13.0: 0%, all 10 neighbour questions refused, 8 of 10 corrections followed
(a development-set number: the fix below was found on this bench).Details
import_sessions.py: Claude Code ~/.claude/projects, Codex ~/.codex/sessions; one page per session and day
under 05 时间日志/会话摘录/, named after the session title. Headless runs (claude -p, codex exec, the nightly
job’s own calls), tool output, thinking, injected context and subagents are left out. A page it did not write is
never touched. A session that grows is re-rendered, keeping its digest until the session changes. --dry-run,
--consent and --revoke are supported, and the nightly job imports yesterday only after --consent. Imported
pages rank as logs: clues, not rules.nightly.py gains import_sessions as its first step. It is off until the host consents, and off is not
reported as a problem.mcp_server.py: MCP over stdio, standard library only. krouter_home.py: the router, the shell router and the
MCP server fall back to ~/.config/krouter/vault when OBSIDIAN_VAULT is unset.what, which, does, should and the
like were in no stop list, so in a small vault they counted as words the vault had never seen. That refused real
questions and let neighbour questions through on what and do. They are now dropped from ranking and refusal,
and plural and singular forms match each other (services / service). The author’s Chinese vault scores the same
before and after.~/.dsh-krouter-keys.env from its own results, dropping the claude long-lived token, pending keys, keys that
failed that day’s probe, and comments. It now updates only the names it manages. Found while testing this release
on the author’s machine: the token was restored and nothing was sent.--dangerously-skip-permissions and that the
Windows task was never run on a real host. Both stopped being true in 0.12.claude stops getting logged out. A logged-in claude keeps a short-lived token and a single-use refresh token in the keychain. When another program holds the same login — on the author’s machine a CLIProxyAPI / EasyCLI copy that refreshes every ~8 hours — whichever refreshes first spends the token and the other is logged out: the CLI went to loggedIn: false half an hour after the proxy’s refresh, while the desktop app (a separate login) kept working. Concurrent claude processes refreshing at once race the same way. Now every KRouter call to claude — nightly steps, the writer, the login probe — uses CLAUDE_CODE_OAUTH_TOKEN from ~/.dsh-krouter-keys.env when present (claude setup-token issues one that lasts a year and needs no refresh), and runs one claude at a time (file lock).VAR= prefix does not match the allow-list), and the allow-list accepts sh / bash / /bin/sh forms.Renamed: DSH-KRouter → KRouter. It was never specific to DeepSeek Harness. Old links, the old clone URL, the old
one-line install and old release downloads all redirect (checked after the rename). The npm / DSH plugin package
keeps its name (dsh-krouter), and so do the skill directory, KROUTER_* variables and the timer labels, so
existing installs are untouched.
The nightly writer is confined (seventh outside review). The review said the claude and grok writers, run with
permission prompts skipped and no sandbox, could read and write the whole account — keys file, ~/.ssh — not just
the vault, and that the docs claimed otherwise. Tested: under the old flags grok read a file outside the vault
through both its file tool and its shell. Now:
<vault>/.grok/sandbox.toml: grok’s
strict (read the working directory and system paths, write the working directory) plus the router read-only and
KRouter’s cache and state writable. Tested: outside file denied through both tools, vault write works, the real
router answers. The runtime prompt now lives in the vault (.krouter/, deleted after the run) because the
sandbox cannot read outside it.--permission-mode dontAsk and tools allow-listed: read and edit inside the vault, run the
router, nothing else (no other shell command, no web fetch). This is a tool-level rule, not a kernel sandbox; it
could not be tested end to end this round (the CLI’s login had expired) and is covered by an argv test.workspace-write sandbox (writes the vault only) and is now described truthfully: it can read
the account. Other CLIs have no confinement KRouter can set, and the install prompt says so.SECURITY.md has a lane-by-lane table.Also from the review
/Users/…/2026Q3Report2026Final.csv, https://…/Install2026Guide, s3://…). Keys in
query parameters and known key shapes inside URLs are still masked.KROUTER_ALLOW_UNVERIFIED=1. SECURITY.md says
what a same-release checksum does and does not prove.bootstrap.ps1 ended with exit $LASTEXITCODE; under irm … | iex that exits the user’s own session, so the window closed before the last line — the one saying what is left to do — could be read. Found by running the published one-liner on a real Windows host after 0.11.2. It now returns instead; a test keeps both PowerShell scripts ASCII-only and free of exit.What a stranger’s agent checks before recommending it, plus the sixth outside review.
Consent and integrity of the one-line install (sixth review)
claude / grok with permission prompts skipped inside the vault; Codex in a workspace-write sandbox; or a direct API call), and how many pages and tokens it will send — from a dry run on the new vault. Only y installs it; no terminal to ask on means not installed; --timer / --no-timer decide up front. install.sh (for terminal users) still installs it; KROUTER_NO_TIMER=1 skips.SHA256SUMS (new release-assets workflow); bootstrap.sh / bootstrap.ps1 verify the checksum before unpacking and stop on a mismatch. KROUTER_TAG pins a release (now in the README).NAME: value, - NAME=…, DeepSeek 密钥:sk-…, a bare key — not only NAME=value. The provider comes from the name, the key’s prefix or a provider word; when none tells, the key waits as KROUTER_PENDING_KEY_n, the page says how to name it, and the notification says that instead of “no model”.sha512-…) unless a key label precedes them. Trade-off, stated in SECURITY.md: an unlabeled bare-hex key is left to the secret-page layer.Windows, now in CI
\, which broke tiering (rule pages ranked like any page: English bench 1/12 on Windows), and CRLF line endings hid the frontmatter, so status: superseded was ignored. Both fixed; Windows passes the suite (POSIX-only tests skip).For a stranger’s agent
SECURITY.md (data flows, what a CLI writer can see, known filter limits, private reporting), CODE_OF_CONDUCT.md, issue and PR templates.dsh plugin --profile web add https://github.com/…/releases/download/vX/dsh-krouter-X.tgz); the package now includes everything install.sh and the one-line install use.verified_at, status, or the nightly / nightly_at fields nightly.py writes onto the health page — staled every card on that page, and the nightly run paid to rewrite them only to stale them again minutes later. Found on the author’s machine after 0.11.0: all five health-page cards stale, 日更每天几点跑 ranked by a different page. New cards are keyed to the page body; cards written earlier (whole-page hash) stay valid until the page changes, so nothing is regenerated in bulk.90 系统文件/自动化/日更健康.md) gets no cards: the system rewrites its body every day (seal dates), so a card on it could never stay valid; the rule pages answer its questions.For anyone, with nothing to know and nothing to maintain. Plus the fifth outside review’s two secret findings.
One line to install
curl -fsSL …/scripts/bootstrap.sh | sh (macOS / Linux) and irm …/scripts/bootstrap.ps1 | iex (Windows) fetch the latest release, make a vault in Documents/第二大脑 (Chinese template when the system language is Chinese) or Documents/Second Brain, install the router, the agent rules and the daily timer, and open the vault in Obsidian. An existing vault only gains missing skeleton files; nothing is overwritten; re-running upgrades. Only Python 3 is required (the Windows script installs it with winget when missing). The last line says the one step left, if any, in plain words. Run end to end on a real Windows 11 host.template-zh/: the template in Chinese, with its own sample alias table.Chinese recall on a fresh (small) vault
lexicon_zh2.txt) decides what is a word. Two or more real words the vault never contains, outnumbering those on the top page, mean the question is about something else (纠错之后的房贷利率是多少); below 300 pages only real or titled words count as matches (天会 no longer does). Chinese template bench: half-off-topic refused 1/6 → 5/6, off-topic 5/6 → 6/6, gold page 9/12 unchanged — and unchanged with the alias table emptied. English template and author vault unchanged (+4 ms per query).Secrets (fifth review)
redact() missed 13 of 16 common shapes. It now covers labels in either language and any separator (密钥:…, 凭据 …, Key: …), table cells, ?key= / &access_token=, connection-string passwords, more provider prefixes, and a backstop for any unbroken piece of ≥ 16 characters mixing letters and digits (Mistral, Together, 智谱, Fireworks, Perplexity, NVIDIA, Cerebras …). All 16 shapes are regression tests, with 11 ordinary texts that must stay untouched.secret: true, is never sent to a model, never quoted (the router shows only its path), never read by the API writer — no declaration needed.~/.dsh-krouter-keys.env (chmod 600) on the next run, the page line becomes a note, and nothing reads keys from the vault. Pasting on the page still works for hosts who never open a terminal.recall-v13 (full rebuild + vacuum on upgrade).When the night’s work did not happen
notify-send, Windows balloon) when the seal or the nightly upkeep was skipped for lack of a model, or failed; at most once a day; KROUTER_NOTIFY=0 off.Docs: README leads with the one-line install; an alias table is no longer a prerequisite for recommending it; what leaves the machine now names secret pages and says why keys never live in the vault.
The DSH bridge tests (extras/dsh/test-*.mjs) no longer write the host’s query log either — the last test entry point that did.
Not changed: answer cards stay on by default until the scheduled real-traffic test on 2026-10-16 (the author’s decision). Tests 172/172 on both router paths.
Fourth outside review. Both new findings reproduced, both fixed with regression tests that fail on 0.10.1.
90 系统文件/自动化/ is a rule zone, so answer cards sent that page’s full text to the model, and a query like DEEPSEEK API key 怎么填 printed the key line into the agent’s context. The author’s own vault had a real key on another page (90 系统文件/Agent记忆/…凭据.md) that answer cards had been sending since 0.9.0. Now: the key page is never a card target, never topic evidence, never an alias-proposal input; and every text KRouter prints, indexes, logs or sends passes through krouter_redact.redact() — values of key-like assignments (*_API_KEY=, token:, password =), known key shapes (sk-…, ghp_…, AKIA…, AIza…, xox…, hf_…, …) and bearer tokens become <redacted>; placeholders and plain numbers stay. Wired into the recall index and how, best lines, literal search (py and sh), the query log, answer cards and their check, topic evidence, learning prompts, alias proposals, and the API writer’s file reads. tests/test_secrets.py: a fake key on the key page and on another page appears in no prompt, no router output (both paths), no query log, no cache file.知 道 已 经, so ordinary Chinese questions were refused. Characters among the 1,500 most frequent (from jieba 0.42.1’s dictionary word frequencies, MIT) no longer count; 咖 啡 垢 still do. Recall bench gains the 12 gold-page questions in Chinese (same sample alias table): 0.9.3 9/12 with 3 of 6 half-off-topic answered, 0.10.1 5/12, 0.10.2 9/12 with every off-topic and half-off-topic question still refused (CI: ≥ 9/12). Author vault: synthetic, held-out and all 92 real queries unchanged.~/.dsh-krouter-keys.env.recall-v12: a cache built by an older version (raw page text) is rebuilt in full on upgrade and vacuumed, so no key survives in it or in its free pages.first_run.sh no longer writes the host’s query log. The demo routes run on the bundled template, not on the host’s traffic, yet each run appended its queries (home, correction, memory …) to ~/.local/state/krouter/queries.jsonl, which the nightly learning reads. 0.10.0 isolated pytest; the demo script, also run by verify_clean.sh, slipped past. It now sets KROUTER_NO_QUERY_LOG=1; a test runs it and checks that no log appears. Found while rolling 0.10.0 out on the author’s machine.Third outside review (two parts: a re-review of 0.9.3, and “would a stranger’s agent recommend this”). Every finding was reproduced first; one proposed fix was measured and replaced (below).
Behaviour that did not match the docs
claude / codex, while the writer detects grok, Codex, claude, cursor-agent, opencode, kimi, pi, vibe, antigravity. A grok-only host got five skipped steps and no sign of it. llm.py now owns the CLI candidates and their login probes; resolve.py imports them, so both lanes pick the same CLI. Each harness has a text-only invocation (grok: single-turn --prompt-file, verified).status stopped before host_action whenever no day was pending (a trailing [ … ] && returned 1 under set -e). Fixed; found while testing the nightly line.Answer cards
日更每天几点跑), and the reviewer’s adversarial card (0.125) sits inside the real cards’ range (0.09–0.17): no word-overlap threshold separates them. Instead: lexical checks gain “most of the answer’s words come from the page” (real cards ≥ 0.38; threshold 0.3), and a second model pass checks each card once — is the question about this page, does the answer answer it, is anything flipped or added. On 160 adversarial cards (half-off-topic question, swapped answer, flipped meaning, copied answer; 40 each) lexical checks catch 39, the second pass 160, with no real card lost beyond the 11 the lexical check already drops. This also closes 0.9.3’s admitted gap: a reworded sentence that flips the page is now caught. Verified cards carry v: 1; unverified ones are re-checked the next night; checkup.py counts them.-/_ pieces (20260811-090406-v4-flash-…) is no longer taken for a key; a 24+ unbroken mixed run (keys, 40-hex tokens) still is.Recall
咖啡机的定时器多久除垢一次 matched 定时 and came back high). Template bench 18/24 → 21/24; author vault synthetic, held-out and all 92 real queries unchanged.tests/fixtures/recall_bench.jsonl, template): 12 gold-page, 6 off-topic, 6 half-off-topic questions. Every off-topic and half-off-topic one must be refused; gold page first ≥ 9/12.The host’s data
90 系统文件/canonical_sources.psv): it syncs with the vault and is in its snapshots. map_for() resolves KROUTER_MAP > the vault’s table > the shipped sample everywhere (router py and sh, recall, L0, checkup); receipts print canonical_map:.install.sh --upgrade (--force is the same) replaces code only and keeps the alias table, ledgers and symlinks; a table edited in the old location moves into the vault once (a symlinked one gets a HOST ACTION instead of being rewritten behind another tool). Fresh installs seed the vault with the sample once.scripts/uninstall.sh: removes the timer (launchd / systemd / crontab line), Cursor rules, hook and skill; saves an alias table that is not in the vault yet; leaves the vault alone; --dry-run, --purge (cache and query log). Keys file is never deleted.extras/host-daily-evolution/run.py (a Python twin of run.sh) and scripts/install_windows_task.ps1 (-Uninstall). The PowerShell script has not been run on a Windows host yet.krouter-zones.psv at the vault root maps the default layout (90 系统文件|System, Agent第二大脑.md|Home.md, …); longest prefix wins, files too. Wired through the router, recall, cards, topics, checkup, nightly, ask, the key page and both writers. The DSH bridge still assumes the default names.propose_aliases.py drafts lock rows from the vault — unlocked rule / topic pages, ranked by how often real traffic reached them without a lock; --model adds model-proposed nouns — into 90 系统文件/自动化/别名候选.md. The host ticks rows; --apply re-checks and appends only those. Nothing is written unticked.Visibility and disclosure
nightly.py runs the five steps and writes each outcome — state, reason, model lane, calls, characters sent (metadata only) — to 90 系统文件/自动化/夜间维护.md and the health page (nightly, nightly_at). status prints it and raises host_action when steps are skipped, failing, or three days old. The launchd job now logs to ~/Library/Logs/dsh-krouter-daily.log.run.sh --dry-run (and nightly.py --dry-run): what would be sent, estimated tokens, which files would be written; no model call, no write.Evidence and process
docs/VERIFY.md says plainly that the 92 real queries are a development set, that the held-out synthetic set is flat from 0.6.0 (88% / 75% → 88% / 71% on today’s vault), the ablations, the adversarial card results, and that no fresh held-out traffic exists yet.tests/conftest.py now isolates state and cache for the whole test session; evaluation scripts set KROUTER_NO_QUERY_LOG=1.CONTRIBUTING.md: scoring-constant and refusal changes ship with before / after numbers on the recall bench and a held-out set; hosts should pin a tag.Tests 133/133 on both router paths.
Fixes from a second outside review, of 0.9.2. Each finding was reproduced before it was fixed.
保留 3 天, 保留 0 天 passed against a page that says 30 天; 端口 80 and 端口 808 passed against 8080. A specific must now appear as a whole token: no digit or letter on either side, and a number is not part of a decimal (3 is not in 1.3; 0.9.2 still matches v0.9.2). Pieces of joined words are checked the same way. In the author vault 11 of 447 cards now fail (was 7); the four new ones all quote a truncated identifier (sv-doma for sv-domain-001).checkup.py lists cards with nothing checkable (89 of 447 in the author vault) so a human can read them.high. On the template vault, a card asking 咖啡机除垢周期是多少 on 经验与方法.md made that page the confident answer. A card now counts only if its question shares at least one content word with its page (question words like 多少 do not count) — checked when the card is written, when the page is indexed (the question is not a trigger otherwise), and when it is read. All 447 real cards share one; the coffee card shares none. checkup.py lists off-page cards.llm.py had its own short provider table — no Gemini, Groq, Fireworks, Mistral, … — and no flagship lock, while the writer (api_writer.py) had both. Now there is one table and one lock, in llm.py; api_writer.py imports them (by file path, so an unrelated llm package cannot stand in). The nightly steps try keys in the writer’s order and lock the catalog’s flagship, unless KROUTER_LEARN_MODEL names one. The lock ranks Opus 5 above Opus 4. The Gemini key goes in a header, not the URL.KROUTER_NIGHTLY=0). Two stale answer: labels on the front page now read answer (generated):.Same-run checks (author vault, installed alias table): synthetic top-1 85%, held-out gold page found 88% / first 75%, wrong locks 0 — identical to 0.9.2. On the 92 real router queries the top hit and the answer line are identical to 0.9.2 for every query (40 carry an answer), so the front-page judge number stands. Tests 119/119 on both router paths.
Fixes from an outside review of 0.9.0. Each finding was reproduced before it was fixed.
answer: only. Card questions were merged into the page’s triggers without checking the hash, so an unrelated card question kept pulling the page up after the page changed. Card questions now count as triggers only while the card’s hash matches the page’s current content. Test: the page leaves the hits once it changes, not just the answer.answer: had no faithfulness check. “Only from this page” was a prompt instruction, nothing more. Now every number, path, command and latin word in an answer must appear verbatim in the page — checked when the card is written and again when it is read — and the line is labeled answer (generated):. Words the answer joined with / or + may be checked piece by piece; number combinations (1:3.3) must appear whole, since they are often computed. In the author vault 7 of 447 cards now fail and are not shown. Secret-like text is dropped before a card is written. The README no longer says the agent “usually does not need to open” the page: the page is the record.checkup.py now lists stale, not-verbatim, and too-broad cards (fewer than two content words).llm.py: *_API_KEY first (model from KROUTER_LEARN_MODEL, else the provider default; a provider with no known model is skipped, not guessed), then a logged-in claude / codex; --cli / KROUTER_LEARN_CLI is the host’s explicit pick and goes first. reconsolidate.py, consolidate_topics.py and answer_cards.py use it; no hard-coded CLI path. Pages that yield no cards are remembered by hash and not re-asked every night. Generation-side tests added.extras/host-daily-evolution/run.sh now runs them after the seal (KROUTER_NIGHTLY=0 turns them off); each step is independent and none changes the seal’s exit code. Documented in README, ARCHITECTURE, and SKILL.md, whose workflow now covers recall:, answer (generated):, scope_widened and recall: none.docs/VERIFY.md speaks for itself.Synthetic sets after the fix: top-1 85%, held-out gold page found 88% (one question fewer than 0.9.0), wrong locks 0, off-topic unchanged. Tests 117/117 on both router paths.
Docs only. The README had grown one appended section per release, each with its own numbers from a different run. Now: the front page states what the product does today (lock, then ranked recall with precomputed answers, refusing instead of guessing; topics and answer cards built overnight) and quotes one same-run number — 92 real router queries, blind-judged, ≤ 0.5.1 vs 0.9.0: mean usefulness 0.62 → 1.47 of 2, direct answers 14 → 55, useless 49 → 12. The four after-the-lock features are one section. Evidence is four layers: docs/VERIFY.md adds layer 4 (real traffic) with the method, every per-release run, the held-out synthetic set, the judge’s noise (±0.05), and what still fails. PROTOCOL.md and docs/ARCHITECTURE.md now describe the miss path the code runs, and drop “a rejected page still locks”, which stopped being true in 0.7.0.
Answer cards (sleep-time compute). The router used to stop at “here is the right page”: the one-line how was just the page’s first sentence (你的层(全局结构)), so an agent still had to open the page. Real traffic is predictable — half the calls repeat an earlier question — so answer_cards.py precomputes, for every rule and topic page, 3–5 question → one-line answer cards taken only from that page (447 cards on 104 pages in the author vault). Cards live in 90 系统文件/自动化/下意识问答.jsonl (no source page is edited), carry the page’s content hash, and are rewritten only when the page changes; a card whose page changed is ignored until then. Card questions also act as recall triggers, and the best-matching card on each hit prints as answer:.
One-concept queries (DSH, 发版, grok-fixed; not bare numbers) skip the two-content-word gate, and a query that is a page’s name (title, file name, or topic_terms) doubles that page; a named dossier now outranks same-named scattered rule pages. Speed: the per-query freshness walk no longer builds a pathlib object per file — recall 112 → 30 ms in-process, router p50 ~105–111 ms.
Field self-report, author vault (not in clone): 92 real router queries, blind-judged 0.8.0 vs 0.9.0: mean 1.34 → 1.43 of 2, direct hits 46 → 52, useless 15 → 12; 40 of 92 now carry an answer: line. Synthetic: top-1 82% → 85%, held-out right page 88% → 92%, wrong locks 0, off-topic unchanged.
Topic dossiers: the missing middle layer. Retrieval was near its ceiling (synthetic: right page top-1 83%, top-10 95%); what still failed on real traffic was knowledge that lived only in daily logs (SyGJ in 44 logs, zero pages). consolidate_topics.py merges each listed topic (90 系统文件/自动化/主题档案清单.psv) into 02 经验与方法/主题档案/主题|…md: one-line summary, current state as of the latest evidence, facts with a [[source]] each, user corrections, timeline, open conflicts, authority pages first. Evidence is the vault only (authority pages, other pages, logs newest first, capped); frontmatter is written by the script, not the model; every wikilink must resolve or is demoted to text; a page is regenerated only when its evidence hash changes. Status stays provisional.
Recall treats dossiers as their own tier and only boosts one when the question names its topic (title or topic_terms), so a 20 KB page cannot win on stray words. Refusal got stricter where dossiers made it looser: bigrams with function characters (的了我你他她吗呢吧啊么什) and words that only appear in model-written triggers no longer count as content; a latin term the vault has never seen (Kubernetes) refuses unless more than two real words match. Single CJK characters are dropped from the query.
Field self-report, author vault (not in clone): 92 real router queries, blind-judged old vs new: mean 0.66 → 1.41 of 2, direct hits 14 → 52, useless 45 → 14; 16 topic questions written before the dossiers existed: 0.62 → 1.31. Synthetic held-out unchanged (88% / top-1 75% / wrong locks 0 / 0 of 6 off-topic answered). p50 ~115 ms.
Memory maintenance, measured on the router’s own real traffic. 92 de-duplicated queries the author’s agents actually sent in 30 days, blind-judged (old vs new shuffled, Opus scores 0/1/2): mean 0.74 → 1.26, direct hits 20 → 42, useless 44 → 18. Synthetic held-out set: right page 88% → 92%, top-1 75%, wrong locks 0, 0/6 off-topic answered; p50 ~113 ms.
纠错 no longer locks 纠错优先级 for Claude Code 纠错. A source page whose status is superseded / retired / rejected / deprecated / archived is out of force: no lock, no L0 row.DSH, SyGJ) answers when its one content term matches; bare numbers are not content terms. Trigger text now feeds the word-likeness statistics, and a bigram that appears in any title or trigger counts as a real word when deciding whether to refuse (别名 in 别名表谁能改).preference / correction / memory / project widen to the whole vault (rule pages first) when their own scope has no page that covers the question, and print scope_widened: vault.~/.local/state/krouter/ (KROUTER_NO_QUERY_LOG=1 turns it off). Half of real calls repeat an earlier question.reconsolidate.py (nightly, one model call): weak queries + their recall candidates → the model picks the page that answers, or none → rel|original query appended to 90 系统文件/**/下意识触发词-自学习.psv. Recall-only, never a lock, no invented paraphrase. Replay on history: 8 learned, 20 none, 12/12 off-topic probes refused.checkup.py (nightly, no model): writes 90 系统文件/自动化/下意识体检.md — locks and triggers pointing at missing / dead pages, supersedes pairs where recall still ranks the replaced page first, rule pages nobody read or recalled in 60 days. Lists only; never edits sources.A lock miss is now answered by L1 recall instead of a whole-sentence literal grep. skill/krouter-obsidian/scripts/recall_index.py keeps a rebuildable SQLite FTS5 cache under ~/.cache/krouter/ (stdlib only, no service, no vectors): CJK bigrams + latin words, title and triggers weighted, rule pages first, superseded pages sink, cross-word junk bigrams filtered. Each hit prints its how line and the best matching line; weak overlap prints recall: none and must not guess. Triggers come from the alias table plus any 90 系统文件/**/下意识触发词.psv (rel|phrase;phrase) and never lock.
The lock got stricter: a short alias inside a longer question (日更 in 日更能不能交给云端跑) only ranks suggestions. route_knowledge.sh now execs the Python twin (one process instead of four); KROUTER_SH_ONLY=1 keeps the shell path. Ships the L0 compiler compile_subconscious.py and the status seal lines (daily_seal*) from the health page.
Field self-report, author vault (not in clone): 30 held-out questions written before tuning, right page 21% → 88%, top-1 0% → 75%, wrong locks 0, 0/6 off-topic answered, p50 118 ms. Clone tests: tests/test_recall_index.py.
Windows can run the same lock through skill/krouter-obsidian/scripts/route_knowledge.py; DSH on win32 prefers that file. Same-score miss across two pages prints host_prompt and must not guess. sha256 falls back to Python when shasum is missing. One protocol, two launchers. Does not ship L0 / 下意识.md.
Ask-as-product matcher in skill/krouter-obsidian/scripts/ask_product.py. At most one provisional page per utterance. record never writes active; promote does, after adopt + this task accepted. Triggers live on the page (triggers:) or 90 系统文件/自动化/ask-triggers.json. install.sh copies extras/cursor/ask-product.mdc. This clone does not ship an author’s trigger table. Codex / Claude still paste the snippet.
Lock-vs-neighbor Claim C: multilingual MiniLM; CJK inversion gone; H15 发版 already ranks the seal page before any floor (precision, not a threshold miss). Claim D: BGE-M3 middleweight dense; hit 16/16; at 0.40 the leftover is neighbor B03 (生产事故 → timer), not one of the 12 OOD negatives. Do not merge C and D into “neither matched the lock.” On this contract (refuse + no neighbor cite), featherweight beats that middleweight stack — the “better than mainstream embedding RAG” line. Heavyweight hybrid + rerank is out of scope. Pytest replays frozen JSON (no extra pip in CI). Hub snapshots are not in the clone. Ship scripts/verify_clean.sh so the upload gate named in CONTRIBUTING exists in the clone.
Ship in_force / live_rows / parse_frontmatter on canonical_lookup.py. verify_canonical_map.py and the lock-vs-neighbor runner imported them since 0.4.1; the lookup module on GitHub did not. Clone python3 scripts/verify_canonical_map.py raised ImportError. Not L0.
Docs and checkers. No new product lane.
docs/VERIFY.md and the README: implementation (clone_25), comparison (lock_vs_neighbor), field self-report (author vault, not in clone). clone_25 25/25 is not the LLM 25/25.scripts/verify_sealed_days.py. Same trust model as verify_canonical_map.py — measures this vault’s 05 时间日志/, does not replay the author’s 72 consecutive seals. Empty files and 待总结 / to-summarize are gaps; a real note on the same day beats a stub.Already on main since 0.4.0, first tagged here: public lock-vs-neighbor fixture (N=36), clone_25 analogue, host-facing listing, both rungs of the ladder, receipt SVGs, latin-fragment alias rule, API-key-first / CLI subscription writer.
preference, correction, memory, project in addition to status / search / suggest.extras/claude-code/CLAUDE.snippet.md.check.sh. Template lamp defaults to unused. first_run.sh proves the extra without starting a timer.dsh plugin add still does not create a cron.extras/dsh registers read-only krouter_status, krouter_search, krouter_suggest.OBSIDIAN_VAULT and alias map as Cursor and Codex.first_run.sh on every push to main.suggest route: prefix/overlap alias hints on a miss. Hints are not hits.suggestions: so an agent can retry one noun.