Release exp-v0.52.197: preserve api_content for agent replay (#6757, @franksong2702)
Ships PR #6757 with a maintainer hardening pass folded in (authorship preserved). Preserves the exact provider-facing reply text (api_content) through the internal session store for byte-accurate agent replay, while guaranteeing it never reaches a browser: every client-facing surface strips it via the public projection. Maintainer fixes (2 defects my adversarial re-gate reproduced): - Selective session redaction restored: only transcript fields are scrubbed; operational fields (e.g. workspace path) pass through unchanged, so a path containing credential-shaped text is no longer masked into an invalid path. - Runner-backed SSE relay now projects payloads before emit, closing an api_content leak through _stream_runner_run_events. +5 regression tests. Codex adversarial re-gate: SAFE TO SHIP. Full suite clean. Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
This commit is contained in:
1 parent
728ec10e66
commit
cf7eb48e56
9 files changed
+3187
-112
No files matched your search
+729
-14
@@ -7986,6 +7986,11 @@ def get_state_db_session_messages(
|
||||
'codex_reasoning_items',
|
||||
'reasoning_content',
|
||||
'codex_message_items',
|
||||
# Hermes Agent stores the exact provider-facing text here when
|
||||
# it differs from the clean transcript content. Keep this
|
||||
# sidecar in the WebUI's internal history; the provider-safe
|
||||
# projection strips it before any direct API request.
|
||||
'api_content',
|
||||
]
|
||||
id_col = ['id'] if 'id' in available else []
|
||||
selected = id_col + ['role', 'content', 'timestamp'] + [c for c in optional if c in available]
|
||||
@@ -8090,6 +8095,11 @@ def get_state_db_session_messages(
|
||||
'content': row['content'],
|
||||
'timestamp': row['timestamp'],
|
||||
}
|
||||
# ``id`` is the durable SQLite row identity, not the WebUI's
|
||||
# session-local stable message id. Keep it in a private
|
||||
# provenance field so duplicate reconciliation can align a
|
||||
# state.db sidecar without changing the existing ``id`` key
|
||||
# used by WebUI transcript merge/dedup logic.
|
||||
for col in optional:
|
||||
if col not in row.keys():
|
||||
continue
|
||||
@@ -8099,6 +8109,16 @@ def get_state_db_session_messages(
|
||||
if col in {'tool_calls', 'reasoning_details', 'codex_reasoning_items', 'codex_message_items'}:
|
||||
value = _json_loads_if_string(value)
|
||||
msg[col] = value
|
||||
# Keep durable provenance only alongside a real Agent replay
|
||||
# sidecar. Ordinary state.db rows must retain their historic
|
||||
# shape and must not acquire internal bookkeeping fields.
|
||||
if (
|
||||
id_col
|
||||
and row['id'] is not None
|
||||
and isinstance(msg.get('api_content'), str)
|
||||
and msg['api_content']
|
||||
):
|
||||
msg['_state_db_row_id'] = row['id']
|
||||
if msg.get('role') == 'tool' and msg.get('tool_name') and not msg.get('name'):
|
||||
msg['name'] = msg['tool_name']
|
||||
msgs.append(msg)
|
||||
@@ -8131,6 +8151,8 @@ def get_state_db_session_message_prefix_summary(
|
||||
before_ts = float(before_timestamp)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if not math.isfinite(before_ts):
|
||||
return None
|
||||
|
||||
if isinstance(profile, str) and profile:
|
||||
db_path = _get_profile_home(profile) / 'state.db'
|
||||
@@ -8219,12 +8241,13 @@ def get_state_db_session_message_keys_before_timestamp(
|
||||
available = {str(row['name']) for row in cur.fetchall()}
|
||||
if not {'id', 'session_id', 'role', 'content', 'timestamp', 'tool_calls'}.issubset(available):
|
||||
return None
|
||||
api_content_select = ", api_content" if "api_content" in available else ""
|
||||
cur.execute(
|
||||
"""
|
||||
f"""
|
||||
SELECT
|
||||
COALESCE(role, '') AS role,
|
||||
COALESCE(content, '') AS content,
|
||||
tool_calls
|
||||
tool_calls{api_content_select}
|
||||
FROM messages
|
||||
WHERE session_id = ? AND timestamp IS NOT NULL AND timestamp < ?
|
||||
ORDER BY timestamp ASC, id ASC
|
||||
@@ -8237,6 +8260,7 @@ def get_state_db_session_message_keys_before_timestamp(
|
||||
"role": row["role"],
|
||||
"content": row["content"],
|
||||
"tool_calls": _json_loads_if_string(row["tool_calls"]),
|
||||
"api_content": row["api_content"] if "api_content" in available else None,
|
||||
}
|
||||
)
|
||||
for row in cur.fetchall()
|
||||
@@ -8299,6 +8323,11 @@ def _normalized_message_timestamp_for_key(value):
|
||||
timestamp = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return str(value)
|
||||
if not math.isfinite(timestamp):
|
||||
# Keep malformed metadata from crashing key construction. The
|
||||
# reconciliation layer separately marks this row invalid so it cannot
|
||||
# fall through to a weaker metadata-free match.
|
||||
return f"<invalid:{value!r}>"
|
||||
# Truncate to second-level granularity so that sub-second drift between
|
||||
# the sidecar JSON write and the state.db created_at write does not cause
|
||||
# the legacy dedup key to differ for the same logical message.
|
||||
@@ -8312,9 +8341,29 @@ def _message_timestamp_as_float(msg):
|
||||
if value is None or value == "":
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
timestamp = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return timestamp if math.isfinite(timestamp) else None
|
||||
|
||||
|
||||
def _session_message_api_content_key(msg: dict):
|
||||
"""Return the exact trusted provider sidecar used in duplicate identity."""
|
||||
if not isinstance(msg, dict):
|
||||
return None
|
||||
value = msg.get("api_content")
|
||||
return value if isinstance(value, str) and value else None
|
||||
|
||||
|
||||
def _session_message_key_with_sidecar(base_key: tuple, msg: dict) -> tuple:
|
||||
"""Append provider sidecar identity only when one is actually present.
|
||||
|
||||
The no-sidecar key shape is an internal compatibility surface used by
|
||||
reconciliation tests and callers. A present sidecar must extend that
|
||||
identity so different provider bytes cannot collapse into one duplicate.
|
||||
"""
|
||||
sidecar = _session_message_api_content_key(msg)
|
||||
return base_key if sidecar is None else (*base_key, sidecar)
|
||||
|
||||
|
||||
def _session_message_merge_key(msg: dict):
|
||||
@@ -8322,7 +8371,9 @@ def _session_message_merge_key(msg: dict):
|
||||
return ("non_dict", repr(msg))
|
||||
message_identity = msg.get("id") or msg.get("message_id")
|
||||
if message_identity:
|
||||
return ("message_id", str(message_identity))
|
||||
return _session_message_key_with_sidecar(
|
||||
("message_id", str(message_identity)), msg
|
||||
)
|
||||
# Include tool_calls so assistant messages that invoke different tools
|
||||
# (but share identical empty content and same-second timestamp) are not
|
||||
# collapsed by the merge-key guard at line ~4216. Without this,
|
||||
@@ -8331,7 +8382,7 @@ def _session_message_merge_key(msg: dict):
|
||||
# every state.db tool-call after the first one registered by the sidecar.
|
||||
_tc = msg.get("tool_calls")
|
||||
_tc_key = json.dumps(_tc, sort_keys=True, default=str) if _tc else ""
|
||||
return (
|
||||
return _session_message_key_with_sidecar((
|
||||
"legacy",
|
||||
str(msg.get("role") or ""),
|
||||
str(msg.get("content") or ""),
|
||||
@@ -8339,7 +8390,7 @@ def _session_message_merge_key(msg: dict):
|
||||
str(msg.get("tool_call_id") or ""),
|
||||
str(msg.get("tool_name") or msg.get("name") or ""),
|
||||
_tc_key,
|
||||
)
|
||||
), msg)
|
||||
|
||||
|
||||
def _session_messages_have_prefix(messages, prefix) -> bool:
|
||||
@@ -8386,6 +8437,585 @@ def _merge_session_display_metadata(target: dict | None, source: dict | None) ->
|
||||
target[key] = copy.deepcopy(value)
|
||||
|
||||
|
||||
def _state_db_row_identity_details(message: dict | None) -> tuple[str | None, bool]:
|
||||
"""Return ``(row_id, valid)`` for private state.db provenance aliases.
|
||||
|
||||
A message carrying two different aliases is contradictory provenance. It
|
||||
must not silently fall through to timestamp/sequence matching, because that
|
||||
would turn an identity conflict into a guessed provider-side payload.
|
||||
"""
|
||||
if not isinstance(message, dict):
|
||||
return None, True
|
||||
values = set()
|
||||
for key in ("_state_db_row_id", "_db_row_id", "state_db_row_id"):
|
||||
if key not in message or message.get(key) in (None, ""):
|
||||
continue
|
||||
value = message.get(key)
|
||||
if isinstance(value, bool):
|
||||
return None, False
|
||||
if isinstance(value, int):
|
||||
normalized = str(value) if value >= 0 else None
|
||||
elif isinstance(value, float):
|
||||
normalized = str(int(value)) if math.isfinite(value) and value >= 0 and value.is_integer() else None
|
||||
elif isinstance(value, str):
|
||||
text = value.strip()
|
||||
normalized = text if text.isdigit() else None
|
||||
if normalized is not None:
|
||||
normalized = str(int(normalized))
|
||||
else:
|
||||
normalized = None
|
||||
if normalized is None:
|
||||
return None, False
|
||||
values.add(normalized)
|
||||
if len(values) > 1:
|
||||
return None, False
|
||||
return (next(iter(values)) if values else None), True
|
||||
|
||||
|
||||
def _state_db_row_identity(message: dict | None):
|
||||
"""Return durable state.db provenance without treating WebUI ``id`` as it."""
|
||||
identity, valid = _state_db_row_identity_details(message)
|
||||
return identity if valid else None
|
||||
|
||||
|
||||
def _stable_message_identity_details(message: dict | None) -> tuple[str | None, bool]:
|
||||
"""Return a canonical stable message id and whether its aliases are valid."""
|
||||
if not isinstance(message, dict):
|
||||
return None, True
|
||||
values = set()
|
||||
for key in ("id", "message_id"):
|
||||
if key not in message or message.get(key) in (None, ""):
|
||||
continue
|
||||
value = message.get(key)
|
||||
if isinstance(value, bool):
|
||||
return None, False
|
||||
if isinstance(value, (int, str)):
|
||||
normalized = str(value).strip()
|
||||
elif isinstance(value, float):
|
||||
normalized = str(value) if math.isfinite(value) else ""
|
||||
else:
|
||||
normalized = ""
|
||||
if not normalized:
|
||||
return None, False
|
||||
values.add(normalized)
|
||||
if len(values) > 1:
|
||||
return None, False
|
||||
return (next(iter(values)) if values else None), True
|
||||
|
||||
|
||||
def _message_identity_compatible(target: dict | None, source: dict | None) -> bool:
|
||||
"""Return whether private identities do not contradict one another."""
|
||||
target_stable, target_stable_valid = _stable_message_identity_details(target)
|
||||
source_stable, source_stable_valid = _stable_message_identity_details(source)
|
||||
if not target_stable_valid or not source_stable_valid:
|
||||
return False
|
||||
if target_stable is not None and source_stable is not None and target_stable != source_stable:
|
||||
return False
|
||||
target_row_id, target_row_id_valid = _state_db_row_identity_details(target)
|
||||
source_row_id, source_row_id_valid = _state_db_row_identity_details(source)
|
||||
if not target_row_id_valid or not source_row_id_valid:
|
||||
return False
|
||||
if target_row_id is not None and source_row_id is not None and target_row_id != source_row_id:
|
||||
return False
|
||||
return _visible_content_compatible(target, source)
|
||||
|
||||
|
||||
def _message_exact_timestamp(message: dict | None):
|
||||
"""Return a numeric transcript timestamp, preserving ``0`` as valid."""
|
||||
if not isinstance(message, dict):
|
||||
return None
|
||||
parsed, valid = _message_exact_timestamp_details(message)
|
||||
return parsed if valid else None
|
||||
|
||||
|
||||
def _message_exact_timestamp_details(message: dict | None) -> tuple[float | None, bool]:
|
||||
"""Return ``(timestamp, valid)`` while distinguishing absent metadata."""
|
||||
if not isinstance(message, dict):
|
||||
return None, True
|
||||
for key in ("timestamp", "_ts"):
|
||||
if key not in message or message.get(key) in (None, ""):
|
||||
continue
|
||||
try:
|
||||
parsed = float(message.get(key))
|
||||
except (TypeError, ValueError):
|
||||
return None, False
|
||||
if not math.isfinite(parsed):
|
||||
return None, False
|
||||
return parsed, True
|
||||
return None, True
|
||||
|
||||
|
||||
def _message_sidecar_role(message: dict | None):
|
||||
if not isinstance(message, dict):
|
||||
return None
|
||||
role = str(message.get("role") or "").strip().lower()
|
||||
return role if role in {"user", "assistant"} else None
|
||||
|
||||
|
||||
_WORKSPACE_PREFIX_RE = re.compile(r"^\s*\[Workspace(?:::v1)?:[^\]]+\]\s*")
|
||||
|
||||
|
||||
def _message_visible_content_key(message: dict | None):
|
||||
"""Return a canonical visible-content key for sidecar reconciliation."""
|
||||
if not isinstance(message, dict):
|
||||
return None
|
||||
role = _message_sidecar_role(message)
|
||||
if role is None:
|
||||
return None
|
||||
|
||||
def _normalize(value, *, strip_prefix=False):
|
||||
if isinstance(value, str):
|
||||
text = value
|
||||
if strip_prefix:
|
||||
text = _WORKSPACE_PREFIX_RE.sub("", text, count=1)
|
||||
return " ".join(text.split())
|
||||
if isinstance(value, list):
|
||||
return [_normalize(item, strip_prefix=strip_prefix) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: _normalize(child, strip_prefix=False)
|
||||
for key, child in value.items()
|
||||
if key not in {"api_content", "_state_db_row_id", "_db_row_id", "state_db_row_id"}
|
||||
}
|
||||
return value
|
||||
|
||||
normalized = _normalize(message.get("content"), strip_prefix=(role == "user"))
|
||||
try:
|
||||
content = json.dumps(normalized, ensure_ascii=False, sort_keys=True, default=str)
|
||||
except (TypeError, ValueError):
|
||||
content = repr(normalized)
|
||||
return role, content
|
||||
|
||||
|
||||
def _visible_content_compatible(target: dict | None, source: dict | None) -> bool:
|
||||
"""Return True only when role and visible content agree exactly."""
|
||||
return _message_visible_content_key(target) == _message_visible_content_key(source)
|
||||
|
||||
|
||||
def _copy_api_content_sidecar(target: dict | None, source: dict | None) -> bool:
|
||||
"""Copy a non-empty internal sidecar without replacing an existing one."""
|
||||
if not isinstance(target, dict) or not isinstance(source, dict):
|
||||
return False
|
||||
target_role = _message_sidecar_role(target)
|
||||
source_role = _message_sidecar_role(source)
|
||||
if target_role is None or target_role != source_role:
|
||||
return False
|
||||
if not _visible_content_compatible(target, source):
|
||||
return False
|
||||
if target.get("api_content") not in (None, ""):
|
||||
return True
|
||||
api_content = source.get("api_content")
|
||||
if isinstance(api_content, str) and api_content:
|
||||
target["api_content"] = api_content
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _reconcile_api_content_sidecars(sidecar_messages: list, state_messages: list) -> None:
|
||||
"""Attach state.db ``api_content`` to the matching sidecar transcript rows.
|
||||
|
||||
Matching is intentionally stricter than visible transcript dedupe. A
|
||||
unique stable message id is preferred, followed by a unique durable
|
||||
state.db row id, exact timestamps, same-second timestamp drift, and finally
|
||||
a mutually-unique role/content pair that may have mixed timestamp metadata.
|
||||
Duplicate, malformed, or contradictory provenance is consumed without a
|
||||
fallback guess. Repeated visible rows without unique provenance remain
|
||||
unattached. If both sides already carry different non-empty provider
|
||||
sidecars, neither is attached; the append-only merge preserves those rows
|
||||
as distinct.
|
||||
"""
|
||||
sidecar = [message for message in sidecar_messages or () if isinstance(message, dict)]
|
||||
state = [
|
||||
message
|
||||
for message in state_messages or ()
|
||||
if isinstance(message, dict)
|
||||
and _message_sidecar_role(message) is not None
|
||||
and isinstance(message.get("api_content"), str)
|
||||
and message.get("api_content")
|
||||
]
|
||||
if not sidecar or not state:
|
||||
return
|
||||
|
||||
used_targets: set[int] = set()
|
||||
used_sources: set[int] = set()
|
||||
|
||||
# Invalid provenance is never treated as absent metadata. Consume those
|
||||
# rows up front so no weaker tier can attach an arbitrary provider sidecar.
|
||||
for index, message in enumerate(sidecar):
|
||||
_, row_id_valid = _state_db_row_identity_details(message)
|
||||
_, timestamp_valid = _message_exact_timestamp_details(message)
|
||||
_, stable_id_valid = _stable_message_identity_details(message)
|
||||
if not row_id_valid or not timestamp_valid or not stable_id_valid:
|
||||
used_targets.add(index)
|
||||
for source_index, message in enumerate(state):
|
||||
_, row_id_valid = _state_db_row_identity_details(message)
|
||||
_, timestamp_valid = _message_exact_timestamp_details(message)
|
||||
_, stable_id_valid = _stable_message_identity_details(message)
|
||||
if not row_id_valid or not timestamp_valid or not stable_id_valid:
|
||||
used_sources.add(source_index)
|
||||
|
||||
# 1. Stable message identity. This is the strongest WebUI-side identity
|
||||
# and must run before timestamp/content fallbacks. Duplicate or conflicting
|
||||
# aliases are consumed above and cannot fall through to a guess.
|
||||
targets_by_stable_id = {}
|
||||
state_by_stable_id = {}
|
||||
for index, message in enumerate(sidecar):
|
||||
if index in used_targets:
|
||||
continue
|
||||
stable_id, valid = _stable_message_identity_details(message)
|
||||
if valid and stable_id is not None:
|
||||
targets_by_stable_id.setdefault(stable_id, []).append(index)
|
||||
for source_index, message in enumerate(state):
|
||||
if source_index in used_sources:
|
||||
continue
|
||||
stable_id, valid = _stable_message_identity_details(message)
|
||||
if valid and stable_id is not None:
|
||||
state_by_stable_id.setdefault(stable_id, []).append(source_index)
|
||||
stable_ids = list(targets_by_stable_id)
|
||||
stable_ids.extend(
|
||||
stable_id
|
||||
for stable_id in state_by_stable_id
|
||||
if stable_id not in targets_by_stable_id
|
||||
)
|
||||
duplicate_stable_target_indexes = set()
|
||||
duplicate_stable_source_indexes = set()
|
||||
for stable_id in stable_ids:
|
||||
target_indexes = targets_by_stable_id.get(stable_id, [])
|
||||
source_indexes = state_by_stable_id.get(stable_id, [])
|
||||
if len(target_indexes) > 1 or len(source_indexes) > 1:
|
||||
# Durable row ids are stronger provenance than a repeated stable
|
||||
# message id. Defer this bucket's quarantine until after the row-id
|
||||
# tier so unique row-id pairs can still resolve it one-to-one.
|
||||
duplicate_stable_target_indexes.update(target_indexes)
|
||||
duplicate_stable_source_indexes.update(source_indexes)
|
||||
|
||||
for stable_id in stable_ids:
|
||||
target_indexes = [
|
||||
index
|
||||
for index in targets_by_stable_id.get(stable_id, ())
|
||||
if index not in used_targets
|
||||
]
|
||||
source_indexes = [
|
||||
index
|
||||
for index in state_by_stable_id.get(stable_id, ())
|
||||
if index not in used_sources
|
||||
]
|
||||
# A singleton stable id present on only one side remains eligible for
|
||||
# stronger durable-row, timestamp, or content matching below.
|
||||
if len(target_indexes) != 1 or len(source_indexes) != 1:
|
||||
continue
|
||||
target_index = target_indexes[0]
|
||||
source_index = source_indexes[0]
|
||||
if not _message_identity_compatible(sidecar[target_index], state[source_index]):
|
||||
used_targets.add(target_index)
|
||||
used_sources.add(source_index)
|
||||
continue
|
||||
target_api_content = _session_message_api_content_key(sidecar[target_index])
|
||||
source_api_content = _session_message_api_content_key(state[source_index])
|
||||
used_targets.add(target_index)
|
||||
used_sources.add(source_index)
|
||||
if (
|
||||
target_api_content is not None
|
||||
and source_api_content is not None
|
||||
and target_api_content != source_api_content
|
||||
):
|
||||
continue
|
||||
_copy_api_content_sidecar(sidecar[target_index], state[source_index])
|
||||
|
||||
# 2. Durable row identity. This path is unambiguous only when every
|
||||
# alias agrees, each row id occurs once on each side, and visible content
|
||||
# is compatible. Any duplicate/conflicting id is consumed and rejected;
|
||||
# it must not fall through to a weaker tier. A unique row id with
|
||||
# conflicting non-empty sidecars is left for the append-only merge, which
|
||||
# can preserve both authoritative payloads without guessing.
|
||||
targets_by_row_id = {}
|
||||
for index, message in enumerate(sidecar):
|
||||
if index in used_targets:
|
||||
continue
|
||||
row_id, valid = _state_db_row_identity_details(message)
|
||||
if not valid:
|
||||
used_targets.add(index)
|
||||
elif row_id is not None:
|
||||
targets_by_row_id.setdefault(row_id, []).append(index)
|
||||
state_by_row_id = {}
|
||||
for source_index, message in enumerate(state):
|
||||
if source_index in used_sources:
|
||||
continue
|
||||
row_id, valid = _state_db_row_identity_details(message)
|
||||
if not valid:
|
||||
used_sources.add(source_index)
|
||||
elif row_id is not None:
|
||||
state_by_row_id.setdefault(row_id, []).append(source_index)
|
||||
|
||||
# Reject duplicate source buckets independently of the target map. A
|
||||
# source-only duplicate id has no target loop iteration to consume it, so
|
||||
# without this pre-pass it would descend into timestamp/metadata-free
|
||||
# matching and attach one arbitrary provider sidecar.
|
||||
for source_indexes in state_by_row_id.values():
|
||||
if len(source_indexes) > 1:
|
||||
used_sources.update(source_indexes)
|
||||
|
||||
for row_id, original_target_indexes in targets_by_row_id.items():
|
||||
original_source_indexes = state_by_row_id.get(row_id, [])
|
||||
target_indexes = [
|
||||
index for index in original_target_indexes if index not in used_targets
|
||||
]
|
||||
source_indexes = [
|
||||
index for index in original_source_indexes if index not in used_sources
|
||||
]
|
||||
if len(original_target_indexes) != 1 or len(original_source_indexes) != 1:
|
||||
used_targets.update(target_indexes)
|
||||
used_sources.update(source_indexes)
|
||||
continue
|
||||
if len(target_indexes) != 1 or len(source_indexes) != 1:
|
||||
continue
|
||||
target_index = target_indexes[0]
|
||||
source_index = source_indexes[0]
|
||||
if not _message_identity_compatible(sidecar[target_index], state[source_index]):
|
||||
# Row ids never override contradictory stable ids, aliases, roles,
|
||||
# or visible content. Quarantine both sides before fallback.
|
||||
used_targets.add(target_index)
|
||||
used_sources.add(source_index)
|
||||
continue
|
||||
target_api_content = _session_message_api_content_key(sidecar[target_index])
|
||||
source_api_content = _session_message_api_content_key(state[source_index])
|
||||
if (
|
||||
target_api_content is not None
|
||||
and source_api_content is not None
|
||||
and target_api_content != source_api_content
|
||||
):
|
||||
# A durable row id is not sufficient to reconcile two already
|
||||
# authoritative provider payloads. Quarantine both rows so a
|
||||
# weaker timestamp/content tier cannot attach one payload to the
|
||||
# other row.
|
||||
used_targets.add(target_index)
|
||||
used_sources.add(source_index)
|
||||
continue
|
||||
used_targets.add(target_index)
|
||||
used_sources.add(source_index)
|
||||
_copy_api_content_sidecar(sidecar[target_index], state[source_index])
|
||||
|
||||
# Any duplicate stable-id rows left unresolved by the authoritative row-id
|
||||
# tier are ambiguous and must not fall through to timestamps/content.
|
||||
used_targets.update(
|
||||
index for index in duplicate_stable_target_indexes if index not in used_targets
|
||||
)
|
||||
used_sources.update(
|
||||
index for index in duplicate_stable_source_indexes if index not in used_sources
|
||||
)
|
||||
|
||||
# 3. Exact role + timestamp. Equal timestamps are not provenance; resolve
|
||||
# only mutually-unique compatible pairs inside a bucket and never guess by
|
||||
# list order when a relationship is ambiguous.
|
||||
sidecar_by_exact_timestamp = {}
|
||||
state_by_exact_timestamp = {}
|
||||
for index, message in enumerate(sidecar):
|
||||
if index in used_targets:
|
||||
continue
|
||||
role = _message_sidecar_role(message)
|
||||
timestamp = _message_exact_timestamp(message)
|
||||
_, valid = _state_db_row_identity_details(message)
|
||||
if valid and role is not None and timestamp is not None:
|
||||
sidecar_by_exact_timestamp.setdefault((role, timestamp), []).append(index)
|
||||
for source_index, message in enumerate(state):
|
||||
if source_index in used_sources:
|
||||
continue
|
||||
role = _message_sidecar_role(message)
|
||||
timestamp = _message_exact_timestamp(message)
|
||||
_, valid = _state_db_row_identity_details(message)
|
||||
if valid and role is not None and timestamp is not None:
|
||||
state_by_exact_timestamp.setdefault((role, timestamp), []).append((source_index, message))
|
||||
for key, state_rows in state_by_exact_timestamp.items():
|
||||
target_indexes = [
|
||||
index
|
||||
for index in sidecar_by_exact_timestamp.get(key, ())
|
||||
if index not in used_targets
|
||||
]
|
||||
if not target_indexes:
|
||||
# No exact-timestamp target is not an ambiguity: leave the source
|
||||
# row available for the documented same-second/content tiers.
|
||||
continue
|
||||
|
||||
# Use the same mutually-unique compatibility graph as the same-second
|
||||
# tier. This resolves distinct content pairs inside a repeated exact
|
||||
# timestamp bucket without guessing by list order.
|
||||
compatible_targets_by_source = {}
|
||||
for source_index, source_message in state_rows:
|
||||
if source_index in used_sources:
|
||||
continue
|
||||
compatible_targets_by_source[source_index] = [
|
||||
target_index
|
||||
for target_index in target_indexes
|
||||
if target_index not in used_targets
|
||||
and _message_identity_compatible(sidecar[target_index], source_message)
|
||||
and (
|
||||
_session_message_api_content_key(sidecar[target_index]) is None
|
||||
or _session_message_api_content_key(sidecar[target_index])
|
||||
== _session_message_api_content_key(source_message)
|
||||
)
|
||||
]
|
||||
|
||||
compatible_sources_by_target = {}
|
||||
for source_index, candidates in compatible_targets_by_source.items():
|
||||
for target_index in candidates:
|
||||
compatible_sources_by_target.setdefault(target_index, []).append(source_index)
|
||||
|
||||
for source_index, candidates in compatible_targets_by_source.items():
|
||||
if len(candidates) != 1:
|
||||
continue
|
||||
target_index = candidates[0]
|
||||
if len(compatible_sources_by_target.get(target_index, ())) != 1:
|
||||
continue
|
||||
if source_index in used_sources or target_index in used_targets:
|
||||
continue
|
||||
used_sources.add(source_index)
|
||||
used_targets.add(target_index)
|
||||
_copy_api_content_sidecar(sidecar[target_index], state[source_index])
|
||||
|
||||
# 4. Same-second sub-second drift. The sidecar JSON and state.db can
|
||||
# record one logical turn with different fractional timestamps while still
|
||||
# landing in the same wall-clock second. Do not use the truncated second
|
||||
# as identity by itself: require a unique *compatible* visible row on each
|
||||
# side. A candidate is compatible only when role + normalized visible
|
||||
# content and all available provenance agree, and neither side carries a
|
||||
# conflicting provider sidecar.
|
||||
# This keeps repeated text, contradictory provenance, and conflicting wire
|
||||
# payloads fail-closed instead of guessing by list order.
|
||||
sidecar_by_same_second = {}
|
||||
state_by_same_second = {}
|
||||
for index, message in enumerate(sidecar):
|
||||
if index in used_targets:
|
||||
continue
|
||||
role = _message_sidecar_role(message)
|
||||
timestamp = _message_exact_timestamp(message)
|
||||
row_id, valid = _state_db_row_identity_details(message)
|
||||
if valid and role is not None and timestamp is not None:
|
||||
second = _normalized_message_timestamp_for_key(timestamp)
|
||||
sidecar_by_same_second.setdefault((role, second), []).append(index)
|
||||
for source_index, message in enumerate(state):
|
||||
if source_index in used_sources:
|
||||
continue
|
||||
role = _message_sidecar_role(message)
|
||||
timestamp = _message_exact_timestamp(message)
|
||||
row_id, valid = _state_db_row_identity_details(message)
|
||||
if valid and role is not None and timestamp is not None:
|
||||
second = _normalized_message_timestamp_for_key(timestamp)
|
||||
state_by_same_second.setdefault((role, second), []).append(source_index)
|
||||
|
||||
for key, state_indexes in state_by_same_second.items():
|
||||
target_indexes = [
|
||||
index
|
||||
for index in sidecar_by_same_second.get(key, ())
|
||||
if index not in used_targets
|
||||
]
|
||||
if not target_indexes:
|
||||
continue
|
||||
|
||||
compatible_targets_by_source = {}
|
||||
for source_index in state_indexes:
|
||||
if source_index in used_sources:
|
||||
continue
|
||||
source_message = state[source_index]
|
||||
compatible_targets_by_source[source_index] = [
|
||||
target_index
|
||||
for target_index in target_indexes
|
||||
if target_index not in used_targets
|
||||
and _message_identity_compatible(sidecar[target_index], source_message)
|
||||
and (
|
||||
_session_message_api_content_key(sidecar[target_index]) is None
|
||||
or _session_message_api_content_key(sidecar[target_index])
|
||||
== _session_message_api_content_key(source_message)
|
||||
)
|
||||
]
|
||||
|
||||
compatible_sources_by_target = {}
|
||||
for source_index, candidates in compatible_targets_by_source.items():
|
||||
for target_index in candidates:
|
||||
compatible_sources_by_target.setdefault(target_index, []).append(source_index)
|
||||
|
||||
# Accept only one-to-one compatible pairs. This deliberately does not
|
||||
# zip a bucket: repeated same-text rows or equal timestamps remain
|
||||
# distinct until durable provenance resolves them.
|
||||
for source_index, candidates in compatible_targets_by_source.items():
|
||||
if len(candidates) != 1:
|
||||
continue
|
||||
target_index = candidates[0]
|
||||
if len(compatible_sources_by_target.get(target_index, ())) != 1:
|
||||
continue
|
||||
if source_index in used_sources or target_index in used_targets:
|
||||
continue
|
||||
used_sources.add(source_index)
|
||||
used_targets.add(target_index)
|
||||
_copy_api_content_sidecar(sidecar[target_index], state[source_index])
|
||||
|
||||
# 5. Role + visible-content fallback tolerates mixed timestamp metadata,
|
||||
# but only when the candidate relationship is mutually unique. Repeated
|
||||
# identical rows have no principled ordering and remain unattached.
|
||||
sidecar_by_visible_content = {}
|
||||
state_by_visible_content = {}
|
||||
for index, message in enumerate(sidecar):
|
||||
if index in used_targets:
|
||||
continue
|
||||
role = _message_sidecar_role(message)
|
||||
_, row_id_valid = _state_db_row_identity_details(message)
|
||||
_, stable_id_valid = _stable_message_identity_details(message)
|
||||
_, timestamp_valid = _message_exact_timestamp_details(message)
|
||||
if (
|
||||
row_id_valid
|
||||
and stable_id_valid
|
||||
and timestamp_valid
|
||||
and role is not None
|
||||
and _message_visible_content_key(message) is not None
|
||||
):
|
||||
sidecar_by_visible_content.setdefault(_message_visible_content_key(message), []).append(index)
|
||||
for source_index, message in enumerate(state):
|
||||
if source_index in used_sources:
|
||||
continue
|
||||
role = _message_sidecar_role(message)
|
||||
_, row_id_valid = _state_db_row_identity_details(message)
|
||||
_, stable_id_valid = _stable_message_identity_details(message)
|
||||
_, timestamp_valid = _message_exact_timestamp_details(message)
|
||||
if (
|
||||
row_id_valid
|
||||
and stable_id_valid
|
||||
and timestamp_valid
|
||||
and role is not None
|
||||
and _message_visible_content_key(message) is not None
|
||||
):
|
||||
state_by_visible_content.setdefault(_message_visible_content_key(message), []).append(source_index)
|
||||
for visible_key, target_indexes in sidecar_by_visible_content.items():
|
||||
source_indexes = state_by_visible_content.get(visible_key, [])
|
||||
if len(target_indexes) != 1 or len(source_indexes) != 1:
|
||||
continue
|
||||
target_index = target_indexes[0]
|
||||
source_index = source_indexes[0]
|
||||
target_timestamp, target_timestamp_valid = _message_exact_timestamp_details(
|
||||
sidecar[target_index]
|
||||
)
|
||||
source_timestamp, source_timestamp_valid = _message_exact_timestamp_details(
|
||||
state[source_index]
|
||||
)
|
||||
if (
|
||||
not target_timestamp_valid
|
||||
or not source_timestamp_valid
|
||||
or (target_timestamp is not None and source_timestamp is not None)
|
||||
):
|
||||
continue
|
||||
if not _message_identity_compatible(sidecar[target_index], state[source_index]):
|
||||
continue
|
||||
target_api_content = _session_message_api_content_key(sidecar[target_index])
|
||||
source_api_content = _session_message_api_content_key(state[source_index])
|
||||
if (
|
||||
target_api_content is not None
|
||||
and source_api_content is not None
|
||||
and target_api_content != source_api_content
|
||||
):
|
||||
continue
|
||||
used_targets.add(target_index)
|
||||
used_sources.add(source_index)
|
||||
_copy_api_content_sidecar(sidecar[target_index], state[source_index])
|
||||
|
||||
|
||||
def _session_message_dedup_key(msg: dict):
|
||||
"""Like _session_message_merge_key but preserves full-precision timestamp.
|
||||
|
||||
@@ -8398,13 +9028,15 @@ def _session_message_dedup_key(msg: dict):
|
||||
return ("non_dict", repr(msg))
|
||||
message_identity = msg.get("id") or msg.get("message_id")
|
||||
if message_identity:
|
||||
return ("message_id", str(message_identity))
|
||||
return _session_message_key_with_sidecar(
|
||||
("message_id", str(message_identity)), msg
|
||||
)
|
||||
# Include tool_calls in the key so assistant messages that carry
|
||||
# different tool invocations (but identical empty content/timestamp)
|
||||
# are never collapsed into one. (#3346 regression)
|
||||
_tc = msg.get("tool_calls")
|
||||
_tc_key = json.dumps(_tc, sort_keys=True, default=str) if _tc else ""
|
||||
return (
|
||||
return _session_message_key_with_sidecar((
|
||||
"legacy",
|
||||
str(msg.get("role") or ""),
|
||||
str(msg.get("content") or ""),
|
||||
@@ -8412,7 +9044,7 @@ def _session_message_dedup_key(msg: dict):
|
||||
str(msg.get("tool_call_id") or ""),
|
||||
str(msg.get("tool_name") or msg.get("name") or ""),
|
||||
_tc_key,
|
||||
)
|
||||
), msg)
|
||||
|
||||
|
||||
def _normalized_session_message_content(msg: dict) -> str:
|
||||
@@ -8450,12 +9082,12 @@ def _session_message_content_key(msg: dict):
|
||||
content = " ".join(
|
||||
_strip_workspace_prefix(content, include_legacy=True).split()
|
||||
)
|
||||
return (
|
||||
return _session_message_key_with_sidecar((
|
||||
role,
|
||||
content,
|
||||
str(msg.get("tool_call_id") or ""),
|
||||
str(msg.get("tool_name") or msg.get("name") or ""),
|
||||
)
|
||||
), msg)
|
||||
|
||||
|
||||
def _session_message_visible_key(msg: dict):
|
||||
@@ -8467,11 +9099,11 @@ def _session_message_visible_key(msg: dict):
|
||||
# ("assistant", "") and the merge treats state.db rows as replays.
|
||||
_tc = msg.get("tool_calls")
|
||||
_tc_key = json.dumps(_tc, sort_keys=True, default=str) if _tc else ""
|
||||
return (
|
||||
return _session_message_key_with_sidecar((
|
||||
str(msg.get("role") or ""),
|
||||
_normalized_session_message_content(msg),
|
||||
_tc_key,
|
||||
)
|
||||
), msg)
|
||||
|
||||
|
||||
def _build_visible_duplicate_lookup(visible_keys: set[tuple]) -> dict:
|
||||
@@ -8496,6 +9128,7 @@ def _matching_visible_duplicate(visible_key: tuple, visible_keys: set[tuple], lo
|
||||
return visible_key
|
||||
role = visible_key[0]
|
||||
content = visible_key[1] if len(visible_key) > 1 else ""
|
||||
sidecar = visible_key[3] if len(visible_key) > 3 else None
|
||||
if not content:
|
||||
return None
|
||||
if lookup is None:
|
||||
@@ -8505,7 +9138,8 @@ def _matching_visible_duplicate(visible_key: tuple, visible_keys: set[tuple], lo
|
||||
for existing_key in lookup.get("by_role", {}).get(role, []):
|
||||
existing_role = existing_key[0]
|
||||
existing_content = existing_key[1] if len(existing_key) > 1 else ""
|
||||
if role != existing_role or not existing_content:
|
||||
existing_sidecar = existing_key[3] if len(existing_key) > 3 else None
|
||||
if role != existing_role or sidecar != existing_sidecar or not existing_content:
|
||||
continue
|
||||
# Exact visible-key equality was checked above. For very large payloads
|
||||
# (tool logs / request dumps), Python-in substring and fuzzy-token
|
||||
@@ -8582,6 +9216,12 @@ def state_db_delta_after_context(sidecar_context: list, state_messages: list) ->
|
||||
if not sidecar_context or not state_messages:
|
||||
return state_messages
|
||||
|
||||
# Attach trusted state.db sidecars before computing context keys. A missing
|
||||
# sidecar is not a wildcard: once attached, it participates in every
|
||||
# duplicate/prefix decision below and distinct provider bytes remain
|
||||
# distinct rows.
|
||||
_reconcile_api_content_sidecars(sidecar_context, state_messages)
|
||||
|
||||
# Recovered interrupted turns are special: the visible interruption marker
|
||||
# is synthetic, so the recovered user turn should still count as a mirrored
|
||||
# prefix when it is the actual aligned prefix row.
|
||||
@@ -8836,6 +9476,46 @@ def merge_session_messages_append_only(
|
||||
"""
|
||||
sidecar_messages = list(sidecar_messages or [])
|
||||
state_messages = list(state_messages or [])
|
||||
_reconcile_api_content_sidecars(sidecar_messages, state_messages)
|
||||
# The reconciler's quarantine sets are invocation-local. Mirror the
|
||||
# identity-bucket guards here because this append-only merge has its own
|
||||
# row-id fast path that must not re-admit a row isolated above.
|
||||
sidecar_row_id_counts = collections.Counter()
|
||||
state_row_id_counts = collections.Counter()
|
||||
for message in sidecar_messages:
|
||||
row_id, row_id_valid = _state_db_row_identity_details(message)
|
||||
if row_id_valid and row_id is not None:
|
||||
sidecar_row_id_counts[row_id] += 1
|
||||
for message in state_messages:
|
||||
row_id, row_id_valid = _state_db_row_identity_details(message)
|
||||
if row_id_valid and row_id is not None:
|
||||
state_row_id_counts[row_id] += 1
|
||||
|
||||
def _row_id_fast_path_allowed(existing, incoming) -> bool:
|
||||
if not isinstance(existing, dict) or not isinstance(incoming, dict):
|
||||
return False
|
||||
existing_row_id, existing_row_id_valid = _state_db_row_identity_details(existing)
|
||||
incoming_row_id, incoming_row_id_valid = _state_db_row_identity_details(incoming)
|
||||
if (
|
||||
not existing_row_id_valid
|
||||
or not incoming_row_id_valid
|
||||
or existing_row_id is None
|
||||
or existing_row_id != incoming_row_id
|
||||
):
|
||||
return False
|
||||
if (
|
||||
sidecar_row_id_counts.get(existing_row_id, 0) > 1
|
||||
or state_row_id_counts.get(existing_row_id, 0) > 1
|
||||
):
|
||||
return False
|
||||
_, existing_timestamp_valid = _message_exact_timestamp_details(existing)
|
||||
_, incoming_timestamp_valid = _message_exact_timestamp_details(incoming)
|
||||
return (
|
||||
existing_timestamp_valid
|
||||
and incoming_timestamp_valid
|
||||
and _message_identity_compatible(existing, incoming)
|
||||
)
|
||||
|
||||
# Per-invocation cache keyed by message identity. Sidecar/state message objects
|
||||
# are retained for this call, and this function does not mutate key-defining
|
||||
# fields before each helper call.
|
||||
@@ -8977,6 +9657,8 @@ def merge_session_messages_append_only(
|
||||
merged_by_message_key = {}
|
||||
merged_by_dedup_key = {}
|
||||
merged_by_visible_key = {}
|
||||
merged_by_row_id = {}
|
||||
ambiguous_row_ids = set()
|
||||
max_sidecar_timestamp = None
|
||||
|
||||
def _remember_merged_message(message):
|
||||
@@ -8985,6 +9667,12 @@ def merge_session_messages_append_only(
|
||||
merged_by_message_key.setdefault(_cached_message_key(message, "merge"), message)
|
||||
merged_by_dedup_key.setdefault(_cached_message_key(message, "dedup"), message)
|
||||
merged_by_visible_key.setdefault(_cached_message_key(message, "visible"), message)
|
||||
row_id, row_id_valid = _state_db_row_identity_details(message)
|
||||
if row_id_valid and row_id is not None:
|
||||
if row_id in merged_by_row_id:
|
||||
ambiguous_row_ids.add(row_id)
|
||||
else:
|
||||
merged_by_row_id[row_id] = message
|
||||
|
||||
for msg in sidecar_messages:
|
||||
timestamp = _message_timestamp_as_float(msg)
|
||||
@@ -9051,6 +9739,32 @@ def merge_session_messages_append_only(
|
||||
# are caught by the dedup guard (#3346).
|
||||
seen_dedup_keys.add(dedup_key)
|
||||
continue
|
||||
row_id, row_id_valid = _state_db_row_identity_details(msg)
|
||||
row_id_sidecar_conflict = False
|
||||
existing = (
|
||||
merged_by_row_id.get(row_id)
|
||||
if row_id_valid and row_id is not None
|
||||
else None
|
||||
)
|
||||
if (
|
||||
row_id_valid
|
||||
and row_id is not None
|
||||
and existing is not None
|
||||
and row_id not in ambiguous_row_ids
|
||||
and _row_id_fast_path_allowed(existing, msg)
|
||||
):
|
||||
existing_api_content = _session_message_api_content_key(existing)
|
||||
incoming_api_content = _session_message_api_content_key(msg)
|
||||
row_id_sidecar_conflict = (
|
||||
existing_api_content is not None
|
||||
and incoming_api_content is not None
|
||||
and existing_api_content != incoming_api_content
|
||||
)
|
||||
if not row_id_sidecar_conflict:
|
||||
if existing_api_content is None and incoming_api_content is not None:
|
||||
_copy_api_content_sidecar(existing, msg)
|
||||
_merge_session_display_metadata(existing, msg)
|
||||
continue
|
||||
# Skip rows ABOVE the watermark only while the sidecar has NOT advanced
|
||||
# past the watermark. Because Session.save() no longer auto-clears the
|
||||
# watermark, an unconditional `timestamp > watermark` skip would become
|
||||
@@ -9185,6 +9899,7 @@ def merge_session_messages_append_only(
|
||||
and max_sidecar_timestamp is not None
|
||||
and timestamp is not None
|
||||
and timestamp <= max_sidecar_timestamp
|
||||
and not row_id_sidecar_conflict
|
||||
):
|
||||
# When a truncation watermark is active and the sidecar holds only
|
||||
# the edited user checkpoint, state.db may contain an assistant/tool
|
||||
|
||||
Reference in new issue
Block a user