This is the fourth changelog since the v1.0 launch, covering two releases (1.0.26 and 1.1.0). It leads with new APIs: a preview that runs on real pipelines and writes nothing, coco.LogicDep for logic dependencies decided per call, max_in_flight and a per-batch rate limiter for batched functions, and a component limit that holds across the whole hierarchy. A faster commit path for moving target states between components follows, then fixes, led by an audit of how every Python connector builds its keys, and internal improvements.
CocoIndex builds fresh, structured context for AI from sources such as PDFs, codebases, emails, screenshots, and meeting notes, and keeps it up to date with an incremental engine. The source is on GitHub.
Why this release cycle matters
- Preview writes nothing.
cocoindex update --previewnow handles rows under table targets, so it runs on most pipelines, and it never stores a memo or writes committed state, so it can’t make the next real update skip work. - Per-call logic dependencies.
coco.LogicDepandcoco.use_logic_dep()let one call record a dependency, such as the version of the parser it used. Bumping that dependency re-runs only the calls that recorded it and the memoized callers above them. - Bounded batching. A batched function takes
max_in_flight, which caps how many batches run at once, andrate_limiter, which takes one token per batch instead of per call. - A component limit that holds across the hierarchy.
max_inflight_componentsnow bounds every running component: a parent lends its slot to one child at a time, and live components share the pool fairly. - Faster commits when target states move. Moving 5,000 target states from one component to another in one commit took 243 seconds and now takes 1 to 2.
- Keys that hold in the edge cases. CocoIndex has identified each object by one key from the start. An audit of every Python connector closed the cases where a connector’s key construction broke that rule: two objects sharing one key, one object’s key changing between runs, or a delete missing its object.
Upgrade notes
Check these before upgrading. Each item names the release it applies from. The 1.1.0 items are what make this a minor version rather than another patch.
Deprecated, still working (with a DeprecationWarning):
- Neo4j and FalkorDB table and relation APIs (v1.1.0). Each has a replacement with the same parameters:
table_target→node_target,declare_table_target→declare_node_target,mount_table_target→mount_node_target(which returns aNodeTarget),relation_target→relationship_target,declare_relation_target→declare_relationship_target,mount_relation_target→mount_relationship_target, andRelationTarget.declare_relation→RelationshipTarget.declare_relationship. The deprecated calls keep the keys they had. Switching an existing table takes two updates, listed under node and relationship APIs; skipping the first deletes the table’s nodes or edges. - Kafka
topic_as_mapwith a list of topics (v1.1.0). The list form still works with its old keys. With one topic, pass it as a string; the keys don’t change. With several, use one feed and one consumer per topic, each under its own subpath. That changes the item keys, so follow the replay steps in the Kafka docs. - Postgres
TableTarget(provider, table_schema)(v1.1.0). Pass the table name:TableTarget(provider, table_name, table_schema). The keys don’t change. - Connector sink contracts (v1.0.26).
from_fn_with_childrenandfrom_async_fn_with_childrenare replaced byTargetActionSink.from_fn(fn, child_handler=...)andfrom_async_fn(...). The old forms still run but don’t support preview. coco.logic_tracked(v1.0.26) is a deprecated alias ofcoco.logic_propagated_to_callers.
Behavior changes:
- Object-store
prefixnames a folder (v1.1.0). For S3, Azure Blob and OCI,prefix="docs"now lists onlydocs/...;docsa.mdanddocs-old/xare no longer included. Target states built from objects that drop out of the listing are removed on the next update. To select objects by how their names start, usepath_matcher. - Preview leaves no trace (v1.0.26). A preview no longer stores, deletes or refreshes memos and no longer writes committed state.
FilePathcomparisons (v1.1.0).FilePaths of different types no longer compare equal, and ordering them raisesTypeError.
New errors:
DirTargetnames that are absolute or leave the directory (v1.1.0).- A new zvec collection with an
Any,object, union orbyteskey column (v1.1.0). - LanceDB key column types that deletes can’t filter on, such as dates, timestamps,
float16and lists (v1.1.0). - Postgres vector indexes whose names truncate to the same 63 bytes (v1.1.0).
- The Valkey field name
vector(v1.1.0). @coco.fn(batching=...)and@coco.fn(runner=...)are type errors; use@coco.fn.as_async. A lonemax_batch_size=on a sync function is rejected instead of ignored (v1.0.26).- A class the state reader can’t load fails at the first write, with a hint to use
coco.unpickle_safe, instead of on the next run (v1.1.0).
One-time effects on the first update after upgrading:
- Google Drive components get new keys and are reprocessed (v1.1.0).
- Objects that shared a key become separate items (v1.1.0).
- SurrealDB relations declared without an explicit
idare re-created under a new record ID (v1.1.0). DirTargetfiles declared under a name that now normalizes, such as./a.txt, are written once under the new name (v1.1.0).
To remove duplicate graph edges left by earlier versions, run one update with --full-reprocess (v1.1.0).
Memo keys and fingerprints now depend only on values, so a structure that references one object twice fingerprints the same as one holding equal copies. For such values, on the first update after upgrading: memoized work may run once more; generate_id, generate_uuid, IdGenerator and UuidGenerator may return new IDs, which changes row keys built from them; and rows tracked by fingerprint_object are written again. See the fix.
Requirements and the Rust SDK:
- FalkorDB needs
falkordb1.6.0 or later (v1.0.26). - The Rust crates need Rust 1.91.1 or later (v1.0.26).
- Rust SDK: the Neo4j and FalkorDB table and relation target functions take
&ContextKey<Graph>instead of&Graph(v1.0.26). - litellm 1.107.0 is excluded from every
litellmrequirement (v1.0.26); see Security and dependencies.
New APIs and capabilities
Preview runs on real pipelines and writes nothing (v1.0.26)
A preview lists what an update would change in the targets, without changing them. Before 1.0.26 it handled only target states with nothing nested under them. Most real pipelines nest: rows under a Postgres table, points under a Qdrant collection, files under a directory target, where the table or directory is a container and the rows or files are its children. As soon as a preview reached a container with children it stopped with an error, preview currently supports flat/leaf target actions only. It was also unsafe: a preview could store, delete or refresh memos, and write committed state, so the next real update could skip work it had never done (#2510).
Both are fixed. Preview now lists the planned actions for target states under a container, reads memos without changing them, and writes no committed state. It also runs no deletes for live components. One exception is deliberate: generate_id and IdGenerator reserve IDs in a preview as in an update, because IDs are opaque and the gap costs nothing; the IDs a preview prints for new items are therefore not the ones the next update assigns.
cocoindex update --preview path/to/app.py
handle = app.update(preview=True)
actions = await handle.result() # one entry per planned target action; nothing was applied
The CLI prints one line per planned action, or No target actions planned. A container’s action is listed in every preview, even when its tracking record (what CocoIndex stored about it after the last run) is unchanged, because equal tracking records do not guarantee that applying the action has no external effect. A preview also applies no actions to a live map, so consumers of the map see it empty; preview the producers’ own targets instead.
For connector authors, a container’s child handler is now a pure function of its action. TargetActionSink.from_fn(fn, child_handler=...) and from_async_fn(...) replace from_fn_with_children and from_async_fn_with_children: fn(context_provider, actions) only applies the actions, and child_handler(context_provider, action) builds the handler for the container’s children, before fn applies them in an update and in place of fn in a preview. The old forms still run with a DeprecationWarning but can’t be previewed.
Logic tracking beyond the decorator (v1.0.26 to v1.1.0)
@coco.fn fingerprints a function’s logic so that editing the function invalidates the memos that depend on it. Two cases a decorator parameter can’t express now have their own tools.
coco.LogicDep and coco.use_logic_dep(): a dependency decided per call (v1.1.0)
deps= on the decorator describes the whole function: every call depends on the value. Some dependencies are decided per call. A tree walk’s visit_file depends on the parser for that file’s language, and a change to one parser should re-run the files it parsed, not every file the walk saw. coco.LogicDep declares such a value once, at module import, and coco.use_logic_dep() records it as part of the current call’s logic (#2590):
PARSER_DEPS = {
name: coco.LogicDep(("parser", name, version))
for name, version in PARSER_VERSIONS.items()
}
@coco.fn(logic_tracking="self", version=1)
async def visit_file(file: File) -> None:
language = detect_language(file.path)
if (dep := PARSER_DEPS.get(language)) is not None:
coco.use_logic_dep(dep) # this call's logic also depends on that parser
...
In the sample, visit_file is tracked as "self", so the functions it calls are not part of its fingerprint; the parser dependency is recorded whatever the mode, because it describes the call rather than the function’s code. A recorded dependency travels like the function’s own fingerprint: into the entry of a memoized caller, onward through "full" callers, and from a mounted component into every ancestor component’s entry. When a later release constructs the LogicDep with a new value, only the entries that recorded the old one are invalidated. In the example, bumping the Python parser’s version re-runs the Python files and the directories above them, and every other subtree is reused.
Two rules follow from how entries are validated:
- Build every
LogicDepat module import, before any component runs. One constructed on first use registers too late, and one constructed per call is released too early; in both cases memoization silently stops working rather than returning stale data. Constructing one inside a component context raises. - Adopting
use_logic_depin a function whose memoized callers already stored entries does not re-store those entries. Ship the adoption with aversionbump on those callers.
The same PR fixed a related bug: a function-memo hit lost the logic dependencies its entry had recorded, so a caller re-stored on that hit missed later changes to deeper callees.
coco.logic_propagated_to_callers(): callbacks through a "self" boundary (v1.0.26)
logic_tracking="self" hides the logic of everything a function calls, including callbacks it received as arguments. A library whose accept is tracked as "self" therefore hides the user’s visit_file: an edit to it invalidates nothing, even though it is the user’s input and not the library’s implementation detail. coco.logic_propagated_to_callers(fn) closes the gap (#2483, #2529). It is library-side; the user only puts @coco.fn on the callback.
# Library
@coco.fn(memo=True, logic_tracking="self", version=1)
async def accept(file: File, visit_file: Callable[[File], Awaitable[None]]) -> None:
await visit_file(file)
@coco.fn
async def walk(repo: Repo, visitor: RepoVisitor) -> None:
visit_file = coco.logic_propagated_to_callers(visitor.visit_file) # walk owns the callback
for file in repo.files():
await coco.use_mount(coco.component_subpath(file.path), accept, file, visit_file)
The function that calls it is the callback’s owner. The wrapper runs fn as usual; afterwards, fn’s logic is recorded in every memo entry between the call site and the owner, whatever their logic_tracking. When the user edits visit_file, every accept memo entry misses. Two wrappers of the same function produce the same key. coco.logic_tracked, its name before 1.0.26 shipped, remains as a deprecated alias and works with a DeprecationWarning.
Batching: a bound on in-flight batches (v1.1.0)
A batched function starts a batch in three situations. The first call on an idle queue runs at once, on its own. A batch that fills to max_batch_size starts as soon as it fills. And when a batch finishes, the next pending batch may start. A call that arrives while batches are running joins the open partial batch and waits for one of those triggers. By default the partial batch starts only once no batch is running at all, so one slow batch holds back the partial batch behind it.
@coco.fn.as_async(batching=True, ...) now takes max_in_flight, which bounds how many batches run at once and changes when a batch may start (#2588). With max_in_flight=n, a batch that fills while n batches are running waits in order, and whenever a batch finishes the next pending batch, full or partial, starts as long as fewer than n are running. A partial batch no longer waits for the slowest batch, only for a free slot.
@coco.fn.as_async(batching=True, max_batch_size=32, max_in_flight=4)
async def embed(texts: list[str]) -> list[list[float]]:
return await client.embed(texts)
Batching: one rate-limit token per batch (v1.1.0)
When each batch is one request against a quota, the same decorator takes rate_limiter, the quota’s RateLimiter (#2589). Every batch takes one token before it runs, so a token pays for one batch rather than one call. No call runs on its own: the first call waits for a token like any other and goes out with everything that arrived meanwhile. Under a scarce quota, calls that arrive while the batcher waits for a token join the next batch; with tokens to spare, a batch goes out at once and the next one fills during its round trip. For requests the body makes beyond the batch’s own, such as a retry or a per-item fallback, the body should take its own tokens from the same limiter. For a limiter kept on the instance, pass a callable that receives self:
class Client:
def __init__(self, limiter: RateLimiter) -> None:
self._limiter = limiter
@coco.fn.as_async(
batching=True, max_batch_size=100, rate_limiter=lambda self: self._limiter
)
async def fetch(self, ids: list[str]) -> list[Item]:
return await self._api.fetch_many(ids) # one request, one token
Slot lending: a component limit that holds across the hierarchy (v1.0.26)
max_inflight_components (or COCOINDEX_MAX_INFLIGHT_COMPONENTS, default 1024) caps how many processing components run at once. The difficulty with any such cap is a hierarchy: a parent that mounts children and waits for them holds a slot while it waits, so with a small cap the slots fill with waiting parents and the children they wait for can never start. Before 1.0.26 the engine avoided that deadlock by releasing a parent’s slot when it mounted its first child, which left everything after that first mount unbounded. A tree walk that read files faster than the downstream stage drained them kept every file component in flight, each holding its contents; one indexer was killed for memory with about fifteen thousand file components in flight while the application’s own limit allowed 256 (#2486).
1.0.26 replaces that with slot lending. A waiting parent keeps its slot and lends it to one child at a time: the first child runs on the parent’s slot, further children take free slots from the pool and wait while it is empty, and when the lent child finishes the slot passes to the parent’s next waiting child. A blocked parent therefore always has one child that can run, so a hierarchy of any depth completes even with max_inflight_components=1, and the components executing at once never exceed the cap. Counting the parents waiting on children, the components in flight are bounded by the cap times the depth of the hierarchy.
Three related rules:
- A component that awaits a function call, batched or not, keeps its slot; only mounting a child lends it.
- Live components borrow the slot of the component that mounted them while it runs, then take slots from the pool.
- Component deletion draws on the same pool.
Fair sharing between live components (v1.1.0)
Live components update on their own schedules, and one can have far more work queued than another: a new component’s first full index next to a refresh of a few changed items. A freed slot used to go to the oldest waiting component, so the component with the large backlog held back every other live component, and the small refresh waited behind the whole backlog (#2624).
The pool is now shared fairly between independent updates: an App.update(), one cycle of a live component, or one item update of mount_each() over a live map. A slot that frees up goes to whichever waiting update holds the fewest slots, then to the earliest waiter among equals. No slot is reserved, so an update that runs alone still gets the whole pool. In the test that pins this, a small refresh running beside a 400-leaf first index used to finish after 283 to 292 of the large leaves and now finishes after 80 to 82.
Smaller additions (v1.0.26)
- Nix gets syntax-aware splitting and structural code patterns (#2434, by @mulatta).
regex_required_literalsis public (#2525).- Error cause chains. Internal errors show their full cause chain, for example
db error: ERROR: deadlock detectedinstead ofdb error(#2489). - The optional
applicationtelemetry tag is documented (#2539). ConnectionFactory.close(). The Neo4j, FalkorDB and SurrealDB factories keep one connection per event loop, and the Neo4j and FalkorDB ones gainclose(), which SurrealDB’s already had. FalkorDB needsfalkordb1.6.0 or later (#2510).
Rust SDK: memo frames, mount keys, and root-provider cleanup (v1.0.26)
- Memo frames. Memoized calls join the enclosing caller’s frame (#2503), so a memoized caller no longer returns stale values or deletes its children’s outputs on a cache hit. Mounting inside
#[function(memo)]is rejected, as in Python. - Mount keys.
use_mount!andmount_each!key by memo identity, so touching one file no longer re-runs every component. Connectors use one sink per handler instead of one apply per row. - Root-provider cleanup.
App::drop_stateand normal updates remove targets declared through root providers (directory files and Postgres, SQLite, LanceDB and Qdrant tables, among others) when they are no longer declared (#2512). Breaking: the Neo4j and FalkorDB table and relation target functions take&ContextKey<Graph>instead of&Graph. - CI coverage. Every SDK feature is type-checked and its hermetic feature tests run (#2523).
Performance
Moving target states between components (v1.1.0)
When a component takes target states that another component owns, the commit did work proportional to the old owner’s whole tracking record for every moved target. The pending-retry check decoded the record once per moved target, and the first commit phase decoded it again, scanned every remaining item, and re-encoded it. Moving N targets from one owner was quadratic in N (#2621, from a fix by @badmonster0).
Each preempted owner’s record is now decoded once per commit and shared by both passes, items are taken out of it in place, and each modified record is encoded once for writing. Writes are unchanged, since there was already one upsert per old owner, and they still apply inside the same transaction as before.
| Targets moved in one commit | Before | After |
|---|---|---|
| 1,000 | 8.7 s | 0.29 s |
| 2,500 | 67.2 s | 0.65 s |
| 5,000 | 242.7 s | 1.0 to 1.8 s |
Debug build, LMDB, same machine for both columns. The previous changelog’s commit-path work covered the per-row cost of a large table; this removes the per-owner cost when ownership moves.
Fixes
Connector keys: collisions and lost writes (v1.1.0)
Every source item and every target state has a key. CocoIndex uses the key to match an object with its previous run and to decide what to create, update or delete. That has been the rule from the first release; what 1.1.0 changes is the edge cases where a connector built keys in a way that broke it. A Google Drive fix (#2485, by @Sujit-1509) started an audit of how every Python connector builds its keys. The audit found four shapes of bug, most of them edge cases, and 1.1.0 closes each one.
Two keys you choose can also reach one object, such as overlapping directories or two ContextKeys for one database. CocoIndex can’t detect that, so it stays your responsibility; the docs now explain where it can happen.
Two different objects shared one key
One overwrote or deleted the other.
- Google Drive: files with the same name, even in one folder. Items are now keyed by file ID (#2485).
- S3, Azure Blob and OCI: object keys that differ only in a doubled or leading
/, a./segment, or\versus/; withprefix="docs",docs/a.mdanddocsa.md; and paths in different buckets, which compared equal (#2601). - Kafka: the same message key on two topics of one
topic_as_mapfeed. A tombstone on one topic deleted the other topic’s outputs (#2597). - Neo4j and FalkorDB: a node label and a relationship type with the same name, and derived edge IDs of different endpoints (
1versus"1", IDs containing_). The fix comes as the new node and relationship target APIs below (#2612). - SurrealDB: derived relation IDs of different endpoints, such as
doc:'chunk_1'anddoc_chunk:1linked to the same tag (#2599). - zvec:
1and"1"in anAny,objector union key column became one document (#2608, #2613). - Postgres: two vector indexes on one table whose names Postgres truncates to the same 63 bytes, so only one of them could exist (#2603, #2615).
- Valkey: a payload key or
FieldDefnamedvector, the hash field that holds the document’s vector (#2606).
One object’s key changed between runs
The delete for the old key removed what the new key had just written.
- localfs: a case-only rename on macOS and Windows, or
./a.txtchanged toa.txt. The file stayed missing until its content changed (#2598). - Qdrant: a UUID point ID written in another text form: upper case, no hyphens, or a URN (#2602).
- Kafka target: a message key that switched between
strandbytes. Its tombstone erased the record on a compacted topic (#2597). - Neo4j and FalkorDB: a renamed vector index (#2611).
A delete missed its object
Stale data stayed behind.
- LanceDB: every row delete failed for a key column that needs quoting (
doc-id), or that holds binary, fixed-size binary or the int64 minimum (#2600). - Doris: keys containing
\. Each update appended a duplicate row, removed items stayed, and a trailing\broke the statement (#2605). - Neo4j and FalkorDB: an edge with an explicit ID whose endpoints changed. Neo4j kept the old edge beside the new one, and FalkorDB failed with a unique constraint violation (#2611). Dropping a node table, by un-declaring it or with
cocoindex drop, left its vector indexes (#2618).
A change went undetected
The item was not reprocessed.
- S3, Azure Blob, OCI and Google Drive binary files: an overwrite that kept the modification time (S3 times are whole seconds). An equal time is now checked against the ETag or MD5 (#2601).
- Doris: a changed vector or inverted index parameter (
index_type,metric_type, dimension, parser). It now rebuilds the table (#2605).
Graph targets: node and relationship APIs
The Neo4j and FalkorDB fix for a label and a relationship type sharing a name, and for ambiguous derived edge IDs, is a new pair of target APIs with the same parameters as the old ones (#2612):
# Before (deprecated; still works with a DeprecationWarning)
entities = await neo4j.mount_table_target(KG_DB, "Entity", entity_schema, primary_key="value")
edges = await neo4j.mount_relation_target(
KG_DB, "RELATIONSHIP", entities, entities, rel_schema, primary_key="id"
)
edges.declare_relation(
from_id="CocoIndex", to_id="Neo4j", record=RelationshipRow(id="rel-1", predicate="writes_to")
)
# After
entities = await neo4j.mount_node_target(KG_DB, "Entity", entity_schema, primary_key="value")
edges = await neo4j.mount_relationship_target(
KG_DB, "RELATIONSHIP", entities, entities, rel_schema, primary_key="id"
)
edges.declare_relationship(
from_id="CocoIndex", to_id="Neo4j", record=RelationshipRow(id="rel-1", predicate="writes_to")
)
On the new APIs, Neo4j labels and relationship types can’t contain __ or end with _, because constraint and index names join the label to a property name with __; on both connectors, derived edge IDs need int or str endpoint IDs.
Switching an existing table keeps its nodes, edges, constraints and indexes, but takes two updates:
- Add
managed_by="user"to each deprecated declaration and run an update. Removing those declarations now deletes nothing. - Replace each call with its replacement, without
managed_by="user", and run another update.
Without the first step, removing a deprecated declaration destroys its table, possibly after the new table has written it. The steps are in the Neo4j and FalkorDB docs under “Switching an existing table”.
Documentation A target state's identity is its key. How CocoIndex matches a declared target with its previous run, and where two keys you choose can reach one physical object.Memo fingerprints depend only on values (v1.1.0)
A memo key or fingerprint used to depend on object identity in two ways. A structure that referenced one object twice fingerprinted differently from one holding two equal copies. And a temporary object freed during fingerprinting could produce a wrong memo hit, rather than a harmless recomputation, once Python reused its ID for another temporary value. Both are fixed by canonicalizing shared values independently and tracking only the active recursion path for cycle detection (#2411, by @hatemadi15, closing #2391). The one-time effect on fingerprints is in the upgrade notes.
Hangs and shutdown (v1.0.26 to v1.1.0)
- GPU pool. A GPU released to a cancelled
acquiregoes back to the pool (#2595). Before, every lateracquireof that GPU hung. - Cancelled source listings. Cancelling a pending read of a synchronous source listing (localfs, Google Drive) no longer hangs
asyncio.run()at shutdown (#2619, by @Sujit-1509). - Resources released at stop.
coco.stop()andcoco.stop_blocking()release the lifespan-provided resources that registered apps held, instead of keeping them until each app’s next update (#2425, by @hatemadi15). - Free-threaded Python 3.14: the background event loop no longer starts twice, and an environment can be reopened without
close()after a localfs-reading app (#2490, #2499).
LiteLLM embedder: media-like text and refused requests (v1.0.26)
LiteLLMEmbedder embeds text starting with data:…;base64,, gs:// or files/ as text rather than treating it as a file reference. A request litellm refuses to build no longer retries until the ten-minute deadline; the batch is split to isolate the bad text, the same mechanism RetryWithSmallerBatch introduced last cycle (#2532).
Other fixes
- Graph drop (v1.0.26).
cocoindex dropand un-declaring a table delete the Neo4j and FalkorDB nodes and relationships the app wrote, and a primary-key change rebuilds the table instead of leaving old-key nodes behind (#2498). - Transient errors (v1.0.26). A Neo4j apply batch retries on transient errors such as
DeadlockDetectedinstead of failing the component (#2491). code_matchprefilters (v1.0.26). Regex prefilters no longer drop nested alternatives, which made searches miss matches (#2525).- C# file-scoped namespaces (v1.1.0). A file-scoped namespace (
namespace X;) shows in therender_matchcontext frames of the declarations after it (#2629). VectorSchema(dtype=np.float32)(v1.1.0). A schema whose dtype is the classnp.float32rather than a dtype instance now reloads on the second run. Other classes the state reader can’t load now fail when first written, with a hint to usecoco.unpickle_safe(#2536, by @rtmalikian).- Passwords out of
repr(v1.1.0). Snowflake and Doris connection configs leave the password out of theirrepr(#2620, by @jerryqu-coco). - OCI Object Storage (v1.1.0). Zero-length reads, and partial reads of empty objects, return empty bytes instead of failing with HTTP 416 (#2466, by @pranav440).
- Reopening an open
db_path(v1.1.0). TheRuntimeErrornow names the path andEnvironment.close(), instead of a misleading message about different options (#2561, by @Siddesh-bype).
Internal improvements
Documentation
- Key identity. The docs explain that a target state’s identity is its key, and where two keys can reach one object: target states, contexts,
mount_each(), and Postgres, SQLite and BigQuery tables. Mounting one component path twice in one run was already an error; its message now says how to fix it (#2609, #2610). - Examples that failed when run as written.
- The
SeparatorSplitterusage example (#2476, by @March-7). - The localfs lifespan example, which named a
coco.EnvBuilderthat doesn’t exist (#2397, by @Siddesh-bype). - The
get_contextexample, which called the asynccoco.default_env()as if it were synchronous, and the App guide’s component path tree (#2416, by @hardness1020).
- The
drop_appsemantics. The docs now say whydrop_appclears the sub-database instead of removing it (#2431, by @hardness1020).- HTTP clients. The docs standardize on httpx and explain why Doris Stream Load keeps aiohttp (#2626).
- Valkey
client_info_tagdefaults to"cocoindex"(#2480, by @Jonathan-Improving). - Agent skills install with uv rather than pip (#2546, #2552), a release playbook was added (#2627), and the Rust guidelines distilled from review comments are linked from
AGENTS.md(#2500).
Security and dependencies
- litellm 1.107.0 was published to PyPI with a credential stealer and a backdoor. It is excluded from every
litellmrequirement: thelitellm,entity_resolution_llmandallextras, and the examples. The locked version, 1.96.2, is unaffected (#2576). - Rust dependencies. The legacy rustls 0.21 stack is dropped from the S3 client, which cleared the rustls-webpki advisories (#2544);
aws-smithy-json0.62.7 is taken for GHSA-8ffr-xgwf-xj56, which raises the minimum Rust version to 1.91.1 (#2542); rustls moves to 0.23.46 for GHSA-2mjx-qc3c-rqvc (#2543). - Dependabot. Security updates are grouped, one PR per ecosystem (#2591).
CI
The main change is that each landed commit now gets exactly one full run: a merge queue with tiered checks, a gate job that the required status checks point at, a skip for drafts, and support for stacked PRs (#2584). Around it:
- Job setup is shared through a composite action (#2578), sccache objects stay on local disk and are saved once per leg (#2583), and jobs free only the disk space they need (#2579).
- The Rust SDK checks run only for SDK changes, plus daily (#2622); CodeQL scans
maindaily instead of every pull request (#2596). - Testcontainers images are pre-pulled before the first test, with a stall timeout and retries (#2551, #2556);
ruff checkand clippy are gated in pre-commit (#2540); root-relative/docs/links are checked against the build output (#2557, #2559).
Summary
This cycle adds a preview that lists what an update would do to the targets and leaves nothing behind, and a way to record a logic dependency for one call so that bumping it re-runs only the calls that used it and their callers. A batched function can cap how many batches run at once and pay one rate-limit token per batch, and the component limit holds across a hierarchy of any depth and is shared fairly between live components. Moving target states between components is no longer quadratic. Underneath, the loose ends in how connectors build keys were closed, so one key per object holds in the edge cases too, memo fingerprints depend only on values, a cancelled GPU acquire no longer poisons the pool, and shutdown no longer hangs on a cancelled source listing.
For the complete list of changes, see the GitHub releases. If CocoIndex is useful to you, consider starring the repository.
Thanks to the community
Thanks to everyone who contributed this cycle, including nine first-time contributors.
@Sujit-1509
Thanks @Sujit-1509 for keying Google Drive items by file ID, the fix that prompted the key audit across every Python connector, and for keeping a cancelled source listing from hanging shutdown.
@hatemadi15
Thanks @hatemadi15, a first-time contributor, for making memo fingerprints depend only on values, which also closed a wrong-memo-hit bug from reused object IDs, and for releasing retained app resources at coco.stop().
@hardness1020
@Siddesh-bype
Thanks @Siddesh-bype, a first-time contributor, for replacing the nonexistent coco.EnvBuilder with coco.EnvironmentBuilder in the localfs guide and for making the error on reopening an open db_path actionable.
@mulatta
Thanks @mulatta, a first-time contributor, for adding Nix syntax support to code splitting and structural code patterns.
@rtmalikian
Thanks @rtmalikian, a first-time contributor, for failing fast at write time for classes the state reader rejects, so the error arrives on the run that caused it rather than the next one.
@pranav440
Thanks @pranav440, a first-time contributor, for handling zero-length and empty-object range reads in OCI Object Storage.
@jerryqu-coco
Thanks @jerryqu-coco, a first-time contributor, for leaving Snowflake and Doris passwords out of config repr.
@March-7
Thanks @March-7, a first-time contributor, for fixing the SeparatorSplitter usage example so it runs as written.
@Jonathan-Improving
Thanks @Jonathan-Improving, a first-time contributor, for documenting the Valkey client_info_tag default.
@Avicennasis
Thanks @Avicennasis, a first-time contributor, for fixing a typo in the conversation_to_knowledge extraction prompt.
Build fresh context for your agents.
CocoIndex keeps derived data in sync with its sources. Only what changed re-runs.