[F057] playbook: index/unindex as a post-commit step so the RAG corpus never leads the status transaction

This commit is contained in:
Renn F
2026-06-28 16:08:45 +02:00
parent adff12a85b
commit bffb0b7200
6 changed files with 428 additions and 64 deletions
+10 -4
View File
@@ -53,14 +53,17 @@ async def approve_playbook(
"""Approve a draft playbook → approved (and indexed into the KB)."""
_require_curator(agent)
try:
playbook = await get_playbook_service(db).approve(
playbook_id, approver_id=agent.agent_id
)
svc = get_playbook_service(db)
playbook = await svc.approve(playbook_id, approver_id=agent.agent_id)
except NotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Playbook not found"
) from exc
# Commit the status change BEFORE indexing: the RAG index write runs through
# its own auto-committing connection, so indexing before commit would durably
# land an approved playbook in the corpus even if this commit rolled back.
await db.commit()
await svc.index_approved(playbook)
return Playbook.model_validate(playbook)
@@ -74,12 +77,15 @@ async def reject_playbook(
"""Reject a playbook → archived, with the Auditor's reason."""
_require_curator(agent)
try:
playbook = await get_playbook_service(db).reject(
svc = get_playbook_service(db)
playbook = await svc.reject(
playbook_id, approver_id=agent.agent_id, reason=body.reason
)
except NotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Playbook not found"
) from exc
# Commit the status change BEFORE de-indexing (see approve_playbook).
await db.commit()
await svc.unindex_playbook(playbook)
return Playbook.model_validate(playbook)
@@ -793,6 +793,18 @@ class ContentActions:
status = "playbook_archived"
except NotFoundError:
return Envelope.not_found(message=f"playbook {playbook_id} not found")
# Commit the status change BEFORE touching the RAG index: the index write
# runs through its own auto-committing connection, so indexing before the
# status commit would durably land (or drop) a playbook in the corpus even
# if this transaction rolled back — a divergence agents surface in
# briefings. ``get_db`` commits the session again after the route returns
# (a no-op on the now-clean transaction); this explicit commit is what
# gates the index.
await self.task.session.commit()
if action == "approve":
await svc.index_approved(playbook)
else:
await svc.unindex_playbook(playbook)
return Envelope.ok(
status=status,
task_id=None,
+20 -8
View File
@@ -68,21 +68,30 @@ class PlaybookService(BaseService):
return playbook
async def approve(self, playbook_id: UUID, approver_id: UUID) -> PlaybookTable:
"""Auditor approves a draft: draft -> approved, stamped."""
"""Auditor approves a draft: draft -> approved, stamped.
Flushes the status change ONLY the RAG index write (``index_approved``)
is a SEPARATE step the caller runs AFTER committing the status. The vector
store writes through its own auto-committing pool connection, so indexing
inline (before the status commit) would durably land an approved playbook
in the corpus even if the status transaction rolled back a divergence
agents then surfaced in briefings.
"""
playbook = await self._get_or_raise(playbook_id)
playbook.status = PlaybookStatus.APPROVED.value
playbook.approved_by = approver_id
playbook.approved_at = datetime.now(UTC)
await self.session.flush()
await self._index_approved(playbook)
self.log.info("Playbook approved", playbook_id=str(playbook_id))
return playbook
async def _index_approved(self, playbook: PlaybookTable) -> None:
async def index_approved(self, playbook: PlaybookTable) -> None:
"""Embed an approved playbook into the PLAYBOOKS RAG index (best-effort).
Gated on ``org_memory_enabled`` so the feature is fully inert when off;
a failure (e.g. the embedder is down) never blocks the approval.
Post-commit step: the caller commits the ``draft -> approved`` status
change FIRST, then runs this so the index never leads the status
transaction. Gated on ``org_memory_enabled`` so the feature is fully inert
when off; a failure (e.g. the embedder is down) never blocks the approval.
"""
if not settings.org_memory_enabled:
return
@@ -114,17 +123,20 @@ class PlaybookService(BaseService):
async def reject(
self, playbook_id: UUID, approver_id: UUID, reason: str
) -> PlaybookTable:
"""Auditor rejects a playbook: -> archived (reason recorded in the log)."""
"""Auditor rejects a playbook: -> archived (reason recorded in the log).
Flushes the status change ONLY ``unindex_playbook`` is a separate
post-commit step (see ``approve`` for the ordering rationale).
"""
playbook = await self._get_or_raise(playbook_id)
playbook.status = PlaybookStatus.ARCHIVED.value
playbook.approved_by = approver_id
playbook.approved_at = datetime.now(UTC)
await self.session.flush()
await self._unindex_playbook(playbook)
self.log.info("Playbook rejected", playbook_id=str(playbook_id), reason=reason)
return playbook
async def _unindex_playbook(self, playbook: PlaybookTable) -> None:
async def unindex_playbook(self, playbook: PlaybookTable) -> None:
"""De-index a playbook from the PLAYBOOKS RAG index (best-effort).
The mirror of :meth:`_index_approved`: a rejected/archived playbook that