Skip to content

fix(sessions): make crystallize retry idempotent for summary pages#256

Open
Yurii214 wants to merge 1 commit into
vouchdev:testfrom
Yurii214:fix/139-crystallize-retry-idempotency
Open

fix(sessions): make crystallize retry idempotent for summary pages#256
Yurii214 wants to merge 1 commit into
vouchdev:testfrom
Yurii214:fix/139-crystallize-retry-idempotency

Conversation

@Yurii214

@Yurii214 Yurii214 commented Jun 21, 2026

Copy link
Copy Markdown

Summary

  • Fixes [bug] crystallize crashes on retry after partial approval #139: partial crystallize() runs that already wrote a session-{id} summary page no longer fail on retry with page session-... already exists.
  • Summary page artifact list is derived from all approved session proposals (not only approvals from the current run).
  • Adds KBStore.update_page() and upserts the summary page (put_page on first write, update_page on retry).

Why

Operators retry crystallize after transient approval failures. The first partial run could approve one proposal and create the summary page; a second run then called put_page() again and crashed before completing.

What might break

  • On-disk layout: none — same pages/session-{id}.md path and shape.
  • Behaviour change (intentional): re-running crystallize on a session that already has approved proposals but no pending ones will now create or refresh the summary page when write_summary_page=True. put_page() remains exclusive-create everywhere else.

Tests

  • PYTHONPATH=src uv run pytest tests/test_sessions.py
  • PYTHONPATH=src uv run pytest --ignore=tests/embeddings (full suite minus optional embedding/MCP extras)
  • PYTHONPATH=src uv run ruff check src/vouch/sessions.py src/vouch/storage.py tests/test_sessions.py
  • Regression: test_crystallize_retry_updates_existing_summary_page (partial approve → retry → same summary page lists all claims)

Fixes #139

Summary by CodeRabbit

  • Bug Fixes

    • Fixed an issue where retrying session crystallization after a partial approval run would fail. The crystallization process now properly updates the existing summary page rather than aborting with a duplicate page error.
  • Tests

    • Added test coverage for session crystallization retry scenarios.

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b5c89610-6dad-46ad-bc3c-b45b42172171

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

KBStore gains an update_page method. crystallize() is updated to derive approved artifact IDs from stored proposals via a new private helper and to conditionally call put_page or update_page based on whether a summary page already exists. A new test validates the retry path.

Changes

crystallize() retry upsert fix

Layer / File(s) Summary
KBStore.update_page method
src/vouch/storage.py
Adds update_page(page) that asserts the on-disk file exists (raising ArtifactNotFoundError if not), overwrites the file, re-embeds, and returns the updated Page.
crystallize() upsert logic and approved-artifact helper
src/vouch/sessions.py
Adds imports for ProposalKind, ArtifactNotFoundError, and approve; adds _approved_artifact_ids_for_session which filters APPROVED proposals by session and kind; replaces the in-loop approved set with the session-derived set for summary-page content; changes persistence from unconditional put_page to a get_pageput_page/update_page conditional.
Retry crystallize test and changelog
tests/test_sessions.py, CHANGELOG.md
Adds test_crystallize_retry_updates_existing_summary_page which patches approve to fail once, then calls crystallize twice, asserting summary_page_id is reused, failures cleared, approved count correct, and summary page claims match current store claims. Adds corresponding changelog entry.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

Possibly related PRs

  • vouchdev/vouch#61: Modifies the same crystallize() summary-page persistence path in src/vouch/sessions.py, making it directly adjacent to the put/update logic changed here.

Poem

