fix(chat): preserve submitted image turns on stale recovery
When a dead stream is reconciled from state.db, retain the submitted prompt and attachment in the sidecar while keeping the Agent-enriched row in model context. Use one parent snapshot for full-session conflict projection so the sidecar owner is stable across the display merge. Assisted-by: Hermes Agent:gpt-6-sol Assisted-by: Codex:gpt-6-luna Assisted-by: Claude Code:claude-opus-5-5
This commit is contained in:
1 parent
4f2bdd2dff
commit
d07c60a92a
5 files changed
+224
-32
No files matched your search
@@ -433,11 +433,23 @@ def test_get_session_projects_parent_only_payload_conflict_without_losing_parent
|
||||
response.update(payload=payload, status=status) or payload
|
||||
),
|
||||
)
|
||||
original_get_session = routes.get_session
|
||||
parent_loads = []
|
||||
|
||||
def get_session_with_fresh_parent(requested_id, metadata_only=False):
|
||||
if requested_id == parent_id:
|
||||
parent_session = models.Session.load(parent_id)
|
||||
parent_loads.append(parent_session)
|
||||
return parent_session
|
||||
return original_get_session(requested_id, metadata_only=metadata_only)
|
||||
|
||||
monkeypatch.setattr(routes, "get_session", get_session_with_fresh_parent)
|
||||
routes._handle_session_get(
|
||||
None,
|
||||
SimpleNamespace(path="/api/session", query=f"session_id={session_id}&resolve_model=0"),
|
||||
)
|
||||
|
||||
assert len(parent_loads) == 1
|
||||
assert response["status"] == 200
|
||||
public_messages = response["payload"]["session"]["messages"]
|
||||
mirror_rows = [message for message in public_messages if message.get("content") == mirror]
|
||||
@@ -452,6 +464,9 @@ def test_get_session_projects_parent_only_payload_conflict_without_losing_parent
|
||||
prefer_context=True,
|
||||
state_messages=state_rows,
|
||||
)
|
||||
assert "Unrelated parent-only row" not in [
|
||||
message.get("content") for message in replay_context
|
||||
]
|
||||
replay_rows = [
|
||||
message for message in _sanitize_messages_for_agent(replay_context)
|
||||
if message.get("content") == mirror
|
||||
|
||||
@@ -818,6 +818,83 @@ def test_get_session_syncs_sidecar_from_newer_state_db_even_when_stream_not_term
|
||||
assert reloaded.messages[-1]["content"] == "latest live progress"
|
||||
|
||||
|
||||
def test_get_session_sync_keeps_pending_image_prompt_when_agent_row_is_proven(monkeypatch, hermes_home):
|
||||
sid = "state_newer_pending_image_sid"
|
||||
stream_id = "dead_pending_image_stream"
|
||||
timestamp = time.time() - models._REPAIR_STALE_PENDING_GRACE_SECONDS - 5
|
||||
prompt = "Describe the attached image"
|
||||
attachment = {"name": "sample.png", "mime": "image/png", "is_image": True}
|
||||
agent_only_note = "Agent-only recall note"
|
||||
agent_user_content = [
|
||||
{"type": "text", "text": prompt},
|
||||
{"type": "image_url", "image_url": {"url": "data:image/png;base64,YWJj"}},
|
||||
{"type": "text", "text": agent_only_note},
|
||||
]
|
||||
previous_messages = [
|
||||
{"role": "user", "content": "old question", "timestamp": timestamp - 2},
|
||||
{"role": "assistant", "content": "old answer", "timestamp": timestamp - 1},
|
||||
]
|
||||
state_messages = [
|
||||
*previous_messages,
|
||||
{"role": "user", "content": agent_user_content, "timestamp": timestamp},
|
||||
{"role": "assistant", "content": "recovered assistant tail", "timestamp": timestamp + 1},
|
||||
]
|
||||
s = Session(
|
||||
session_id=sid,
|
||||
title="Pending image recovery",
|
||||
messages=previous_messages,
|
||||
context_messages=previous_messages,
|
||||
active_stream_id=stream_id,
|
||||
pending_user_message=prompt,
|
||||
pending_attachments=[attachment],
|
||||
pending_started_at=timestamp,
|
||||
pending_user_source="webui",
|
||||
_webui_pending_user_timestamp_identity=(stream_id, timestamp),
|
||||
)
|
||||
s.save()
|
||||
models.SESSIONS.pop(sid, None)
|
||||
|
||||
monkeypatch.setattr(
|
||||
models,
|
||||
"get_state_db_session_summary",
|
||||
lambda sid_arg, profile=None: {
|
||||
"message_count": len(state_messages),
|
||||
"last_message_at": timestamp + 1,
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
models,
|
||||
"get_state_db_session_messages",
|
||||
lambda sid_arg, **kwargs: list(state_messages),
|
||||
)
|
||||
|
||||
models.get_session(sid)
|
||||
reloaded = Session.load(sid)
|
||||
|
||||
submitted_rows = [
|
||||
message for message in reloaded.messages
|
||||
if message.get("role") == "user" and message.get("content") == prompt
|
||||
]
|
||||
assert len(submitted_rows) == 1
|
||||
assert submitted_rows[0]["timestamp"] == timestamp
|
||||
assert submitted_rows[0]["attachments"] == [attachment]
|
||||
assert reloaded.messages[-1] == state_messages[-1]
|
||||
assert agent_only_note not in str(reloaded.messages)
|
||||
|
||||
context_rows = models.reconciled_state_db_messages_for_session(
|
||||
reloaded,
|
||||
prefer_context=True,
|
||||
state_messages=state_messages,
|
||||
)
|
||||
enriched_rows = [
|
||||
message for message in context_rows
|
||||
if message.get("role") == "user" and message.get("timestamp") == timestamp
|
||||
]
|
||||
assert len(enriched_rows) == 1
|
||||
assert enriched_rows[0]["content"] == agent_user_content
|
||||
assert agent_only_note in str(enriched_rows[0]["content"])
|
||||
|
||||
|
||||
def test_get_session_does_not_sync_while_stream_is_still_live(monkeypatch):
|
||||
"""A still-running worker owns its own writeback; do not race it.
|
||||
|
||||
@@ -884,6 +961,7 @@ def test_sync_save_failure_does_not_mutate_session(monkeypatch):
|
||||
"""
|
||||
sid = "state_sync_save_fail_sid"
|
||||
stream_id = "stream_save_fail_no_done"
|
||||
attachment = {"name": "sample.png", "mime": "image/png", "is_image": True}
|
||||
s = Session(
|
||||
session_id=sid,
|
||||
title="Save failure",
|
||||
@@ -893,7 +971,10 @@ def test_sync_save_failure_does_not_mutate_session(monkeypatch):
|
||||
],
|
||||
active_stream_id=stream_id,
|
||||
pending_user_message="new request",
|
||||
pending_attachments=[attachment],
|
||||
pending_started_at=102.0,
|
||||
pending_user_source="webui",
|
||||
_webui_pending_user_timestamp_identity=(stream_id, 102.0),
|
||||
)
|
||||
s.save()
|
||||
|
||||
@@ -926,7 +1007,13 @@ def test_sync_save_failure_does_not_mutate_session(monkeypatch):
|
||||
assert result is False
|
||||
assert s.active_stream_id == stream_id
|
||||
assert s.pending_user_message == "new request"
|
||||
assert s.pending_attachments == [attachment]
|
||||
assert [m["content"] for m in s.messages] == ["old question", "old answer"]
|
||||
reloaded = Session.load(sid)
|
||||
assert reloaded.active_stream_id == stream_id
|
||||
assert reloaded.pending_user_message == "new request"
|
||||
assert reloaded.pending_attachments == s.pending_attachments
|
||||
assert [m["content"] for m in reloaded.messages] == ["old question", "old answer"]
|
||||
|
||||
|
||||
def test_sync_persists_recovered_state_db_tail_when_stream_dead(monkeypatch):
|
||||
|
||||
Reference in new issue
Block a user