Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.d/2.20.3-global-ask-atomic-rollback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
### Fixed

- Roll back a Global Ask turn and its citations when final authorization fails,
so a rejected answer cannot poison the existing session.
77 changes: 40 additions & 37 deletions backend/app/main.py
Comment thread
seonghobae marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
load_global_ask_context,
persist_global_ask_summary,
persist_global_ask_turn,
persist_global_ask_turn_in_transaction,
persist_post_chat,
)
from backend.app.post_summary_ingestion import (
Expand Down Expand Up @@ -2787,25 +2788,26 @@ async def chat_about_post(
) from exc
cited_ids = list(answer.cited_post_ids)
async with pool.acquire() as conn:
await persist_post_chat(
conn,
post_id,
question,
answer.answer_text,
cited_ids,
knowledge_cutoff=knowledge_cutoff,
)
answer_evidence = await read_authorized_ask_evidence(
conn,
cited_post_ids=cited_ids,
corporate_entity_ids=account.corporate_entity_ids,
knowledge_cutoff=knowledge_cutoff,
)
if not answer_evidence.all_citations_visible:
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
"Post chat evidence changed before the answer could be returned",
)
async with conn.transaction():
await persist_post_chat(
conn,
post_id,
question,
answer.answer_text,
cited_ids,
knowledge_cutoff=knowledge_cutoff,
)
answer_evidence = await read_authorized_ask_evidence(
conn,
cited_post_ids=cited_ids,
corporate_entity_ids=account.corporate_entity_ids,
knowledge_cutoff=knowledge_cutoff,
)
if not answer_evidence.all_citations_visible:
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
"Post chat evidence changed before the answer could be returned",
)
await publish_activity_event(
valkey,
post_id,
Expand Down Expand Up @@ -2946,24 +2948,25 @@ async def ask_agent(
) from exc
cited_ids = list(answer.cited_post_ids)
async with pool.acquire() as conn:
await persist_global_ask_turn(
conn,
conversation.session_id,
question,
answer.answer_text,
cited_ids,
)
answer_evidence = await read_authorized_ask_evidence(
conn,
cited_post_ids=cited_ids,
corporate_entity_ids=account.corporate_entity_ids,
knowledge_cutoff=knowledge_cutoff,
)
if not answer_evidence.all_citations_visible:
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
"Global Ask evidence changed before the answer could be returned",
)
async with conn.transaction():
await persist_global_ask_turn_in_transaction(
conn,
conversation.session_id,
question,
answer.answer_text,
cited_ids,
)
answer_evidence = await read_authorized_ask_evidence(
conn,
cited_post_ids=cited_ids,
corporate_entity_ids=account.corporate_entity_ids,
knowledge_cutoff=knowledge_cutoff,
)
Comment on lines +2959 to +2964

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Reauthorization read now runs inside the write transaction

The final read_authorized_ask_evidence executes inside the API-owned conn.transaction() while the session row is held under for update (post_chat_ingestion.py). It is read-only and under READ COMMITTED still observes concurrently committed visibility changes, so the race check is preserved; the session lock is just held marginally longer.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

if not answer_evidence.all_citations_visible:
raise HTTPException(
status.HTTP_503_SERVICE_UNAVAILABLE,
"Global Ask evidence changed before the answer could be returned",
)
await publish_operation_event(
valkey,
account.user_account_id,
Expand Down
84 changes: 59 additions & 25 deletions backend/app/post_chat_ingestion.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,46 +193,80 @@ async def persist_global_ask_summary(
)


async def persist_global_ask_turn(
async def _persist_global_ask_turn_rows(
conn: asyncpg.Connection,
session_id: str,
question: str,
answer: str,
cited_post_ids: Iterable[str],
) -> int:
"""Append one serialized turn and its normalized citation references."""
"""Append one turn while the caller owns the surrounding transaction."""
citations = list(dict.fromkeys(str(post_id) for post_id in cited_post_ids))
async with conn.transaction():
await conn.fetchrow(
"select global_ask_session_id from global_ask_session where global_ask_session_id = $1 for update",
await conn.fetchrow(
"select global_ask_session_id from global_ask_session where global_ask_session_id = $1 for update",
session_id,
)
ordinal = int(
await conn.fetchval(
"select coalesce(max(turn_ordinal), 0) + 1 from global_ask_turn where global_ask_session_id = $1",
session_id,
)
ordinal = int(
await conn.fetchval(
"select coalesce(max(turn_ordinal), 0) + 1 from global_ask_turn where global_ask_session_id = $1",
session_id,
)
)
)
await conn.execute(
"insert into global_ask_turn (global_ask_session_id, turn_ordinal, question_text, answer_text) values ($1, $2, $3, $4)",
session_id,
ordinal,
question,
answer,
)
for citation_ordinal, post_id in enumerate(citations):
await conn.execute(
"insert into global_ask_turn (global_ask_session_id, turn_ordinal, question_text, answer_text) values ($1, $2, $3, $4)",
"insert into global_ask_turn_citation (global_ask_session_id, turn_ordinal, citation_ordinal, cited_post_id) values ($1, $2, $3, $4)",
session_id,
ordinal,
question,
answer,
citation_ordinal,
post_id,
)
for citation_ordinal, post_id in enumerate(citations):
await conn.execute(
"insert into global_ask_turn_citation (global_ask_session_id, turn_ordinal, citation_ordinal, cited_post_id) values ($1, $2, $3, $4)",
session_id,
ordinal,
citation_ordinal,
post_id,
)
await conn.execute(
"update global_ask_session set updated_at = now() where global_ask_session_id = $1",
await conn.execute(
"update global_ask_session set updated_at = now() where global_ask_session_id = $1",
session_id,
)
return ordinal


async def persist_global_ask_turn(
conn: asyncpg.Connection,
session_id: str,
question: str,
answer: str,
cited_post_ids: Iterable[str],
) -> int:
"""Append one serialized turn and its normalized citation references."""
async with conn.transaction():
return await _persist_global_ask_turn_rows(
conn,
session_id,
question,
answer,
cited_post_ids,
)
return ordinal


async def persist_global_ask_turn_in_transaction(
conn: asyncpg.Connection,
session_id: str,
question: str,
answer: str,
cited_post_ids: Iterable[str],
) -> int:
"""Append one turn while the API owns the reauthorization transaction."""
return await _persist_global_ask_turn_rows(
conn,
session_id,
question,
answer,
cited_post_ids,
)


async def _normalize_post_body_text(
Expand Down
Loading