Users can now set `indexing_params` and `query_params` under `embedding:` in
`global_settings.yml` to pass extra kwargs to the embedder separately for
indexing vs. query — supporting asymmetric retrieval models (Cohere v3,
Voyage, Nvidia NIM, Gemini, nomic-ai code/text models, Snowflake arctic,
etc.).
- `ccc init` auto-populates these from a curated table of known models and
prints the applied defaults; unknown models get a commented-out template
for the accepted keys (`prompt_name` for sentence-transformers;
`input_type`, `dimensions` for litellm).
- Daemon validates the effective params at startup; invalid keys fail fast
with a clear error.
- Backward compat: configs for `nomic-ai/CodeRankEmbed` /
`nomic-ai/nomic-embed-code` that predate this feature keep the previous
hardcoded `prompt_name=query` behavior, and a one-time handshake warning
asks users to make the setting explicit. The warning is suppressible by
any non-None `query_params` (including `{}`).
- `ccc doctor` now tests indexing and query separately so asymmetric
misconfigurations surface independently.
Drops the legacy `shared.query_prompt_name` module variable and
`_QUERY_PROMPT_MODELS` set; the new resolution path is centralized in
`embedder_params.resolve_embedder_params` and the curated defaults live in
`embedder_defaults._DEFAULT_PARAMS`.
Also enables `litellm.drop_params = True` so provider-specific kwargs that
a particular model doesn't accept are silently dropped instead of failing.
The module-level `shared.embedder` global was written by `create_embedder()`
but never read from production code — the embedder flows through the
`EMBEDDER` ContextKey via `context.provide` / `use_context`.
Drop the global along with the two test readers that kept it alive:
- `tests/test_daemon.py` — the `daemon_sock` fixture pre-loaded an embedder
and monkeypatched `dm.create_embedder` to reuse it. That was only useful
as a cross-module cache via the now-dead global; with a session-scoped
fixture, `run_daemon()` loads the embedder once from the saved settings
regardless.
- `tests/test_chunker_registry.py` — removed a stale
`monkeypatch.setattr(_shared, "embedder", stub)` whose comment claimed
CodeChunk.embedding read the global at schema resolution time. It reads
the EMBEDDER ContextKey instead; the stub is already wired through
`Project.create(..., stub, ...)`.
- Move `sentence-transformers` behind `[embeddings-local]` and `[default]`
extras (via `cocoindex[sentence-transformers]`), so `pip install
cocoindex-code` is LiteLLM-only. Closes#117.
- `ccc init` is now interactive when global settings don't exist: pick
provider (sentence-transformers / litellm) and model via a
questionary TUI. New `--litellm-model MODEL` flag skips prompts and
is the non-TTY escape hatch for LiteLLM. Closes#70.
- Change the default sentence-transformers model from
`all-MiniLM-L6-v2` to `Snowflake/snowflake-arctic-embed-xs`
(lighter, better quality for code).
- Generated `global_settings.yml` now includes a `ccc doctor` reminder
and commented-out env-var examples (OPENAI_API_KEY, GEMINI_API_KEY,
ANTHROPIC_API_KEY, VOYAGE_API_KEY).
- Model test during init runs in the daemon via the existing
`DoctorRequest` path; the daemon loads the model once and stays
running, so the user's next `ccc index` starts warm.
- Docker image now installs `cocoindex-code[default]` and pre-caches
the new default model. The `COCOINDEX_CODE_EMBEDDING_MODEL` env var
is no longer documented for Docker; users mount a
`global_settings.yml` or pass `--litellm-model`.
- Extract `check_embedding` + `EmbeddingCheckResult` into `shared.py`;
refactor daemon `_check_model` to delegate. Error messages in doctor
output now include the exception type name (strictly more
informative).
- Tests switch to a lighter `paraphrase-MiniLM-L3-v2` model via a new
`make_test_user_settings()` helper in `conftest.py`, leaving CI
cache costs unchanged.
* refactor: extract daemon path helpers to avoid CLI importing cocoindex
Move daemon_dir, daemon_socket_path, daemon_pid_path, daemon_log_path,
and connection_family from daemon.py into a new lightweight _daemon_paths.py
module. This prevents the CLI client from transitively importing cocoindex
and its heavy dependencies (numpy, torch, etc.) when it only needs path
utilities.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* ci: mark free-threaded Python CI jobs as continue-on-error
tokenizers lacks pre-built wheels for cp314t (free-threaded ABI),
so uv falls back to compiling from Rust source. This source build
is fragile across macOS runner image updates and broke after the
20260406 image bump. Mark 3.14t jobs as continue-on-error since
free-threaded wheel coverage across the ecosystem is still limited.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: enable mypy explicit_package_bases, remove stale type: ignore comments
explicit_package_bases = true resolves module paths from the repo root,
preventing double-discovery of files in tests/ under different module names.
Required for tests/example_toml_chunker.py to be importable as
example_toml_chunker rather than an ambiguous bare module.
Also sets asyncio_mode = auto so pytest-asyncio behaviour is explicit
(default in 1.3.0 is strict).
With explicit_package_bases active, mypy can fully resolve the Response
union in test_daemon.py — the 16 type: ignore[union-attr] and
type: ignore[attr-defined] comments that were suppressing false positives
under the old resolution are now unused and removed.
# Conflicts:
# tests/test_daemon.py
* feat: pluggable chunker registry with settings integration
Improves retrieval precision by letting users split specific file types at
semantic boundaries (e.g. TOML sections, SQL statements) instead of the
default line-window splitter.
## What
- cocoindex_code/chunking.py: public API module exporting ChunkerFn
(Callable alias), CHUNKER_REGISTRY context key, and re-exports of
Chunk/TextPosition from upstream. Single import path for chunker authors.
- tests/example_toml_chunker.py: demo chunker splitting at [section]
headers; excludes [[array_of_tables]] via negative lookahead. Lives in
tests/ to signal it belongs to a separate package, not the core library.
- ProjectSettings.chunkers: new list[ChunkerMapping] field, serialised as
YAML. Each entry maps a file extension to a 'module.path:callable' string.
Users activate chunkers by editing .cocoindex_code/settings.yml — no code
changes required.
- daemon.py: _resolve_chunker_registry resolves ChunkerMapping entries via
importlib at project load time and passes the result to Project.create().
callable() guard gives a clear error at startup rather than a TypeError
per file.
- Project.create(chunker_registry=...): new optional parameter. Injected as
a cocoindex context key (tracked=False) rather than exposed via env internals.
Empty registry by default — zero behavioural delta for existing users.
- indexer.py: process_file checks the registry per file suffix; falls through
to RecursiveSplitter unchanged when no chunker is registered.
## Design decisions
- ChunkerFn returns (language_override, chunks): language_override=None keeps
detect_code_language() result; non-None lets the chunker correct it (e.g.
.sls files starting with #!py).
- tracked=False is consistent with SQLITE_DB, CODEBASE_DIR, and other
non-serialisable context keys. Changing a chunker requires a daemon restart,
which triggers a full re-index anyway.
- _resolve_chunker_registry lives in daemon.py, its only call site, keeping
settings.py as pure schema/IO and chunking.py as pure type definitions.
# Conflicts:
# src/cocoindex_code/daemon.py
# src/cocoindex_code/indexer.py
# src/cocoindex_code/project.py
# tests/test_settings.py
* feat: revisit and simplify CLI-daemon connection model
- CLI-daemon connections are stateless now.
- Also stop caching project settings in memory. Much easier to pick up
new version.
* fix: type annotation
* fix: make sure daemons are brought down cleanly
* chore: be more verbose in pre-commit workflow
* fix: split CI workflow to avoid prek output capture deadlock on Windows
prek --verbose captures subprocess stdout via pipe. On Windows, the 4KB
pipe buffer fills when pytest produces verbose output, causing a deadlock
(pytest blocks writing, prek blocks waiting for exit). Split into two
steps: prek --skip pytest for lint checks, and pytest running directly
for streaming output.
Also:
- Add OSError catch in _pid_alive for Windows edge cases
- Properly shut down daemon thread in test_daemon.py session fixture
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: catch SystemError from os.kill on Windows
On Windows, os.kill(pid, 0) raises SystemError when the target process
has exited but its handle state is in transition (WinError 87). This
corrupts CPython C exception state, causing subsequent built-in calls
like time.monotonic() to also raise SystemError.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: avoid os.kill on Windows due to CPython C exception state corruption
os.kill(pid, 0) on Windows corrupts CPython C-level exception state
even after the raised OSError is caught. This causes subsequent calls to
C built-ins (time.monotonic, time.sleep) to raise SystemError. Use
ctypes OpenProcess instead, which is the proper Win32 API for checking
process existence.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: stablize setting files
* fix: improve daemon readiness detection on Windows
On Windows, os.path.exists() is unreliable for named pipes (\\.\pipe\*).
Replace it with actual connection attempts in _wait_for_daemon and
is_daemon_running. Also increase the default timeout from 10s to 30s
since Windows CI can be slower during model loading.
Additionally, after TerminateProcess on Windows (SIGTERM fallback in
stop_daemon), wait for the old process to fully exit before returning,
so that named pipe handles are released before starting a new daemon.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: assorted improvements for CLI/daemon to be resilient and ergonomic
* tests: add more e2e tests
* fix: resolve path comparison and LMDB cleanup issues on Windows/3.14t
Three root causes fixed:
1. Path resolution mismatch on Windows: init and auto_init_project used
unresolved Path.cwd() but find_parent_with_marker/find_project_root
resolve internally, causing comparison failures and daemon key mismatches.
2. LMDB not released on remove_project: The daemon ProjectRegistry
dropped the Project from dicts without closing its SQLite connection or
forcing GC of the Rust LMDB environment. On free-threaded Python (3.14t)
and Windows, deferred GC kept the LMDB open, causing environment already
open errors and PermissionErrors when deleting db files.
3. Silent connection close on streaming errors: When update_index async
iteration failed in the daemon, the connection was closed without sending
an ErrorResponse, causing the client to get an unhelpful EOFError.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: break LMDB reference chain explicitly and resolve cwd in path filter
On 3.14t (free-threaded Python), gc.collect() alone does not release the
Rust LMDB environment because deferred reference counting keeps
core.Environment alive through App._core_env_app, ContextProvider._core_env,
and Environment._core_env. Explicitly null these internal references in
Project.close() before gc.collect() so the Rust object is freed promptly.
Also resolve Path.cwd() in resolve_default_path() — on Windows, the
unresolved cwd did not match the resolved project_root, causing
relative_to() to fail and the subdirectory path filter to be skipped.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use double gc.collect() for layered deferred refcount on 3.14t
Revert fragile internal attribute clearing. On free-threaded Python, the
first gc.collect() frees Python wrappers whose Rust Drop implementations
issue further deferred Py_DECREF calls on core.Environment; a second
gc.collect() flushes those and actually drops the LMDB handle.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add sleep in tests for LMDB release on free-threaded Python
On 3.14t, deferred reference counting means the Rust LMDB environment
is not released immediately after remove_project + gc.collect(). Add a
1-second sleep in the two tests that reset and re-index, giving the
runtime time to process pending deferred Py_DECREF calls.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: restart daemon after reset in tests, increase daemon wait timeout
Instead of sleeping, restart the daemon after reset in the two tests
that re-index after removing databases. This reliably releases the LMDB
environment on all platforms including free-threaded Python (3.14t).
Also increase _wait_for_daemon timeout from 5s to 10s — Windows CI
runners occasionally need longer to start the daemon subprocess.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>