You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[Bug]: Legacy uint16-truncated records are permanently undeletable — delete/upsert still json.loads corrupt old_fields, no repair path for pre-#2171 stores #2966
Follow-up to #2117 / #1892: stores that contain legacy uint16-truncated records (written by versions before #2171) still have no cleanup path on current main.
Great progress has already landed for this family of corruption:
However, all of these are read-side mitigations. The mutation paths still hard-fail on a corrupt record:
LocalIndex.delete_data() and upsert_data() both run _convert_delta_list_for_index() (openviking/storage/vectordb/index/local_index.py:339), which calls FieldTypeConverter.convert_fields_for_index() → a bare json.loads(fields_json) on fields / old_fields with no error handling.
StoreManager.delete_data() populates DeltaRecord.old_fields directly from the stored (corrupt) candidate value.
Net effect: a legacy-corrupt record can never be deleted or overwritten through normal APIs — the JSONDecodeError makes the delete/upsert fail every time. In our production incident this had a nasty second-order consequence: a memory-cleanup job deleted the AGFS .md files, but the corresponding vectordb deletes silently failed, leaving 41 "ghost" records (source file gone, corrupt vector record permanently stuck, Candidate data is None ... skipping warnings on every query).
This is exactly the "Bug 1 needs its own thread" part that was deferred in #1892 and (as far as I can search) never got that thread.
Steps to Reproduce
Run a pre-fix(vectordb): reject oversized bytes row strings #2171 image (e.g. the 2026-05-15 ghcr.io/volcengine/openviking:latest) with vectordb.backend: local, and commit a memory whose serialized fields JSON exceeds 65535 bytes (easy with CJK content: \uXXXX escaping inflates UTF-8 roughly 2×, so a ~40 KB Chinese .md is enough). The uint16 length prefix wraps mod 65536 and a truncated record is persisted.
Upgrade to current main. Startup recovery and search now tolerate the record (thanks!).
Try to delete (or overwrite) that record via any path that reaches LocalCollection.delete_data() — e.g. forget, or a memory-cleanup flow.
Expected Behavior
Deleting a record whose stored fields/old_fields no longer parse should still succeed — best-effort: treat unparseable old_fields as empty for index-delete purposes (the label is all the engine really needs to drop it), log a warning, and remove the record from CandsTable.
Ideally, an offline fsck/repair subcommand that scans CandsTable/DeltaTable for records whose fields fail json.loads, reports them, and quarantines/purges them on request — so operators of pre-fix(vectordb): reject oversized bytes row strings #2171 stores have an official migration path instead of raw RocksDB surgery.
Actual Behavior
_convert_delta_list_for_index raises json.decoder.JSONDecodeError, the delete/upsert fails, and the corrupt record is permanently stuck. Combined with higher layers that delete the source file first, this produces undeletable ghost records.
Minimal Reproducible Example
# maintenance-style scan we used to quantify the damage (run inside the image)importjsonfromopenviking.storage.vectordb.store.dataimportCandidateData, DeltaRecordfromopenviking.storage.vectordb.store.store_managerimportStoreManager, create_store_managersm=create_store_manager("local", "/app/data/vectordb/context/store")
fork, vinsm.storage.read_all(StoreManager.CandsTable):
c=CandidateData.from_bytes(data=v)
try:
json.loads(c.fields)
exceptExceptionase:
print("corrupt candidate", k, e)
# LocalCollection.delete_data([c.label]) -> raises the same JSONDecodeError,# so there is no API-level way to get rid of this record.
Error Logs
Real-world incident (store written by the 2026-05-15 image; 425 candidates, 42 corrupt; 3628 delta records, 268 corrupt; container crash-looped 397 times on that image — current main survives startup but cannot clean the store):
File ".../openviking/storage/vectordb/index/local_index.py", line 355, in _convert_delta_list_for_index
self.field_type_converter.convert_fields_for_index(data.old_fields)
File ".../openviking/storage/vectordb/utils/data_processor.py", line 337, in convert_fields_for_index
data = json.loads(fields_json)
json.decoder.JSONDecodeError: Unterminated string starting at: line 1 column 296 (char 295)
Forensics note for anyone hitting this: the truncated values are not recoverable — the writer sized the buffer with the true length but only memcpy'd len & 0xFFFF bytes, so the blob tail after the wrapped length is uninitialized memory. A verified workaround is to back up data/vectordb, then delete the corrupt KV pairs directly via StoreManager.storage.delete(keys, table) for both DeltaTable and CandsTable; vectors for the healthy records live in CandsTable, so no re-embedding is needed.
Environment
OpenViking: ghcr.io/volcengine/openviking:latest (image created 2026-05-15) for the corruption; code inspection of current main for the still-broken delete/upsert path
Storage: local (PersistCollection + RocksDB store), engine variant x86_avx512
OS: Linux (Debian, kernel 6.10), Python 3.13 (in-image)
Bug Description
Follow-up to #2117 / #1892: stores that contain legacy uint16-truncated records (written by versions before #2171) still have no cleanup path on current
main.Great progress has already landed for this family of corruption:
PersistCollection._replay_recovery_recordsskips corrupt delta records during startup recovery (batch → individual retry → skip);fieldsduring search.However, all of these are read-side mitigations. The mutation paths still hard-fail on a corrupt record:
LocalIndex.delete_data()andupsert_data()both run_convert_delta_list_for_index()(openviking/storage/vectordb/index/local_index.py:339), which callsFieldTypeConverter.convert_fields_for_index()→ a barejson.loads(fields_json)onfields/old_fieldswith no error handling.StoreManager.delete_data()populatesDeltaRecord.old_fieldsdirectly from the stored (corrupt) candidate value.Net effect: a legacy-corrupt record can never be deleted or overwritten through normal APIs — the
JSONDecodeErrormakes the delete/upsert fail every time. In our production incident this had a nasty second-order consequence: a memory-cleanup job deleted the AGFS.mdfiles, but the corresponding vectordb deletes silently failed, leaving 41 "ghost" records (source file gone, corrupt vector record permanently stuck,Candidate data is None ... skippingwarnings on every query).This is exactly the "Bug 1 needs its own thread" part that was deferred in #1892 and (as far as I can search) never got that thread.
Steps to Reproduce
ghcr.io/volcengine/openviking:latest) withvectordb.backend: local, and commit a memory whose serializedfieldsJSON exceeds 65535 bytes (easy with CJK content:\uXXXXescaping inflates UTF-8 roughly 2×, so a ~40 KB Chinese.mdis enough). The uint16 length prefix wraps mod 65536 and a truncated record is persisted.main. Startup recovery and search now tolerate the record (thanks!).LocalCollection.delete_data()— e.g.forget, or a memory-cleanup flow.Expected Behavior
fields/old_fieldsno longer parse should still succeed — best-effort: treat unparseableold_fieldsas empty for index-delete purposes (the label is all the engine really needs to drop it), log a warning, and remove the record from CandsTable.fsck/repair subcommand that scans CandsTable/DeltaTable for records whosefieldsfailjson.loads, reports them, and quarantines/purges them on request — so operators of pre-fix(vectordb): reject oversized bytes row strings #2171 stores have an official migration path instead of raw RocksDB surgery.Actual Behavior
_convert_delta_list_for_indexraisesjson.decoder.JSONDecodeError, the delete/upsert fails, and the corrupt record is permanently stuck. Combined with higher layers that delete the source file first, this produces undeletable ghost records.Minimal Reproducible Example
Error Logs
Real-world incident (store written by the 2026-05-15 image; 425 candidates, 42 corrupt; 3628 delta records, 268 corrupt; container crash-looped 397 times on that image — current main survives startup but cannot clean the store):
Forensics note for anyone hitting this: the truncated values are not recoverable — the writer sized the buffer with the true length but only memcpy'd
len & 0xFFFFbytes, so the blob tail after the wrapped length is uninitialized memory. A verified workaround is to back updata/vectordb, then delete the corrupt KV pairs directly viaStoreManager.storage.delete(keys, table)for both DeltaTable and CandsTable; vectors for the healthy records live in CandsTable, so no re-embedding is needed.Environment
ghcr.io/volcengine/openviking:latest(image created 2026-05-15) for the corruption; code inspection of currentmainfor the still-broken delete/upsert pathx86_avx512References