---
title: "CocoIndex Changelog 1.0.26 - 1.1.0"
description: "CocoIndex 1.0.26 and 1.1.0: a preview that writes nothing, per-call logic deps, bounded batching and component concurrency, and a connector key audit."
last_updated: 2026-11-02
doc_version: "2026-11-02"
canonical: https://cocoindex.io/blogs/changelog-1026-110/
---
# CocoIndex Changelog 1.0.26 - 1.1.0

> CocoIndex 1.0.26 and 1.1.0: a preview that writes nothing, per-call logic deps, bounded batching and component concurrency, and a connector key audit.

Published: 2026-11-02 · Canonical: https://cocoindex.io/blogs/changelog-1026-110/

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](https://cocoindex.io/docs/programming_guide/processing_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](https://cocoindex.io/docs/programming_guide/core_concepts/). The source is on [GitHub](https://github.com/cocoindex-io/cocoindex).

## Why this release cycle matters

- **Preview writes nothing.** [`cocoindex update --preview`](#preview-runs-on-real-pipelines-and-writes-nothing-v1026) now 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.LogicDep` and `coco.use_logic_dep()`](#logic-tracking-beyond-the-decorator-v1026-to-v110) 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`](#batching-a-bound-on-in-flight-batches-v110), which caps how many batches run at once, and [`rate_limiter`](#batching-one-rate-limit-token-per-batch-v110), which takes one token per batch instead of per call.
- **A component limit that holds across the hierarchy.** [`max_inflight_components`](#slot-lending-a-component-limit-that-holds-across-the-hierarchy-v1026) now bounds every running component: a parent lends its slot to one child at a time, and live components [share the pool fairly](#fair-sharing-between-live-components-v110).
- **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](#moving-target-states-between-components-v110).
- **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](#connector-keys-collisions-and-lost-writes-v110) 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 a `NodeTarget`), `relation_target` → `relationship_target`, `declare_relation_target` → `declare_relationship_target`, `mount_relation_target` → `mount_relationship_target`, and `RelationTarget.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](#graph-targets-node-and-relationship-apis); skipping the first deletes the table's nodes or edges.
- **Kafka `topic_as_map` with 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](https://cocoindex.io/docs/connectors/kafka/).
- **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_children` and `from_async_fn_with_children` are replaced by `TargetActionSink.from_fn(fn, child_handler=...)` and `from_async_fn(...)`. The old forms still run but don't support preview.
- **`coco.logic_tracked`** (v1.0.26) is a deprecated alias of `coco.logic_propagated_to_callers`.

**Behavior changes:**

- **Object-store `prefix` names a folder** (v1.1.0). For S3, Azure Blob and OCI, `prefix="docs"` now lists only `docs/...`; `docsa.md` and `docs-old/x` are 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, use `path_matcher`.
- **Preview leaves no trace** (v1.0.26). A preview no longer stores, deletes or refreshes memos and no longer writes committed state.
- **`FilePath` comparisons** (v1.1.0). `FilePath`s of different types no longer compare equal, and ordering them raises `TypeError`.

**New errors:**

- `DirTarget` names that are absolute or leave the directory (v1.1.0).
- A new zvec collection with an `Any`, `object`, union or `bytes` key column (v1.1.0).
- LanceDB key column types that deletes can't filter on, such as dates, timestamps, `float16` and 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 lone `max_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 `id` are re-created under a new record ID (v1.1.0).
- `DirTarget` files 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).

:::warning[Memo fingerprints change for values with shared sub-objects (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](#memo-fingerprints-depend-only-on-values-v110).
:::

**Requirements and the Rust SDK:**

- FalkorDB needs `falkordb` 1.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` instead of `&Graph` (v1.0.26).
- litellm 1.107.0 is excluded from every `litellm` requirement (v1.0.26); see [Security and dependencies](#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](https://cocoindex.io/docs/programming_guide/function/#memoization), and write committed state, so the next real update could skip work it had never done ([**#2510**](https://github.com/cocoindex-io/cocoindex/pull/2510)).

Both are fixed. Preview now lists the planned actions for [target states](https://cocoindex.io/docs/programming_guide/target_state/) under a container, reads memos without changing them, and writes no committed state. It also runs no deletes for [live components](https://cocoindex.io/docs/advanced_topics/live_component/). 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.

```bash
cocoindex update --preview path/to/app.py
```

```python
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](https://cocoindex.io/docs/common_resources/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.

<DocsLinkCard
  href="https://cocoindex.io/docs/advanced_topics/custom_target_connector/"
  eyebrow="Documentation"
  title="Container targets and the <em>child handler</em> factory."
  description="How a container sink declares its children, why the factory must be side-effect free, and how to migrate a sink written against the deprecated contracts."
/>

### 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**](https://github.com/cocoindex-io/cocoindex/pull/2590)):

```python
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 `LogicDep` at 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_dep` in a function whose memoized callers already stored entries does not re-store those entries. Ship the adoption with a `version` bump 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**](https://github.com/cocoindex-io/cocoindex/pull/2483), [**#2529**](https://github.com/cocoindex-io/cocoindex/pull/2529)). It is library-side; the user only puts `@coco.fn` on the callback.

```python
# 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`.

<DocsLinkCard
  href="https://cocoindex.io/docs/programming_guide/function/"
  eyebrow="Documentation"
  title="What a function's <em>logic fingerprint</em> covers."
  description="logic_tracking modes, deps=, per-call dependencies with LogicDep and use_logic_dep, and carrying a callback's logic through a self-tracked layer."
/>

### 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**](https://github.com/cocoindex-io/cocoindex/pull/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.

```python
@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`](https://cocoindex.io/docs/common_resources/rate_limiting/) ([**#2589**](https://github.com/cocoindex-io/cocoindex/pull/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`:

```python
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**](https://github.com/cocoindex-io/cocoindex/pull/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**](https://github.com/cocoindex-io/cocoindex/pull/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.

<DocsLinkCard
  href="https://cocoindex.io/docs/advanced_topics/concurrency_control/"
  eyebrow="Documentation"
  title="Bound a hierarchy of components <em>without deadlocking</em> it."
  description="Setting max_inflight_components, how a parent lends its slot, and how live components share the limit fairly."
/>

### Smaller additions (v1.0.26)

- **Nix** gets syntax-aware splitting and structural code patterns ([**#2434**](https://github.com/cocoindex-io/cocoindex/pull/2434), by [@mulatta](https://github.com/mulatta)).
- **`regex_required_literals`** is public ([**#2525**](https://github.com/cocoindex-io/cocoindex/pull/2525)).
- **Error cause chains.** Internal errors show their full cause chain, for example `db error: ERROR: deadlock detected` instead of `db error` ([**#2489**](https://github.com/cocoindex-io/cocoindex/pull/2489)).
- **The optional `application` telemetry tag** is documented ([**#2539**](https://github.com/cocoindex-io/cocoindex/pull/2539)).
- **`ConnectionFactory.close()`.** The Neo4j, FalkorDB and SurrealDB factories keep one connection per event loop, and the Neo4j and FalkorDB ones gain `close()`, which SurrealDB's already had. FalkorDB needs `falkordb` 1.6.0 or later ([**#2510**](https://github.com/cocoindex-io/cocoindex/pull/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**](https://github.com/cocoindex-io/cocoindex/pull/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!` and `mount_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_state` and 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**](https://github.com/cocoindex-io/cocoindex/pull/2512)). Breaking: the Neo4j and FalkorDB table and relation target functions take `&ContextKey` instead of `&Graph`.
- **CI coverage.** Every SDK feature is type-checked and its hermetic feature tests run ([**#2523**](https://github.com/cocoindex-io/cocoindex/pull/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**](https://github.com/cocoindex-io/cocoindex/pull/2621), from a fix by [@badmonster0](https://github.com/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](/blogs/changelog-1017-1025/#large-target-updates-less-memory-fewer-reconciles-v1025) 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**](https://github.com/cocoindex-io/cocoindex/pull/2485), by [@Sujit-1509](https://github.com/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 `ContextKey`s 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**](https://github.com/cocoindex-io/cocoindex/pull/2485)).
- **S3, Azure Blob and OCI:** object keys that differ only in a doubled or leading `/`, a `./` segment, or `\` versus `/`; with `prefix="docs"`, `docs/a.md` and `docsa.md`; and paths in different buckets, which compared equal ([**#2601**](https://github.com/cocoindex-io/cocoindex/pull/2601)).
- **Kafka:** the same message key on two topics of one `topic_as_map` feed. A tombstone on one topic deleted the other topic's outputs ([**#2597**](https://github.com/cocoindex-io/cocoindex/pull/2597)).
- **Neo4j and FalkorDB:** a node label and a relationship type with the same name, and derived edge IDs of different endpoints (`1` versus `"1"`, IDs containing `_`). The fix comes as the new node and relationship target APIs below ([**#2612**](https://github.com/cocoindex-io/cocoindex/pull/2612)).
- **SurrealDB:** derived relation IDs of different endpoints, such as `doc:'chunk_1'` and `doc_chunk:1` linked to the same tag ([**#2599**](https://github.com/cocoindex-io/cocoindex/pull/2599)).
- **zvec:** `1` and `"1"` in an `Any`, `object` or union key column became one document ([**#2608**](https://github.com/cocoindex-io/cocoindex/pull/2608), [**#2613**](https://github.com/cocoindex-io/cocoindex/pull/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**](https://github.com/cocoindex-io/cocoindex/pull/2603), [**#2615**](https://github.com/cocoindex-io/cocoindex/pull/2615)).
- **Valkey:** a payload key or `FieldDef` named `vector`, the hash field that holds the document's vector ([**#2606**](https://github.com/cocoindex-io/cocoindex/pull/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.txt` changed to `a.txt`. The file stayed missing until its content changed ([**#2598**](https://github.com/cocoindex-io/cocoindex/pull/2598)).
- **Qdrant:** a UUID point ID written in another text form: upper case, no hyphens, or a URN ([**#2602**](https://github.com/cocoindex-io/cocoindex/pull/2602)).
- **Kafka target:** a message key that switched between `str` and `bytes`. Its tombstone erased the record on a compacted topic ([**#2597**](https://github.com/cocoindex-io/cocoindex/pull/2597)).
- **Neo4j and FalkorDB:** a renamed vector index ([**#2611**](https://github.com/cocoindex-io/cocoindex/pull/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**](https://github.com/cocoindex-io/cocoindex/pull/2600)).
- **Doris:** keys containing `\`. Each update appended a duplicate row, removed items stayed, and a trailing `\` broke the statement ([**#2605**](https://github.com/cocoindex-io/cocoindex/pull/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**](https://github.com/cocoindex-io/cocoindex/pull/2611)). Dropping a node table, by un-declaring it or with `cocoindex drop`, left its vector indexes ([**#2618**](https://github.com/cocoindex-io/cocoindex/pull/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**](https://github.com/cocoindex-io/cocoindex/pull/2601)).
- **Doris:** a changed vector or inverted index parameter (`index_type`, `metric_type`, dimension, parser). It now rebuilds the table ([**#2605**](https://github.com/cocoindex-io/cocoindex/pull/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**](https://github.com/cocoindex-io/cocoindex/pull/2612)):

```python
# 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:

1. Add `managed_by="user"` to each deprecated declaration and run an update. Removing those declarations now deletes nothing.
2. 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](https://cocoindex.io/docs/connectors/neo4j/) and [FalkorDB](https://cocoindex.io/docs/connectors/falkordb/) docs under "Switching an existing table".

<DocsLinkCard
  href="https://cocoindex.io/docs/programming_guide/target_state/"
  eyebrow="Documentation"
  title="A target state's identity is its <em>key</em>."
  description="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**](https://github.com/cocoindex-io/cocoindex/pull/2411), by [@hatemadi15](https://github.com/hatemadi15), closing [#2391](https://github.com/cocoindex-io/cocoindex/issues/2391)). The one-time effect on fingerprints is in the [upgrade notes](#upgrade-notes).

### Hangs and shutdown (v1.0.26 to v1.1.0)

- **GPU pool.** A GPU released to a cancelled `acquire` goes back to the pool ([**#2595**](https://github.com/cocoindex-io/cocoindex/pull/2595)). Before, every later `acquire` of 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**](https://github.com/cocoindex-io/cocoindex/pull/2619), by [@Sujit-1509](https://github.com/Sujit-1509)).
- **Resources released at stop.** `coco.stop()` and `coco.stop_blocking()` release the lifespan-provided resources that registered apps held, instead of keeping them until each app's next update ([**#2425**](https://github.com/cocoindex-io/cocoindex/pull/2425), by [@hatemadi15](https://github.com/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**](https://github.com/cocoindex-io/cocoindex/pull/2490), [**#2499**](https://github.com/cocoindex-io/cocoindex/pull/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`](/blogs/changelog-1017-1025/#cocoretrywithsmallerbatch-batch-splitting-on-failure-v1017) introduced last cycle ([**#2532**](https://github.com/cocoindex-io/cocoindex/pull/2532)).

### Other fixes

- **Graph drop** (v1.0.26). `cocoindex drop` and 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**](https://github.com/cocoindex-io/cocoindex/pull/2498)).
- **Transient errors** (v1.0.26). A Neo4j apply batch retries on transient errors such as `DeadlockDetected` instead of failing the component ([**#2491**](https://github.com/cocoindex-io/cocoindex/pull/2491)).
- **`code_match` prefilters** (v1.0.26). Regex prefilters no longer drop nested alternatives, which made searches miss matches ([**#2525**](https://github.com/cocoindex-io/cocoindex/pull/2525)).
- **C# file-scoped namespaces** (v1.1.0). A file-scoped namespace (`namespace X;`) shows in the `render_match` context frames of the declarations after it ([**#2629**](https://github.com/cocoindex-io/cocoindex/pull/2629)).
- **`VectorSchema(dtype=np.float32)`** (v1.1.0). A schema whose dtype is the class `np.float32` rather 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 use `coco.unpickle_safe` ([**#2536**](https://github.com/cocoindex-io/cocoindex/pull/2536), by [@rtmalikian](https://github.com/rtmalikian)).
- **Passwords out of `repr`** (v1.1.0). Snowflake and Doris connection configs leave the password out of their `repr` ([**#2620**](https://github.com/cocoindex-io/cocoindex/pull/2620), by [@jerryqu-coco](https://github.com/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**](https://github.com/cocoindex-io/cocoindex/pull/2466), by [@pranav440](https://github.com/pranav440)).
- **Reopening an open `db_path`** (v1.1.0). The `RuntimeError` now names the path and `Environment.close()`, instead of a misleading message about different options ([**#2561**](https://github.com/cocoindex-io/cocoindex/pull/2561), by [@Siddesh-bype](https://github.com/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**](https://github.com/cocoindex-io/cocoindex/pull/2609), [**#2610**](https://github.com/cocoindex-io/cocoindex/pull/2610)).
- **Examples that failed when run as written.**
  - The `SeparatorSplitter` usage example ([**#2476**](https://github.com/cocoindex-io/cocoindex/pull/2476), by [@March-7](https://github.com/March-7)).
  - The localfs lifespan example, which named a `coco.EnvBuilder` that doesn't exist ([**#2397**](https://github.com/cocoindex-io/cocoindex/pull/2397), by [@Siddesh-bype](https://github.com/Siddesh-bype)).
  - The `get_context` example, which called the async `coco.default_env()` as if it were synchronous, and the App guide's component path tree ([**#2416**](https://github.com/cocoindex-io/cocoindex/pull/2416), by [@hardness1020](https://github.com/hardness1020)).
- **`drop_app` semantics.** The docs now say why `drop_app` clears the sub-database instead of removing it ([**#2431**](https://github.com/cocoindex-io/cocoindex/pull/2431), by [@hardness1020](https://github.com/hardness1020)).
- **HTTP clients.** The docs standardize on httpx and explain why Doris Stream Load keeps aiohttp ([**#2626**](https://github.com/cocoindex-io/cocoindex/pull/2626)).
- **Valkey `client_info_tag`** defaults to `"cocoindex"` ([**#2480**](https://github.com/cocoindex-io/cocoindex/pull/2480), by [@Jonathan-Improving](https://github.com/Jonathan-Improving)).
- **Agent skills** install with uv rather than pip ([**#2546**](https://github.com/cocoindex-io/cocoindex/pull/2546), [**#2552**](https://github.com/cocoindex-io/cocoindex/pull/2552)), a release playbook was added ([**#2627**](https://github.com/cocoindex-io/cocoindex/pull/2627)), and the Rust guidelines distilled from review comments are linked from `AGENTS.md` ([**#2500**](https://github.com/cocoindex-io/cocoindex/pull/2500)).

### Security and dependencies

- **litellm 1.107.0** was published to PyPI with a credential stealer and a backdoor. It is excluded from every `litellm` requirement: the `litellm`, `entity_resolution_llm` and `all` extras, and the examples. The locked version, 1.96.2, is unaffected ([**#2576**](https://github.com/cocoindex-io/cocoindex/pull/2576)).
- **Rust dependencies.** The legacy rustls 0.21 stack is dropped from the S3 client, which cleared the rustls-webpki advisories ([**#2544**](https://github.com/cocoindex-io/cocoindex/pull/2544)); `aws-smithy-json` 0.62.7 is taken for GHSA-8ffr-xgwf-xj56, which raises the minimum Rust version to 1.91.1 ([**#2542**](https://github.com/cocoindex-io/cocoindex/pull/2542)); rustls moves to 0.23.46 for GHSA-2mjx-qc3c-rqvc ([**#2543**](https://github.com/cocoindex-io/cocoindex/pull/2543)).
- **Dependabot.** Security updates are grouped, one PR per ecosystem ([**#2591**](https://github.com/cocoindex-io/cocoindex/pull/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**](https://github.com/cocoindex-io/cocoindex/pull/2584)). Around it:

- Job setup is shared through a composite action ([**#2578**](https://github.com/cocoindex-io/cocoindex/pull/2578)), sccache objects stay on local disk and are saved once per leg ([**#2583**](https://github.com/cocoindex-io/cocoindex/pull/2583)), and jobs free only the disk space they need ([**#2579**](https://github.com/cocoindex-io/cocoindex/pull/2579)).
- The Rust SDK checks run only for SDK changes, plus daily ([**#2622**](https://github.com/cocoindex-io/cocoindex/pull/2622)); CodeQL scans `main` daily instead of every pull request ([**#2596**](https://github.com/cocoindex-io/cocoindex/pull/2596)).
- Testcontainers images are pre-pulled before the first test, with a stall timeout and retries ([**#2551**](https://github.com/cocoindex-io/cocoindex/pull/2551), [**#2556**](https://github.com/cocoindex-io/cocoindex/pull/2556)); `ruff check` and clippy are gated in pre-commit ([**#2540**](https://github.com/cocoindex-io/cocoindex/pull/2540)); root-relative `/docs/` links are checked against the build output ([**#2557**](https://github.com/cocoindex-io/cocoindex/pull/2557), [**#2559**](https://github.com/cocoindex-io/cocoindex/pull/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](https://github.com/cocoindex-io/cocoindex/releases). If CocoIndex is useful to you, consider starring the [repository](https://github.com/cocoindex-io/cocoindex).

## Thanks to the community

Thanks to everyone who contributed this cycle, including nine first-time contributors.

### @Sujit-1509

Thanks [@Sujit-1509](https://github.com/Sujit-1509) for [keying Google Drive items by file ID](https://github.com/cocoindex-io/cocoindex/pull/2485), the fix that prompted the key audit across every Python connector, and for [keeping a cancelled source listing from hanging shutdown](https://github.com/cocoindex-io/cocoindex/pull/2619).

### @hatemadi15

Thanks [@hatemadi15](https://github.com/hatemadi15), a first-time contributor, for [making memo fingerprints depend only on values](https://github.com/cocoindex-io/cocoindex/pull/2411), which also closed a wrong-memo-hit bug from reused object IDs, and for [releasing retained app resources at `coco.stop()`](https://github.com/cocoindex-io/cocoindex/pull/2425).

### @hardness1020

Thanks [@hardness1020](https://github.com/hardness1020) for [fixing three places where the docs and the code disagreed](https://github.com/cocoindex-io/cocoindex/pull/2416) and for [correcting the explanation of what `drop_app` does to the sub-database](https://github.com/cocoindex-io/cocoindex/pull/2431).

### @Siddesh-bype

Thanks [@Siddesh-bype](https://github.com/Siddesh-bype), a first-time contributor, for [replacing the nonexistent `coco.EnvBuilder` with `coco.EnvironmentBuilder` in the localfs guide](https://github.com/cocoindex-io/cocoindex/pull/2397) and for [making the error on reopening an open `db_path` actionable](https://github.com/cocoindex-io/cocoindex/pull/2561).

### @mulatta

Thanks [@mulatta](https://github.com/mulatta), a first-time contributor, for [adding Nix syntax support](https://github.com/cocoindex-io/cocoindex/pull/2434) to code splitting and structural code patterns.

### @rtmalikian

Thanks [@rtmalikian](https://github.com/rtmalikian), a first-time contributor, for [failing fast at write time for classes the state reader rejects](https://github.com/cocoindex-io/cocoindex/pull/2536), so the error arrives on the run that caused it rather than the next one.

### @pranav440

Thanks [@pranav440](https://github.com/pranav440), a first-time contributor, for [handling zero-length and empty-object range reads in OCI Object Storage](https://github.com/cocoindex-io/cocoindex/pull/2466).

### @jerryqu-coco

Thanks [@jerryqu-coco](https://github.com/jerryqu-coco), a first-time contributor, for [leaving Snowflake and Doris passwords out of config `repr`](https://github.com/cocoindex-io/cocoindex/pull/2620).

### @March-7

Thanks [@March-7](https://github.com/March-7), a first-time contributor, for [fixing the `SeparatorSplitter` usage example](https://github.com/cocoindex-io/cocoindex/pull/2476) so it runs as written.

### @Jonathan-Improving

Thanks [@Jonathan-Improving](https://github.com/Jonathan-Improving), a first-time contributor, for [documenting the Valkey `client_info_tag` default](https://github.com/cocoindex-io/cocoindex/pull/2480).

### @Avicennasis

Thanks [@Avicennasis](https://github.com/Avicennasis), a first-time contributor, for [fixing a typo in the conversation_to_knowledge extraction prompt](https://github.com/cocoindex-io/cocoindex/pull/2593).

## Sitemap

- [Blog index](https://cocoindex.io/blogs/)
- [Site index (llms.txt)](https://cocoindex.io/llms.txt)
- [Full blog corpus](https://cocoindex.io/llms-full.txt)
- [Markdown sitemap](https://cocoindex.io/sitemap.md)
- [XML sitemap](https://cocoindex.io/sitemap.xml)
- [RSS feed](https://cocoindex.io/blogs/rss.xml)
