fix: Make crystallize idempotent for session summary pages#118
Conversation
📝 WalkthroughWalkthroughSession crystallization is made idempotent by deriving summary page artifacts from APPROVED proposals and adopting a conditional upsert pattern (try get, create if missing, update if exists) for the summary page. KBStore gains ChangesIdempotent Session Crystallization
Sequence DiagramsequenceDiagram
participant crystallize
participant _approved_artifact_ids_for_session
participant KBStore as KBStore<br/>(get_page/put_page/update_page)
crystallize->>_approved_artifact_ids_for_session: scan APPROVED proposals
_approved_artifact_ids_for_session-->>crystallize: artifact_ids
crystallize->>KBStore: get_page(summary_page_id)
alt page exists
KBStore-->>crystallize: existing Page
crystallize->>KBStore: update_page(updated_page)
KBStore-->>crystallize: updated Page
else ArtifactNotFoundError
crystallize->>KBStore: put_page(new_page)
KBStore-->>crystallize: created Page
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/vouch/sessions.py`:
- Around line 110-115: The current get-then-put path using
store.get_page(page.id) followed by store.put_page(page) is TOCTOU-prone;
instead try to create the page directly and, if the create fails because the
page already exists, fall back to update. Replace the try/except/else block
around store.get_page/store.put_page/store.update_page with a single attempt:
call store.put_page(page) inside a try, catch the specific "already exists"
error returned by put_page (e.g., ArtifactAlreadyExistsError or the
store-specific conflict exception) and call store.update_page(page) in that
except handler; if you don't have a typed conflict exception, catch the error
thrown by put_page when the page exists and then call store.update_page(page).
Ensure page.id and the same semantics are preserved.
In `@src/vouch/storage.py`:
- Around line 353-357: update_page currently writes and embeds a Page without
validating that any referenced Claim exists (unlike put_page), allowing updates
to introduce dangling claim references; modify update_page (and before calling
_embed_and_store) to perform the same claim-existence validation logic used in
put_page (check each claim id's existence via the same helper/path check, e.g.,
_claim_path or the claim existence function used by put_page) and raise
ArtifactNotFoundError for missing claims so referential integrity is preserved.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 316a7a50-dd5c-4879-a5c9-c3bd6e6d6af0
📒 Files selected for processing (3)
src/vouch/sessions.pysrc/vouch/storage.pytests/test_sessions.py
| try: | ||
| store.get_page(page.id) | ||
| except ArtifactNotFoundError: | ||
| store.put_page(page) | ||
| else: | ||
| store.update_page(page) |
There was a problem hiding this comment.
Make summary-page upsert race-safe for concurrent retries.
The Line 111→113 check/create path is TOCTOU-prone: a parallel crystallize can create the page after get_page() fails, making put_page() raise and fail this run.
Suggested fix
- try:
- store.get_page(page.id)
- except ArtifactNotFoundError:
- store.put_page(page)
- else:
- store.update_page(page)
+ try:
+ store.update_page(page)
+ except ArtifactNotFoundError:
+ try:
+ store.put_page(page)
+ except ValueError:
+ # Lost create race to another worker; converge by updating.
+ store.update_page(page)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try: | |
| store.get_page(page.id) | |
| except ArtifactNotFoundError: | |
| store.put_page(page) | |
| else: | |
| store.update_page(page) | |
| try: | |
| store.update_page(page) | |
| except ArtifactNotFoundError: | |
| try: | |
| store.put_page(page) | |
| except ValueError: | |
| # Lost create race to another worker; converge by updating. | |
| store.update_page(page) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/vouch/sessions.py` around lines 110 - 115, The current get-then-put path
using store.get_page(page.id) followed by store.put_page(page) is TOCTOU-prone;
instead try to create the page directly and, if the create fails because the
page already exists, fall back to update. Replace the try/except/else block
around store.get_page/store.put_page/store.update_page with a single attempt:
call store.put_page(page) inside a try, catch the specific "already exists"
error returned by put_page (e.g., ArtifactAlreadyExistsError or the
store-specific conflict exception) and call store.update_page(page) in that
except handler; if you don't have a typed conflict exception, catch the error
thrown by put_page when the page exists and then call store.update_page(page).
Ensure page.id and the same semantics are preserved.
| def update_page(self, page: Page) -> Page: | ||
| if not self._page_path(page.id).exists(): | ||
| raise ArtifactNotFoundError(f"page {page.id}") | ||
| self._page_path(page.id).write_text(_serialize_page(page)) | ||
| self._embed_and_store(kind="page", id=page.id, text=f"{page.title}\n\n{page.body}") |
There was a problem hiding this comment.
Preserve page claim referential integrity on updates.
update_page() skips the claim-existence validation that put_page() performs, so Line 356 can persist pages referencing missing claims.
Suggested fix
def update_page(self, page: Page) -> Page:
+ for cid in page.claims:
+ if not self._claim_path(cid).exists():
+ raise ValueError(f"page {page.id} references unknown claim {cid}")
if not self._page_path(page.id).exists():
raise ArtifactNotFoundError(f"page {page.id}")
self._page_path(page.id).write_text(_serialize_page(page))
self._embed_and_store(kind="page", id=page.id, text=f"{page.title}\n\n{page.body}")
return page🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/vouch/storage.py` around lines 353 - 357, update_page currently writes
and embeds a Page without validating that any referenced Claim exists (unlike
put_page), allowing updates to introduce dangling claim references; modify
update_page (and before calling _embed_and_store) to perform the same
claim-existence validation logic used in put_page (check each claim id's
existence via the same helper/path check, e.g., _claim_path or the claim
existence function used by put_page) and raise ArtifactNotFoundError for missing
claims so referential integrity is preserved.
crystallize idempotent for session summary pagescrystallize idempotent for session summary pages
Related issues
Closes #117
What changed
crystallizenow builds the session summary page from all approved proposals in the session (not just approvals from the current run), and create-or-updatespages/session-<session_id>.mdinstead of callingput_pageand failing on reruns. AddedKBStore.update_page()for safe in-place page updates, plus a regression test covering partial first crystallize followed by a successful retry.Why
vouch crystallize <session_id>is used in agent workflows and retry loops. After a first successful run (or a partial run that already wrote the summary page), a second call could abort withpage session-<id> already exists -- choose a different slug, leaving automation stuck even when pending proposals still needed approval. Making summary-page handling convergent lets operators safely rerun crystallize until all session proposals are approved.What might break
crystallizeon a session that already has a summary page now succeeds and updates that page body/claims list to reflect all approved session artifacts.write_summary_page=True.put_page()remains exclusive-create; only the crystallize path uses the new update behaviour.Not a breaking change for typical first-run crystallize flows. Existing
.vouch/directories are compatible without migration.VEP
Not required — internal reliability fix to existing session crystallization behaviour; no new surface, transport, or schema contract.
Tests
make checkpasses locally (lint + mypy + pytest)test_crystallize_retry_updates_existing_summary_pageintests/test_sessions.py)CHANGELOG.mdupdated under## [Unreleased]Summary by CodeRabbit
New Features
Improvements