HKUDS/DeepTutor · error · OpValidationError
add: refs must be non-empty
Error message
add: refs must be non-empty
What it means
OpValidationError raised in _validate when an AddOp carries an empty refs list. Every memory entry must cite at least one provenance reference (source message/span), which is what later consolidation and undo rely on; an entry with no refs cannot be traced back and is rejected before apply mutates anything.
Source
Thrown at deeptutor/services/memory/ops.py:81
class OpValidationError(Exception):
"""Raised when a batch fails pre-flight validation."""
def _validate(doc: Document, ops: list[Op]) -> None:
edits: set[str] = set()
deletes: set[str] = set()
for op in ops:
if isinstance(op, AddOp):
if not op.text or len(op.text) > _MAX_TEXT_LEN:
raise OpValidationError(
f"add: text length must be 1..{_MAX_TEXT_LEN} (got {len(op.text)})"
)
if not op.section or len(op.section) > _MAX_SECTION_LEN:
raise OpValidationError(f"add: invalid section {op.section!r}")
if not op.refs:
raise OpValidationError("add: refs must be non-empty")
for ref in op.refs:
if not is_valid_ref(ref):
raise OpValidationError(f"add: malformed ref {ref!r}")
elif isinstance(op, EditOp):
if not is_entry_id(op.target_id):
raise OpValidationError(f"edit: malformed target_id {op.target_id!r}")
if doc.find(op.target_id) is None:
raise OpValidationError(f"edit: target_id {op.target_id} not found")
if not op.new_text or len(op.new_text) > _MAX_TEXT_LEN:
raise OpValidationError(
f"edit: text length must be 1..{_MAX_TEXT_LEN} (got {len(op.new_text)})"
)
if not op.new_refs:
raise OpValidationError("edit: refs must be non-empty")
for ref in op.new_refs:
if not is_valid_ref(ref):
raise OpValidationError(f"edit: malformed ref {ref!r}")
if op.target_id in deletes:View on GitHub (pinned to 3e82f13042)
Solutions
- Always derive refs from the triggering source (e.g. message id / span ref) and pass them in AddOp.refs
- If your pipeline has no real source, synthesize a stable ref (e.g. 'manual:<uuid>' if the ref grammar allows) rather than an empty list
- Guard upstream: skip the write and log when the ref-extraction step yields nothing
Example fix
# before
op = AddOp(text=note, section="facts", refs=[])
# after
if not refs:
refs = [f"msg:{source_message_id}"]
op = AddOp(text=note, section="facts", refs=refs) Defensive patterns
Strategy: validation
Validate before calling
refs = [r for r in raw_refs if r]
if not refs:
refs = [f"msg:{source_message_id}"] Type guard
def has_valid_refs(refs) -> bool:
return isinstance(refs, (list, tuple)) and len(refs) > 0 Try / catch
try:
ops.apply(doc, [op])
except OpValidationError as e:
if "refs must be non-empty" in str(e):
op = replace(op, refs=[fallback_ref])
ops.apply(doc, [op])
else:
raise Prevention
- Thread source provenance into every memory write from the start
- Skip+log writes whose ref extraction came back empty
- Require refs in the tool schema so LLM calls can't omit them
When it happens
Trigger: AddOp(text=..., section=..., refs=[]) or refs=None passed to ops.apply — typically a caller forgetting to thread the originating message/source span into the op, or an LLM tool-call omitting the refs argument.
Common situations: New integration code writing memories without capturing source references; tests building AddOp fixtures minimally; ref extraction step returning an empty list that is passed through unchecked.
Related errors
- add: text length must be 1..{_MAX_TEXT_LEN} (got {len(op.tex
- add: invalid section {op.section!r}
- add: malformed ref {ref!r}
- edit: malformed target_id {op.target_id!r}
- edit: target_id {op.target_id} not found
AI-assisted analysis of HKUDS/DeepTutor@3e82f13042 (2026-08-27).
Data as JSON: /api/errors/b3d4396eb6d2dc7d.
Report an issue: GitHub.