Added
- Unified distribution, Phase 2 (GH #724/#726): self-service addon
publishing + hosted installs. Newlean-ctx addon publish --namespace <ns>wraps the authoringlean-ctx-addon.tomlverbatim into a signed
kind=addoncontext package and uploads it through the existing ctxpkg
publish path — after local gates that mirror the hosted listing bar
(schema, runnable[mcp]endpoint, audit verdict: blocking findings
refuse,reviewpublishes disclosed;--checkruns everything without
network I/O).lean-ctx addon add <ns>/<name>[@version]installs hosted
packs: index-verified download, integrity hashes, mandatory ed25519
signature, kind ↔ payload coherence — then the embedded manifest walks the
unchanged consent → preflight → health-probe pipeline;addon update
re-resolves from the install source. The context registry refuses to
importkind=addonpacks (wrong trust chain, useaddon add). The
bundled registries are now generated snapshots:gen_registryvalidates,
sorts and canonicalizesrust/data/{addon,grammar}_registry.json;
CI + preflight fail on drift (deterministic, timestamp-free, #498). - Unified distribution, Phase 1 (GH #724/#725): managed addon binaries +
thekindpackage taxonomy..ctxpkgmanifests gain an optionalkind
field (context|skills|addon|grammar; defaultcontext,
omitted when serializing, so every existing package stays byte-identical —
non-context kinds require schema v2). Addon manifests gain an
[artifacts.<target-triple>]block:addon adddownloads the prebuilt
binary for the current platform into the managed bin dir
(<data_dir>/addons/bin/<name>/<version>/, neverPATH), verifies its
SHA-256 before the atomic install, auto-pins that hash as the spawn-time
binhash, and rewrites the gateway command to the absolute managed path —
zeroPATHinteraction, tamper ⇒ spawn refused,addons.policy = locked
blocks the fetch before any network I/O. Newlean-ctx addon update <name>
installs side-by-side, health-gates, then prunes;addon removedeletes the
managed binaries;doctorverifies every receipt (exists + hash + not
revoked). The grammar-dylib fetch (#690) now shares the same
download→verify→install core (artifact_install), byte-identical behavior. - Universal cost coverage for every provider — LiteLLM catalog, gateway cost
headers, operator price overrides (GL #1189). GL #1179 made OpenRouter
turns exactly billed and OpenRouter-listed models live-priced; three gaps
remained and are now closed: (1) second live catalog — the LiteLLM
community price map (~2900 entries, no key) is fetched alongside the
OpenRouter catalog in the same refresh/disk-cache/kill-switch cycle and
merged gap-filling (OpenRouter wins on conflicts), coveringazure/,
bedrock/,vertex_ai/,groq/,mistral/, embedding models and niche
hosts; either source failing is tolerated (fail-open, previous table kept);
(2) measured cost from response headers — LiteLLM-style gateways report
the billed USD per turn inx-litellm-response-cost, which the proxy now
reads (plus an operator-defined header via[proxy] cost_response_header)
and books as provider-measured cost; a body-reported figure (OpenRouter
usage.cost) always beats the header, and junk header values never enter
the ledger; (3) first-class negotiated prices —[cost.prices."<model>"]
in config.toml (input_per_m,output_per_m, optional cache rates) merges
into the pricing table as exact entries, overriding embedded and live
catalog rows for committed-use discounts, Azure PTU or zero-rated internal
models; only a provider-measured bill ranks higher. Exact matching now runs
against the full loaded table (custom override names price exactly), and the
price-source ladder is uniform for every provider: measured bill > operator
override > live catalogs > embedded list > family heuristic > blended
fallback — each rung honestly labeled viacost_source. - Index-time include/exclude filters (GH #735). The retrieval corpus is
now declared explicitly instead of abusing.gitignorefor retrieval
policy: new[index]config (exclude,include,respect_gitignore)
plus per-run CLI overlays on everylean-ctx indexbuild command —
repeatable--exclude <glob>/--include <glob>(both--flag valueand
--flag=valueforms) and--no-gitignore/--respect-gitignore. One
shared filter layer (core::index_filter) drives the BM25 walk, the graph
scan, the graph staleness check, and the watch snapshot; the semantic index
chunks the BM25 corpus and inherits the same universe — excluded files
produce no chunks, graph nodes, or embeddings. Globs match the
root-relative path (forward slashes on every platform); exclude wins over
include; a non-empty include list admits matching files only. CLI
--excludeappends to config,--includereplaces the config set for the
run, and a run with overlay flags skips the daemon delegation so the
one-off corpus can't be overwritten by a config-built index.index status
(human +--json) reports the active filter summary; repo-local config
extends global excludes and can only tighten gitignore handling. Empty
filters preserve today's behavior byte-for-byte. - Personas now drive the whole pipeline (persona-spec-v1 runtime wiring,
GL #1178). The five declared persona fields were spec +
capability-reporting only; every one of them is now consumed at runtime:
default_read_modeenters thectx_readmode chain (explicit arg > policy
pack > persona > profile/auto;"auto"= no opinion),compressorcompacts
ctx_url_readflowing-text modes through the extension registry
(research→markdown,support/lead-gen→prose; extractive
facts/quotes/linksstay verbatim to protect citations),chunker
makes token-budget truncation cut on chunk boundaries (paragraphs / line
windows) instead of mid-sentence,intent_taxonomylands as the
contract-promised persona block (PERSONA:/INTENTS:/DEFAULTS:) in
the MCP session instructions, andsensitivity_floorfolds into
[sensitivity]enforcement (Config::sensitivity_effective) — a floor
abovepublicenables enforcement out of the box and can only tighten an
explicit config (LEAN_CTX_SENSITIVITY=offstays the kill switch). The
codingdefault declares the historical defaults
(auto/identity/public), so default installs remain byte-identical
(#498 prompt-cache stability). Contract doc gains a "Runtime wiring" table
(docs/contracts/persona-spec-v1.md).
Fixed
- MCP stdio server processes leaked after client disconnect (GH #733).
When the transport closed while an abandoned tool handler (#271 watchdog)
still occupied a blocking thread, the implicit Tokio runtime drop blocked
on that thread forever — the server process survived the disconnect
invisibly. Clients that force-reconnect on tool timeout (e.g. the Pi
extension's MCP bridge) spawn a fresh server per reconnect, so every
abandoned handler leaked one ~26 MBlean-ctxprocess; on a 1.9 GB
machine 50+ accumulated within a session and exhausted RAM + swap. The
runtime now shuts down with a bounded 2 s grace period after the transport
closes (telemetry is already flushed at that point); hung stragglers die
with the process. - Gateway console showed ~15x the real OpenRouter bill for unknown models
(GL #1179). An external gateway run (Claude Code → lean-ctx → OpenRouter,
deepseek/deepseek-v4-flash) was billed $0.05 by OpenRouter while the
console reported $0.74: the embedded price table had no V4 entry, so the
heuristic matcher silently fell back to 2025-eradeepseek-v3list prices —
and presented the estimate as COST. Cost accounting is now three-layered and
honest: (1) measured provider cost — OpenRouter chat requests opt into
usage: {include: true}(only when the effective upstream is openrouter.ai;
api.openai.com never sees the non-standard field), and the response scanner
absorbsusage.costpluscost_details.upstream_inference_cost(BYOK) as
the authoritative billed charge, which beats every table estimate in the
meter, the policy gate andusage_events; (2) live prices for all
models — a newcore::gain::live_pricingmodule fetches the public
OpenRouter models catalog (320 models) with a 24h disk cache, atomic swap,badges, KPI foot line, CSV columns
background refresh at proxy/gateway/dashboard startup, slug normalization
(vendor prefixes, date suffixes,:free/:extendedvariants) and a
LEAN_CTX_LIVE_PRICING=offkill switch, slotted between exact embedded
matches and the heuristic inModelPricing::quote; (3) cost provenance
— every usage event now recordscost_source
(provider|live|list|heuristic|shadow), the admin console marks
measured vs estimated spend (✓/
measured_requests/estimated_requests),lean-ctx spendprints ✓/*
markers per model, and/api/status+ the health strip expose live-pricing
freshness. Result: OpenRouter-billed turns show the exact invoice amount;
everything else shows current list prices instead of stale hardcoded ones,
and estimates are visibly labeled as such. - Cursor sessions ran with read compression silently disabled (GH #722).
Cursor ≥ 3.7 exportsCLAUDE_PROJECT_DIRto its hook child processes for
Claude-compat — and lean-ctx's guard-host detection took that variable as
proof the host is Claude Code, soread_redirect = autoswitched the
PreToolUse Read redirect off (the #637 protection) in every Cursor
session: native Reads/Greps passed through uncompressed, with no warning
(a 2-hour Cursor session showed 152 file reads, 0 redirected). Cursor has no
read-before-write guard, so its own markers (CURSOR_VERSION,
CURSOR_PROJECT_DIR,CURSOR_AGENT, …) now identify the host first and win
over the compat variable; the PostToolUse read-dedup inherits the same
corrected detection. Real Claude Code behavior is unchanged. - A stdin-reading command could wedge an agent's shell session forever
(GH #723). The bufferedlean-ctx -cpath inherited the host's stdin, so
a command that falls back to reading stdin — e.g.rgleft without a path
argument by an empty$(…)substitution — blocked on a pipe that never
delivers EOF. Worse, the timeout kill only reaped the direct shell child:
orphaned grandchildren kept the captured stdout pipe open, the reader
threads never saw EOF, and the caller hung after the timeout had fired
(observed: one orphanedrgkept a Cursor shell session dead for hours).
In agent/pipe contexts (stdin not a TTY) the child now gets/dev/null
stdin (immediate EOF) and runs in its own process group, which the timeout
kill signals as a whole — grandchildren die, pipes close, the session stays
alive. Interactive TTY usage (prompts, Ctrl+C routing) is unchanged. - Hook wrapper scripts killed every session on synced multi-machine setups
(GH #719). Each session heal rewrote the~/.claude/hooks/lean-ctx-*
wrappers with the machine-absolute path of the local binary. On a peer
machine sharing that home directory (Dropbox/Syncthing, different username),
every hook then exec'd a non-existent path and each session died silently.
Wrappers now keep their portable$HOME-based form: a heal no longer
overwrites a wrapper whose binary resolves on this machine, the
LEAN_CTX_HOOK_BINARYoverride is emitted verbatim, and all binary
references are shell-quoted (installs under paths with spaces work). The
self-rewrite guard accepts both quoted and legacy unquoted forms, and
lean-ctx doctornow flags wrapper scripts whose binary no longer resolves.
Thanks @tr3lane for the precise follow-up to #708. ledger evictalways reported "Evicted 0/1" (GH #715). Eviction matched
targets by exact string equality, but the ledger stores absolute canonical
paths while users (and lean-ctx's own eviction hints) pass project-relative
paths or basenames — so nothing ever matched. Targets now resolve in three
stages: exact → relative to the project root → unique suffix match, with
ambiguous suffixes reported alongside their candidates instead of silently
doing nothing. The same resolver backsremoveandset_state, eviction
hints print paths that actually resolve, and stored entries are lexically
normalized on load (migrating old backslash entries). The dashboard's
"Evict" button used to only add an exclude overlay — pressure never dropped;
it now performs a real ledger eviction and applies the overlay to the
resolved canonical path.normalize_dashboard_demo_pathemits forward
slashes on Windows. Thanks @ITFinesse for the report.secret_detectionredacted harmless identifiers (GH #718). Keyword
alternations had no word boundaries, so camelCase identifiers
(superuserPassword = "postgres",getStripeSecretKey(),
GITHUB_FEEDBACK_TOKEN) and the "Generic long secret" rule (any 32+ char
value, e.g.confirmRequiredEndpointKeySchema) triggered[REDACTED]in
compressed reads of ordinary source code. Detection keywords now require
word boundaries, right-hand sides that are code identifiers or property
access (no quotes, no digits) are exempt, and placeholder values
(change_me,<your-key>,xxx…,dummy) are skipped. A new subtractive
secret_detection.exclude_patternsconfig lets teams whitelist
project-specific false positives — applied in both redaction layers
(compressed output and audit/tee). Real credentials (sk-ant-…, AWS keys,
quoted high-entropy strings) redact exactly as before, with regression
tests covering every reported repro. Thanks @jackkeller for the
exceptionally precise report.- Dashboard showed "idle" while a session was actively working (GH #717).
Three stacked causes:/api/workspacesdeduplicated workspaces by exact
string match, so Windows path variants of the same root (C:\proj,
C:/proj,c:/proj) rendered duplicate cards where the stale twin sat on
"idle"; unlike/api/sessionit never merged the proxy's live
stats.last_usefreshness; and sessions only flushed to disk every 5
changes, so slow-ticking sessions stayed invisible for the whole batch
window. Workspaces now group under a lexical canonical key (freshest
timestamp wins, counters take the max), the freshest workspace absorbs
stats.last_use, and unsaved changes flush after 60 s (the first change of
a fresh session immediately)./api/agentsuses the same 10-minute
activity threshold as the workspace panel, which now also shows tool calls
per workspace. Thanks @ITFinesse for the report.
Upgrade
lean-ctx update # recommended (auto-downloads + refreshes shell hooks)
cargo install lean-ctx # or
npm update -g lean-ctx-bin # or
brew upgrade lean-ctxNote: After upgrading via cargo/npm/brew, run
lean-ctx setupto refresh shell aliases.lean-ctx updatedoes this automatically.
Full Changelog: v3.9.2...v3.9.2