bytedance/deer-flow · error · ValueError
change_set.upsertRevisions values must be null or integers >
Error message
change_set.upsertRevisions values must be null or integers >= 1
What it means
apply_changes() validates each expected fact revision: it must be None or an int >= 1. The check explicitly excludes bools (isinstance(x, bool)) because bool is a subclass of int in Python, so True would otherwise pass as revision 1. Values come from upsertRevisions[fact_id] or the fact's own 'revision' field.
Source
Thrown at backend/packages/harness/deerflow/agents/memory/backends/deermem/deermem/core/storage.py:1268
if not isinstance(upserts, list) or not isinstance(deletes, list):
raise ValueError("change_set.upserts and change_set.deletes must be lists")
if delete_revisions is not None and not isinstance(delete_revisions, dict):
raise ValueError("change_set.deleteRevisions must be an object")
if upsert_revisions is not None and not isinstance(upsert_revisions, dict):
raise ValueError("change_set.upsertRevisions must be an object")
normalized_upsert_revisions: dict[str, int | None] = {}
for incoming in upserts:
if not isinstance(incoming, dict):
raise ValueError("change_set.upserts must contain fact objects")
incoming["id"] = str(incoming.get("id") or f"fact_{uuid.uuid4().hex}")
fact_id = incoming["id"]
if isinstance(upsert_revisions, dict) and fact_id in upsert_revisions:
expected_fact_revision = upsert_revisions[fact_id]
else:
expected_fact_revision = incoming.get("revision") if "revision" in incoming else None
if expected_fact_revision is not None and (isinstance(expected_fact_revision, bool) or not isinstance(expected_fact_revision, int) or expected_fact_revision < 1):
raise ValueError("change_set.upsertRevisions values must be null or integers >= 1")
normalized_upsert_revisions[fact_id] = expected_fact_revision
path = self._get_memory_file_path(agent_name, user_id=user_id)
key = self._cache_key(agent_name, user_id=user_id)
expected = expected_manifest_revision
notifications: list[RetrievalNotification] = []
memory_file: dict[str, Any] | None = None
safe_delete_rebase = not deletes or (isinstance(delete_revisions, dict) and all(str(fact_id) in delete_revisions for fact_id in deletes))
safe_upsert_rebase = all(str(incoming["id"]) in normalized_upsert_revisions for incoming in upserts)
for attempt in range(3):
try:
with self._scope_lock(key), _process_file_lock(path.parent / ".memory.lock", float(getattr(self._config, "file_lock_timeout_seconds", 10))):
self._recover_if_needed(path)
memory_file, notifications = self._commit_changes_locked(
path,
user_id=user_id,
agent_name=agent_name,
upserts=upserts,View on GitHub (pinned to 1dd6ba1acb)
Solutions
- Use null for 'no expected revision' and integers >= 1 otherwise.
- Coerce at the boundary: int(rev) after checking str/bool, and map 0 to None.
- Strip stale 'revision' fields from upserted fact bodies if you do not intend an optimistic-concurrency check.
Example fix
# before
storage.apply_changes({"upserts": facts, "upsertRevisions": {fid: "3" for fid in ids}}, agent_name=a)
# after
revisions = {fid: (int(rev) if rev not in (None, 0) else None) for fid, rev in raw.items()}
storage.apply_changes({"upserts": facts, "upsertRevisions": revisions}, agent_name=a) Defensive patterns
Strategy: validation
Validate before calling
def norm_rev(v):
if v in (None, 0, ""):
return None
v = int(v)
if v < 1:
return None
return v
change_set["upsertRevisions"] = {fid: norm_rev(rev) for fid, rev in change_set.get("upsertRevisions", {}).items()} Type guard
def is_valid_revision(v: object) -> TypeGuard[int | None]:
return v is None or (isinstance(v, int) and not isinstance(v, bool) and v >= 1) Prevention
- Remember bool is an int subclass; never store flags in revision fields.
- Use null (not 0) for 'no expected revision'.
- Coerce string revisions from JSON clients at the boundary.
When it happens
Trigger: upsertRevisions with a bool (True/False), 0, negative int, float like 1.5, or numeric string '3'; or an upserted fact carrying revision: 0 / revision: True in its own body.
Common situations: JSON clients sending revisions as strings; using 0 as 'no revision yet' (the API expects null); flags accidentally stored in the revision field after a schema mixup.
Related errors
- fact.revision must be an integer >= 1
- change_set.deleteRevisions must be an object
- change_set.upsertRevisions must be an object
- retrieval fact.id must be a non-empty string
- retrieval fact.content must be a non-empty string
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/14031506a6f75439.
Report an issue: GitHub.