🐇 Hoppity-hop through the session once more,
No "page already exists" crash at the door.
With update_page ready, the summary stays,
Retrying crystallize now earns my praise!
Two hops, one page—idempotent days!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(sessions): make crystallize retry idempotent for summary pages' accurately and concisely describes the main change - making the crystallize function idempotent for summary page creation to fix retry failures.
Linked Issues check ✅ Passed The PR fully addresses issue #139 by implementing idempotent summary page creation through the new update_page method, allowing safe retries of partial crystallize runs.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the crystallize retry idempotency issue: the new update_page method, the revised summary page creation logic, and comprehensive test coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 check-then-act pattern in the store.get_page,
store.put_page, and store.update_page sequence creates a race condition where
concurrent calls can both pass the get_page check and then fail when put_page
tries to create an already-existing page. Reverse the logic to attempt put_page
first and catch the "already exists" error (likely a specific exception), then
fall back to store.update_page in the exception handler instead of the
try-except-else structure. This makes the upsert operation race-safe by handling
the concurrent creation case gracefully.

In `@src/vouch/storage.py`:
- Around line 353-358: The update_page method is missing claim existence
validation that is enforced in put_page(), allowing pages to be persisted with
dangling claims references. Add the same claim validation logic used in
put_page() to the update_page method before the page is written to storage
(before the _page_path write_text call). This ensures that all claims referenced
in the page being updated are verified to exist, maintaining page-claim
integrity.
🪄 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: f0af788b-afb8-4c55-ab8e-4f8457a86a0b

📥 Commits

Reviewing files that changed from the base of the PR and between 18faf17 and 5ee0be0.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • src/vouch/sessions.py
  • src/vouch/storage.py
  • tests/test_sessions.py

Comment thread src/vouch/sessions.py
Comment on lines +110 to +115
try:
store.get_page(page.id)
except ArtifactNotFoundError:
store.put_page(page)
else:
store.update_page(page)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make summary-page upsert race-safe.

Lines 110-115 use a check-then-create flow. Concurrent crystallize() calls can both miss on get_page(), then one put_page() fails with “already exists”, breaking retry safety.

Suggested patch
-        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; page now exists.
+                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 check-then-act
pattern in the store.get_page, store.put_page, and store.update_page sequence
creates a race condition where concurrent calls can both pass the get_page check
and then fail when put_page tries to create an already-existing page. Reverse
the logic to attempt put_page first and catch the "already exists" error (likely
a specific exception), then fall back to store.update_page in the exception
handler instead of the try-except-else structure. This makes the upsert
operation race-safe by handling the concurrent creation case gracefully.

Comment thread src/vouch/storage.py
Comment on lines +353 to +358
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}")
return page

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve page-claim integrity checks in update_page.

Line 353 adds an update path that skips the claim existence validation enforced by put_page(). That allows persisting pages with dangling claims references.

Suggested patch
 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
📝 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.

Suggested change
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}")
return page
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 - 358, The update_page method is
missing claim existence validation that is enforced in put_page(), allowing
pages to be persisted with dangling claims references. Add the same claim
validation logic used in put_page() to the update_page method before the page is
written to storage (before the _page_path write_text call). This ensures that
all claims referenced in the page being updated are verified to exist,
maintaining page-claim integrity.

@plind-junior plind-junior changed the base branch from main to test June 22, 2026 07:24
@plind-junior

Copy link
Copy Markdown
Collaborator

Thanks for your contribution! Fix the conflict

@Yurii214

Yurii214 commented Jun 29, 2026

Copy link
Copy Markdown
Author

@plind-junior Thanks for the earlier note on the conflict — rebased onto latest test (0f9687e), resolved merge conflicts in CHANGELOG / sessions / tests, and CI is green again.

Working through the CodeRabbit race/validation notes next; happy to push an update if you’d rather review the core retry fix first.

Appreciate the review 🙏

Partial crystallize runs that already wrote session-{id} summary pages
failed on retry with "page already exists". Upsert the summary page
and derive its artifact list from all approved session proposals.

Fixes vouchdev#139

Co-authored-by: Cursor <cursoragent@cursor.com>
@Yurii214 Yurii214 force-pushed the fix/139-crystallize-retry-idempotency branch from 8440927 to 0f9687e Compare June 29, 2026 02:37
@github-actions github-actions Bot added docs documentation, specs, examples, and repo guidance tests tests and fixtures size: S 50-199 changed non-doc lines labels Jun 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs documentation, specs, examples, and repo guidance size: S 50-199 changed non-doc lines tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] crystallize crashes on retry after partial approval

3 participants