fix: preserve Chatwoot message visibility and identity

This commit is contained in:
plx
2026-08-14 14:08:23 +00:00
parent 475ff15b9b
commit c4d55d37ed
10 changed files with 531 additions and 71 deletions

View File

@@ -25,6 +25,92 @@ def _uuid_or_none(value):
return value or None
def _lock_source_identity(conn: Any, source_system: str, source_event_id: str | None) -> None:
if source_event_id:
conn.execute(text("SELECT pg_advisory_xact_lock(hashtext(:identity))"), {
"identity": f"message:{source_system}:{source_event_id}",
})
def save_inbound_message(
*,
request: AnalyzeRequest,
raw_event_id: Optional[str] = None,
source_event_id: Optional[str] = None,
raw_body: Optional[str] = None,
clean_body: Optional[str] = None,
) -> Tuple[str, Optional[str]]:
"""Persist the canonical inbound message before classification.
Chatwoot identity comes from its raw event when callers do not pass it.
The advisory lock makes retries safe even while older databases are waiting
for the canonical identity index migration.
"""
if not settings.clientflow_persist:
return "", source_event_id
source_system = request.source or "manual"
conversation_id = request.conversation_id or "manual"
with engine.begin() as conn:
if raw_event_id:
raw = conn.execute(text("""
SELECT source_system, source_event_id
FROM raw_events WHERE id = CAST(:raw_event_id AS UUID)
"""), {"raw_event_id": raw_event_id}).mappings().first()
if raw:
source_system = str(raw.get("source_system") or source_system)
source_event_id = source_event_id or raw.get("source_event_id")
_lock_source_identity(conn, source_system, source_event_id)
existing = None
if source_event_id:
existing = conn.execute(text("""
SELECT id::text FROM messages
WHERE source_system=:source_system AND source_event_id=:source_event_id
LIMIT 1
"""), {"source_system": source_system, "source_event_id": source_event_id}).first()
if not existing and raw_event_id:
existing = conn.execute(text("""
SELECT id::text FROM messages WHERE raw_event_id=CAST(:raw_event_id AS UUID) LIMIT 1
"""), {"raw_event_id": raw_event_id}).first()
if existing:
message_id = str(existing[0])
conn.execute(text("""
UPDATE messages SET
source_event_id=COALESCE(source_event_id, :source_event_id),
raw_event_id=COALESCE(raw_event_id, CAST(:raw_event_id AS UUID)),
conversation_id=COALESCE(NULLIF(conversation_id,''), :conversation_id),
contact_id=COALESCE(NULLIF(contact_id,''), :contact_id),
raw_body=COALESCE(NULLIF(raw_body,''), :raw_body),
clean_body=COALESCE(NULLIF(clean_body,''), :clean_body)
WHERE id=CAST(:message_id AS UUID)
"""), {"message_id": message_id, "source_event_id": source_event_id,
"raw_event_id": raw_event_id, "conversation_id": conversation_id,
"contact_id": request.contact_id, "raw_body": raw_body or request.last_customer_message,
"clean_body": clean_body or request.last_customer_message})
else:
row = conn.execute(text("""
INSERT INTO messages (
raw_event_id, source_system, source_event_id, conversation_id,
contact_id, direction, raw_body, clean_body, previous_context, metadata
) VALUES (
CAST(:raw_event_id AS UUID), :source_system, :source_event_id,
:conversation_id, :contact_id, 'inbound', :raw_body, :clean_body,
:previous_context, CAST(:metadata AS JSONB)
) RETURNING id::text
"""), {"raw_event_id": raw_event_id, "source_system": source_system,
"source_event_id": source_event_id, "conversation_id": conversation_id,
"contact_id": request.contact_id, "raw_body": raw_body or request.last_customer_message,
"clean_body": clean_body or request.last_customer_message,
"previous_context": request.previous_context,
"metadata": _json({"current_state": request.current_state.model_dump()})}).first()
message_id = str(row[0])
if raw_event_id:
conn.execute(text("""UPDATE raw_events SET message_id=CAST(:message_id AS UUID)
WHERE id=CAST(:raw_event_id AS UUID)"""), {
"message_id": message_id, "raw_event_id": raw_event_id,
})
return message_id, source_event_id
def save_action_run(
*,
request: AnalyzeRequest,
@@ -37,53 +123,30 @@ def save_action_run(
raw_body: Optional[str] = None,
clean_body: Optional[str] = None,
raw_event_id: Optional[str] = None,
source_event_id: Optional[str] = None,
message_id: Optional[str] = None,
) -> Tuple[str, str]:
if not settings.clientflow_persist:
return "", ""
conversation_id = request.conversation_id or "manual"
with engine.begin() as conn:
message_row = conn.execute(text("""
INSERT INTO messages (
raw_event_id,
source_system,
source_event_id,
conversation_id,
contact_id,
direction,
raw_body,
clean_body,
previous_context,
metadata
)
VALUES (
CAST(:raw_event_id AS UUID),
:source_system,
NULL,
:conversation_id,
:contact_id,
'inbound',
:raw_body,
:clean_body,
:previous_context,
CAST(:metadata AS JSONB)
)
RETURNING id::text
"""), {
"raw_event_id": raw_event_id,
"source_system": request.source or "manual",
"conversation_id": conversation_id,
"contact_id": request.contact_id,
"raw_body": raw_body or request.last_customer_message,
"clean_body": clean_body or request.last_customer_message,
"previous_context": request.previous_context,
"metadata": _json({
"current_state": request.current_state.model_dump(),
}),
}).fetchone()
if not message_id:
message_id, source_event_id = save_inbound_message(
request=request, raw_event_id=raw_event_id, source_event_id=source_event_id,
raw_body=raw_body, clean_body=clean_body,
)
message_id = message_row[0]
with engine.begin() as conn:
_lock_source_identity(conn, request.source or "manual", source_event_id)
existing_run = conn.execute(text("""
SELECT id::text FROM action_runs
WHERE message_id=CAST(:message_id AS UUID)
OR (CAST(:raw_event_id AS UUID) IS NOT NULL AND raw_event_id=CAST(:raw_event_id AS UUID))
ORDER BY created_at LIMIT 1
"""), {"message_id": message_id, "raw_event_id": raw_event_id}).first()
if existing_run:
return str(existing_run[0]), str(message_id)
run_row = conn.execute(text("""
INSERT INTO action_runs (