@@ -314,15 +314,22 @@ def kb_to_vault(store: KBStore, vault_dir: Path) -> VaultSyncResult:
314314
315315
316316def _has_pending_page_proposal (
317- store : KBStore , page_id : str , * , body : str | None = None
317+ store : KBStore , page_id : str , * , source_id : str | None = None
318318) -> bool :
319319 """Return True if a pending proposal already targets ``page_id``.
320320
321- When ``body`` is supplied, only returns True if the pending proposal
322- also carries the same body — allowing a second different vault edit
323- to file a new proposal even while the first is still pending.
324- Prevents duplicate proposals when vault_to_kb runs multiple times
325- before the reviewer approves the first proposal for a given page edit.
321+ When ``source_id`` is supplied, only returns True if the pending
322+ proposal also cites that vault-edit source — allowing a second,
323+ *different* vault edit to file its own proposal even while the first
324+ is still pending, rather than being silently coalesced into it and
325+ then clobbered by the next backward sync pass. The source id is the
326+ content-address (sha256) of the whole mirror file `put_source` would
327+ assign — the canonical fingerprint of the complete edit (title, type,
328+ tags, claims, entities, body), not just the body text. Without this
329+ guard, running vault_to_kb twice before the first proposal is
330+ approved files duplicate proposals for the same edit, cluttering the
331+ review queue and causing the second approve to fail with "page
332+ already exists".
326333 """
327334 from .models import ProposalKind , ProposalStatus
328335 for proposal in store .list_proposals (ProposalStatus .PENDING ):
@@ -331,7 +338,7 @@ def _has_pending_page_proposal(
331338 payload = proposal .payload
332339 if not isinstance (payload , dict ) or payload .get ("id" ) != page_id :
333340 continue
334- if body is None or payload .get ("body " ) == body :
341+ if source_id is None or source_id in ( payload .get ("sources " ) or []) :
335342 return True
336343 return False
337344
@@ -398,12 +405,15 @@ def vault_to_kb(
398405 result .pages_skipped_unknown_id .append (rel )
399406 continue
400407
401- # Fix 2 (#219): skip if a pending proposal already targets this page
402- # id. Without this guard, running vault_to_kb twice before the first
403- # proposal is approved files duplicate proposals for the same edit,
404- # cluttering the review queue and causing the second approve to fail
405- # with "page already exists".
406- if _has_pending_page_proposal (store , page_id ):
408+ # Fix 2 (#219, refined by #706): skip if a pending proposal already
409+ # targets this page id *with this exact edit* (source_id=current_hash
410+ # — the content address the vault-edit source below will get).
411+ # Without the source_id check, a second, different edit made while
412+ # the first proposal is still pending would be misclassified as the
413+ # same duplicate, skipped without a new proposal, and then silently
414+ # clobbered by the next backward sync pass restoring the mirror to
415+ # canonical KB content — the second edit vanishing with no trace.
416+ if _has_pending_page_proposal (store , page_id , source_id = current_hash ):
407417 log .debug (
408418 "vault sync: pending proposal already exists for page %r; "
409419 "skipping to avoid duplicate" ,
0 commit comments