Skip to content

v3.9.2

Latest

Choose a tag to compare

@github-actions github-actions released this 07 Jul 06:39

Added

  • Unified distribution, Phase 2 (GH #724/#726): self-service addon
    publishing + hosted installs.
    New lean-ctx addon publish --namespace <ns> wraps the authoring lean-ctx-addon.toml verbatim into a signed
    kind=addon context 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, review publishes disclosed; --check runs 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
    import kind=addon packs (wrong trust chain, use addon add). The
    bundled registries are now generated snapshots: gen_registry validates,
    sorts and canonicalizes rust/data/{addon,grammar}_registry.json;
    CI + preflight fail on drift (deterministic, timestamp-free, #498).
  • Unified distribution, Phase 1 (GH #724/#725): managed addon binaries +
    the kind package taxonomy.
    .ctxpkg manifests gain an optional kind
    field (context | skills | addon | grammar; default context,
    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 add downloads the prebuilt
    binary for the current platform into the managed bin dir
    (<data_dir>/addons/bin/<name>/<version>/, never PATH), 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 —
    zero PATH interaction, tamper ⇒ spawn refused, addons.policy = locked
    blocks the fetch before any network I/O. New lean-ctx addon update <name>
    installs side-by-side, health-gates, then prunes; addon remove deletes the
    managed binaries; doctor verifies 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), covering azure/,
    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 in x-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 via cost_source.
  • Index-time include/exclude filters (GH #735). The retrieval corpus is
    now declared explicitly instead of abusing .gitignore for retrieval
    policy: new [index] config (exclude, include, respect_gitignore)
    plus per-run CLI overlays on every lean-ctx index build command —
    repeatable --exclude <glob> / --include <glob> (both --flag value and
    --flag=value forms) 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
    --exclude appends to config, --include replaces 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_mode enters the ctx_read mode chain (explicit arg > policy
    pack > persona > profile/auto; "auto" = no opinion), compressor compacts
    ctx_url_read flowing-text modes through the extension registry
    (researchmarkdown, support/lead-genprose; extractive
    facts/quotes/links stay verbatim to protect citations), chunker
    makes token-budget truncation cut on chunk boundaries (paragraphs / line
    windows) instead of mid-sentence, intent_taxonomy lands as the
    contract-promised persona block (PERSONA: / INTENTS: / DEFAULTS:) in
    the MCP session instructions, and sensitivity_floor folds into
    [sensitivity] enforcement (Config::sensitivity_effective) — a floor
    above public enables enforcement out of the box and can only tighten an
    explicit config (LEAN_CTX_SENSITIVITY=off stays the kill switch). The
    coding default 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 MB lean-ctx process; 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-era deepseek-v3 list 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
    absorbs usage.cost plus cost_details.upstream_inference_cost (BYOK) as
    the authoritative billed charge, which beats every table estimate in the
    meter, the policy gate and usage_events; (2) live prices for all
    models
    — a new core::gain::live_pricing module fetches the public
    OpenRouter models catalog (320 models) with a 24h disk cache, atomic swap,
    background refresh at proxy/gateway/dashboard startup, slug normalization
    (vendor prefixes, date suffixes, :free/:extended variants) and a
    LEAN_CTX_LIVE_PRICING=off kill switch, slotted between exact embedded
    matches and the heuristic in ModelPricing::quote; (3) cost provenance
    — every usage event now records cost_source
    (provider|live|list|heuristic|shadow), the admin console marks
    measured vs estimated spend (✓/
    badges, KPI foot line, CSV columns
    measured_requests/estimated_requests), lean-ctx spend prints ✓/*
    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 exports CLAUDE_PROJECT_DIR to its hook child processes for
    Claude-compat — and lean-ctx's guard-host detection took that variable as
    proof the host is Claude Code, so read_redirect = auto switched 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 buffered lean-ctx -c path inherited the host's stdin, so
    a command that falls back to reading stdin — e.g. rg left 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 orphaned rg kept 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_BINARY override 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 doctor now flags wrapper scripts whose binary no longer resolves.
    Thanks @tr3lane for the precise follow-up to #708.
  • ledger evict always 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 backs remove and set_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_path emits forward
    slashes on Windows. Thanks @ITFinesse for the report.
  • secret_detection redacted 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_patterns config 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/workspaces deduplicated 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/session it never merged the proxy's live
    stats.last_use freshness; 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/agents uses 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-ctx

Note: After upgrading via cargo/npm/brew, run lean-ctx setup to refresh shell aliases. lean-ctx update does this automatically.

Full Changelog: v3.9.2...v3.9.2