Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ab92796
feat(telemetry): Change A+B — store_queries config + session_id on co…
RodrigoCordoba Jun 11, 2026
5ce44a2
feat(telemetry): Change C — dropped-capsule sidecar + retrieval.regre…
RodrigoCordoba Jun 11, 2026
aacfb5f
Merge story/telemetry-capture: query storage, session linkage, regret…
RodrigoCordoba Jun 11, 2026
a3fe232
feat(roi): ROI ledger — kimetsu brain roi + stop-hook savings line
RodrigoCordoba Jun 11, 2026
d478816
Merge story/roi-ledger: kimetsu brain roi, stop-hook savings line, me…
RodrigoCordoba Jun 11, 2026
0a44130
feat(brain): v1.5 story 2.1 — render-time capsule compression (defaul…
RodrigoCordoba Jun 11, 2026
c4c20e4
Merge story/token-budget: render-time compression + session dedupe
RodrigoCordoba Jun 11, 2026
39bb245
feat(brain): add record_mcp_citation — write memory.cited event from …
RodrigoCordoba Jun 11, 2026
bb99a76
feat(mcp): Part A — kimetsu_brain_cite MCP tool closes ground-truth gap
RodrigoCordoba Jun 11, 2026
2048f6c
feat(brain): tuneset — personal eval-set builder from context.served …
RodrigoCordoba Jun 11, 2026
f03ca95
feat(cli): kimetsu brain tune --status — personal eval readiness + ki…
RodrigoCordoba Jun 11, 2026
4c5c132
feat(brain): tune.rs — sweep space, objective, holdout split, history…
RodrigoCordoba Jun 11, 2026
c991dc7
feat(cli): kimetsu brain tune sweep — dry-run/apply/revert with holdo…
RodrigoCordoba Jun 11, 2026
139ff06
chore: fmt + clippy gate-pass for self-tuning brain (v1.5)
RodrigoCordoba Jun 11, 2026
189d29e
fix(tune): sweep against the production embedder, not Noop
RodrigoCordoba Jun 11, 2026
58bd120
chore: fmt artifacts + lockfile for self-tuning story
RodrigoCordoba Jun 11, 2026
8372a7c
Merge story/self-tuning: MCP cite tool, personal eval set, guarded tu…
RodrigoCordoba Jun 11, 2026
148b0a1
feat(consolidation): Story 3.1 — near-duplicate merge via memory.supe…
RodrigoCordoba Jun 11, 2026
b3bf274
feat(cli): Stories 3.2+3.3 — brain consolidate + brain triage commands
RodrigoCordoba Jun 11, 2026
063c9c7
Merge story/consolidation: superseded_by migration, merge/distill/triage
RodrigoCordoba Jun 11, 2026
db66d63
feat(brain): export --redact / --redact-tags flags, closes #24
RodrigoCordoba Jun 11, 2026
578de3b
feat(bridge): Cursor + Gemini CLI installer seams (story 5.2+5.3)
RodrigoCordoba Jun 11, 2026
44e4127
ci: add test-embeddings job, ubuntu-only, closes #22
RodrigoCordoba Jun 11, 2026
3e906fb
Merge story/reach: export --redact, Cursor + Gemini CLI installers, C…
RodrigoCordoba Jun 11, 2026
7653827
docs(release): v1.5.0 CHANGELOG, README, HOW-KIMETSU-WORKS, INSTALL
RodrigoCordoba Jun 11, 2026
2f1bfe9
docs: fix tune flag name in CHANGELOG — min_lexical_coverage
RodrigoCordoba Jun 11, 2026
89a6f51
docs: truthfulness-audit fixes for v1.5.0 claims
RodrigoCordoba Jun 11, 2026
ccab4e2
fix(consolidate): make consolidation merge REBUILD-SAFE via enriched …
RodrigoCordoba Jun 11, 2026
2d86de5
fix(retrieval): exclude superseded rows from search, top, prune, anal…
RodrigoCordoba Jun 11, 2026
ba60954
fix(tune): gate --apply behind personal data — refuse in fixture-fall…
RodrigoCordoba Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,36 @@ jobs:
KIMETSU_USER_BRAIN: "0"
run: cargo test --workspace --locked -- --test-threads=1

test-embeddings:
name: test (embeddings, ubuntu)
runs-on: ubuntu-latest
# Closes #22: run the full test suite compiled with the `embeddings` feature
# (ONNX/fastembed) on Linux only. macOS/Windows are covered by `test`
# (which feature-unifies the workspace and already pulls in the embeddings
# code paths), but the explicit ubuntu run here pins an embeddings-specific
# build cache and ensures the feature compiles cleanly as a distinct slice.
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-test-embeddings
# Cache the HuggingFace model files and the fastembed on-disk ONNX cache
# so repeated runs do not re-download ~200 MB of weights.
- name: Cache HuggingFace / fastembed model files
uses: actions/cache@v4
with:
path: |
~/.cache/huggingface
.fastembed_cache
key: embeddings-models-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
restore-keys: embeddings-models-${{ runner.os }}-
- name: cargo test (embeddings)
env:
KIMETSU_USER_BRAIN: "0"
run: cargo test --workspace --locked --features embeddings -- --test-threads=1

audit:
name: cargo-audit (RUSTSEC)
runs-on: ubuntu-latest
Expand Down
99 changes: 99 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,105 @@ onward the project follows SemVer normally: patch releases are
bug-fix-only, minor releases are backward-compatible additions, and
breaking changes require a major bump.

## v1.5.0 — pays for itself

ADDED
* **Telemetry capture.** Raw query text is now stored in `context.served`
telemetry events when `[learning] store_queries = true` (default; set
`false` to revert to the pre-v1.5 query-hash-only behavior). A
`session_id` field is also written when the host provides one (Claude Code
hooks; absent for hosts that do not emit it). Dropped-capsule sidecar
(`~/.kimetsu/cache/<hash>/dropped-recent.json`) records capsules that
were floor-filtered out so that a later citation of one is detected as a
`retrieval.regret` event, feeding the self-tuning loop. All telemetry
stays on-machine; nothing is exported.

* **ROI ledger — `kimetsu brain roi`.** Conservative per-kind token-savings
estimates (failure_pattern=1500, command=400, convention=300, fact=500,
preference=200 tokens per citation) minus brain-injection overhead give a
net-positive / net-negative verdict. Dollar estimates are shown when the
active model is recognized from a built-in price table (Claude 3/4, GPT-4/5
families, Bedrock routing prefixes) or when `[model] price_per_mtok` is set
in `project.toml`. `--window 7d|30d|all` and `--json` for scripting. The
Stop hook appends a per-session savings sentence when ≥1 citation occurred;
zero-citation sessions are silent. Calibration methodology and honest
limitations: `docs/ROI-METHODOLOGY.md`.

* **Token budget — render-time capsule compression and session dedupe.**
Two `[broker]` toggles, both default `true`:
- `compress_capsules`: capsule summaries are compressed at render time
(strips `[tags: ...]` / `(context: ...)` annotations, caps at 3
sentences). Ranking is never affected — this runs only after retrieval
and reranking. Set `false` to inject full memory text.
- `session_dedupe`: the `UserPromptSubmit` hook skips capsules whose
handle was already injected earlier in the same session (tracked via
the proactive-state sidecar). Soft policy: falls back to injecting all
capsules when dedupe would produce an empty set. This addresses the
pre-v1.5 behavior where the main hook re-injected the same top capsule
on every prompt of a long session.

* **Self-Tuning Brain — `kimetsu_brain_cite` MCP tool + `kimetsu brain tune`.**
`kimetsu_brain_cite` is a new write-gated MCP tool that records a
`memory.cited` event from inside an MCP session, closing the ground-truth
gap when the model leans on a memory but doesn't explicitly call
`cite_memory`. Personal eval set: `tuneset` builds positive eval cases from
`context.served` + citation joins (exact session_id or ±30-minute window
fallback). `kimetsu brain tune --status` reports case count and kind
coverage. `kimetsu brain tune` (dry-run by default) sweeps `broker.min_lexical_coverage`
∈ {0.3, 0.4, 0.5, 0.6} × `broker.min_semantic_score` ∈ {-1.0(AUTO), 0.0, 0.25,
0.35, 0.45} against the production embedder; `--apply` writes only the
floor parameters (not the reranker — that change is recommended separately);
`--revert` restores the previous tune-history entry. A holdout guardrail
(deterministic 20% split) prevents writing a config that regresses the
holdout objective.

* **Consolidation — `kimetsu brain consolidate` + `kimetsu brain triage`.**
Schema migrated to **v3** (`superseded_by` column + index on `memories`).
`brain consolidate` (Story 3.1, default): brute-force cosine scan within the
same embedding model; clusters at ≥ 0.92 cosine (configurable with
`--threshold`) are merged — survivor keeps its id and text, members get
`superseded_by` set and a `memory.superseded` event written (rebuild-safe);
citations are reassigned to the survivor. `--distill` (Story 3.2): looser
clusters (0.75–0.85 cosine band, ≥3 memories, ≥1 shared tag) are fed to
the configured distiller; result lands as a memory proposal for human
review; prints clusters and exits 0 when no distiller is configured.
`brain triage` (Story 3.3): interactive per-item keep / prune / skip of
memories below a usefulness and age threshold (`--score-floor 0.2`,
`--age-days 30`); `--prune-all --yes` for batch non-interactive pruning.

* **Reach — export redact, Cursor + Gemini CLI installers, CI embeddings job.**
`kimetsu brain export --redact` strips the `(context: …)` segment from
exported memory text; `--redact-tags` (requires `--redact`) additionally
strips the `[tags: …]` prefix. Both flags are useful for sharing brains
without leaking workspace-specific file paths or tag metadata.
`kimetsu plugin install cursor` and `kimetsu plugin install gemini-cli`
write MCP config (`.cursor/mcp.json` / `.gemini/settings.json`) and an
always-on guidance file (`.cursor/rules/kimetsu-brain/rule.md` for workspace
installs; `GEMINI.md` merged into the project root or `~/.gemini/GEMINI.md`
for global installs). Neither host has a `UserPromptSubmit`-style hook
system, so MCP + the guidance file are the complete integration surface.
**Note: Cursor and Gemini CLI config schemas were inferred from their public
docs as of June 2026 and have not been verified against live host builds —
treat these installers as a starting-point and check the generated files
before committing them.** CI: a new `test-embeddings` job (ubuntu-only,
`--features embeddings`, HuggingFace + fastembed cache) runs alongside the
existing lean test matrix.

CHANGED
* `kimetsu.mcp_write_tools` gate now covers `kimetsu_brain_cite` alongside
the existing write tools (same env / config / default-true logic for the
local stdio server; remote server remains env-only, default-deny).
* Stop hook output includes a per-session token-savings line when the brain
was cited at least once that session.
* `brain.db` schema advanced from **v2 → v3** (automatic on first
read-write open; sidecar backup taken per the existing migration policy).
* `brain export` gains `--redact` / `--redact-tags` flags (no behavior
change for existing callers).

FIXED
* Tune sweep now runs against the production embedder (not the Noop embedder
used in tests), so floor calibration is on real vectors.

## v1.0.0 — durable migrations, analytics, semantic retrieval, proactive recall

ADDED
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ the same expensive exploration you already paid for last week.

Kimetsu fixes the forgetting. It's a **sidecar brain**: a single Rust binary
that runs next to any supported host agent through MCP (Claude Code, Codex, Pi,
OpenClaw) or as its own terminal chat — or, in beta, server-hosted over HTTP MCP
and shared across a team. It learns which memories the model *actually used to
win*, and lets that knowledge compound across runs.
OpenClaw, Cursor, Gemini CLI) or as its own terminal chat — or, in beta,
server-hosted over HTTP MCP and shared across a team. It learns which memories
the model *actually used to win*, and lets that knowledge compound across runs.

- **It remembers.** Project conventions, failure patterns, the exact command
that regenerates your schema — captured once, retrieved automatically.
Expand Down Expand Up @@ -101,6 +101,8 @@ watch it come back:
kimetsu brain memory add --scope project --kind convention "Use cargo nextest for all test runs"
kimetsu brain context "how do I run tests?" # broker-ranked context bundle
kimetsu brain insights # is the brain actually helping?
kimetsu brain roi # did it pay for itself? (token savings vs overhead)
kimetsu brain tune # self-tune retrieval floors from your own eval data
```

Prefer a standalone REPL? `kimetsu chat --workspace . --project .` is a full
Expand Down
7 changes: 6 additions & 1 deletion crates/kimetsu-agent/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1099,7 +1099,12 @@ pub fn run_coding(options: CodingRunOptions) -> KimetsuResult<CodingRunResult> {
),
)?;

projector::apply_events(&conn, &read_trace(&run_paths.trace_jsonl)?)?;
let trace_events = read_trace(&run_paths.trace_jsonl)?;
projector::apply_events(&conn, &trace_events)?;
// v1.5: best-effort regret detection — check whether any cited memory
// was in the recent-dropped window (excluded by the relevance floor
// in the hook but cited by the model), and emit retrieval.regret events.
project::emit_regret_for_cited_memories(&paths.repo_root, &trace_events);

Ok(CodingRunResult {
run_id,
Expand Down
5 changes: 4 additions & 1 deletion crates/kimetsu-brain/src/analytics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,9 @@ pub fn compute_insights(start: &Path, opts: InsightsOptions) -> KimetsuResult<In
"
SELECT memory_id, text, usefulness_score, use_count
FROM memories
WHERE invalidated_at IS NULL AND use_count >= 1
WHERE invalidated_at IS NULL
AND superseded_by IS NULL
AND use_count >= 1
ORDER BY (usefulness_score / CAST(use_count AS REAL)) DESC, use_count DESC
LIMIT ?1
",
Expand All @@ -387,6 +389,7 @@ pub fn compute_insights(start: &Path, opts: InsightsOptions) -> KimetsuResult<In
SELECT memory_id, text, usefulness_score, use_count
FROM memories
WHERE invalidated_at IS NULL
AND superseded_by IS NULL
AND use_count >= 3
AND (usefulness_score / CAST(use_count AS REAL)) <= -0.2
ORDER BY (usefulness_score / CAST(use_count AS REAL)) ASC
Expand Down
30 changes: 23 additions & 7 deletions crates/kimetsu-brain/src/ann.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,14 @@ pub fn cached_handle(conn: &Connection) -> Option<Handle> {
reg.get(&sidecar).cloned()
}

/// Remove a superseded memory from the cached ANN index by its `memory_id`.
/// Mirrors `on_invalidate` — superseded rows are excluded from ANN retrieval
/// the same way invalidated rows are. No-op for in-memory DBs / cold indexes
/// (reconcile-on-open handles those).
pub fn on_supersede(conn: &Connection, memory_id: &str) {
on_invalidate(conn, memory_id);
}

/// Remove a memory from the cached index by its `memory_id` (no-op for
/// in-memory DBs / cold indexes — reconcile-on-open will catch it).
pub fn on_invalidate(conn: &Connection, memory_id: &str) {
Expand Down Expand Up @@ -347,7 +355,8 @@ impl AnnIndex {
fn reserve_and_load_active(&mut self, conn: &Connection) -> KimetsuResult<()> {
let count: i64 = conn.query_row(
"SELECT COUNT(*) FROM memories
WHERE invalidated_at IS NULL AND embedding IS NOT NULL AND embedding_model = ?1",
WHERE invalidated_at IS NULL AND superseded_by IS NULL
AND embedding IS NOT NULL AND embedding_model = ?1",
rusqlite::params![self.model_id],
|r| r.get(0),
)?;
Expand All @@ -361,7 +370,8 @@ impl AnnIndex {
const BUILD_CHUNK: usize = 16384;
let mut stmt = conn.prepare(
"SELECT rowid, embedding FROM memories
WHERE invalidated_at IS NULL AND embedding IS NOT NULL AND embedding_model = ?1
WHERE invalidated_at IS NULL AND superseded_by IS NULL
AND embedding IS NOT NULL AND embedding_model = ?1
ORDER BY rowid",
)?;
let mut rows_iter = stmt.query(rusqlite::params![self.model_id])?;
Expand Down Expand Up @@ -570,7 +580,8 @@ impl AnnIndex {
const RECONCILE_CHUNK: usize = 16384;
let delta_count: i64 = conn.query_row(
"SELECT COUNT(*) FROM memories
WHERE invalidated_at IS NULL AND embedding IS NOT NULL
WHERE invalidated_at IS NULL AND superseded_by IS NULL
AND embedding IS NOT NULL
AND embedding_model = ?1 AND rowid > ?2",
rusqlite::params![self.model_id, self.max_rowid_indexed],
|r| r.get(0),
Expand All @@ -582,7 +593,8 @@ impl AnnIndex {
}
let mut stmt = conn.prepare(
"SELECT rowid, embedding FROM memories
WHERE invalidated_at IS NULL AND embedding IS NOT NULL
WHERE invalidated_at IS NULL AND superseded_by IS NULL
AND embedding IS NOT NULL
AND embedding_model = ?1 AND rowid > ?2 ORDER BY rowid",
)?;
let mut rows_iter = stmt.query(rusqlite::params![self.model_id, self.max_rowid_indexed])?;
Expand Down Expand Up @@ -618,12 +630,16 @@ impl AnnIndex {
drop(stmt);
self.max_rowid_indexed = max_rowid;

// 3b. Remove rows now invalidated (only those <= the watermark; newer
// ones were never added). `remove` is a no-op if absent.
// 3b. Remove rows now invalidated or superseded (only those <=
// the watermark; newer ones were never added). `remove` is a
// no-op if absent. Superseded rows are treated the same as
// invalidated: they stop appearing as ANN candidates so
// retrieval queries only surface the survivor.
let gone: Vec<i64> = {
let mut stmt = conn.prepare(
"SELECT rowid FROM memories
WHERE invalidated_at IS NOT NULL AND rowid <= ?1",
WHERE (invalidated_at IS NOT NULL OR superseded_by IS NOT NULL)
AND rowid <= ?1",
)?;
stmt.query_map(rusqlite::params![self.max_rowid_indexed], |r| {
r.get::<_, i64>(0)
Expand Down
Loading
Loading