Skip to content

fix: guard buddy_hatch + repair duplicate companion rows#140

Open
TheMichaelFeng wants to merge 1 commit into
fiorastudio:masterfrom
TheMichaelFeng:fix/hatch-companion-persistence
Open

fix: guard buddy_hatch + repair duplicate companion rows#140
TheMichaelFeng wants to merge 1 commit into
fiorastudio:masterfrom
TheMichaelFeng:fix/hatch-companion-persistence

Conversation

@TheMichaelFeng

@TheMichaelFeng TheMichaelFeng commented Jun 30, 2026

Copy link
Copy Markdown

The bug

buddy_hatch did a bare INSERT, and every read path uses SELECT ... FROM companions LIMIT 1 with no ORDER BY. So after hatching, SQLite could keep surfacing an older row, the new companion looked like it didn't save.

Approach (reworked after review feedback)

The first pass (delete-before-insert) was incomplete: it only removed one row, and it made buddy_hatch silently destructive toward rescued buddies. This replaces it.

buddy_hatch refuses to overwrite. If a companion already exists, it returns early and tells the user to buddy_respawn first. Onboarding's rescue-then-hatch flow can no longer destroy a rescued buddy. Releasing stays an explicit, separate action.

Legacy duplicates are repaired separately. New repairDuplicateCompanions() runs as a startup migration in initDb(). It collapses all duplicate rows (not just one) down to the most-progressed buddy, highest XP, ties broken toward the earliest row (the one the buggy LIMIT 1 read was already surfacing).

The collapse is transactional. It runs inside db.transaction(), clearing legacy child rows before their parent (those tables predate ON DELETE CASCADE), so a crash mid-repair can't leave a half-collapsed DB.

Tests

New hatch-guard.test.ts covers repair (survivor selection, orphan cleanup, tie-breaking, initDb() integration, no-op at 0/1 rows) and rescue-then-hatch (the rescued buddy survives; hatch is refused while it exists).

Note

~9 tests fail on master unrelated to this change (Penguin animation width, doctor, reasoning detectors), verified pre-existing by stashing this diff and re-running.

@fiorastudio

Copy link
Copy Markdown
Owner

hi @TheMichaelFeng thank you for the submission! based on the review from gpt5.5,

This doesn’t fully fix the reported bug. SELECT ... LIMIT 1 removes only one companion, but affected databases may already contain several rows. After deleting A and inserting C, B remains and may still be returned by LIMIT 1.
It also makes buddy_hatch silently destructive. After onboarding rescues a buddy, the installer tells users to hatch one; following that instruction would now delete the rescued buddy and its data.

Could you address these issues:
1/Prevent buddy_hatch from replacing an existing buddy without explicit confirmation/buddy_respawn.
2/Repair legacy duplicate rows separately.
3/Wrap any replacement cleanup and insertion in a transaction.
4/Add tests for multiple existing rows and rescue-then-hatch behavior.

The earlier delete-before-insert approach was incomplete on two fronts,
both raised in review:

1. It only removed ONE existing row (SELECT ... LIMIT 1), so databases
   already carrying several companions (from the original bare-INSERT
   hatch bug) could still surface a stale row after hatching.
2. It made buddy_hatch silently destructive. Onboarding rescues a buddy
   and then tells the user to hatch; following that instruction wiped
   the rescued buddy and its history.

This replaces that approach:

- buddy_hatch now refuses when a companion already exists, directing the
  user to buddy_respawn first. Hatching is never silently destructive;
  releasing a buddy stays an explicit, separate action.
- repairDuplicateCompanions() runs as a startup migration in initDb() and
  collapses ALL duplicate rows down to one canonical buddy — the
  most-progressed (highest-XP) row, ties broken toward the earliest.
  Legacy child rows are cleared before their parent (the legacy tables
  have no ON DELETE CASCADE) and the whole collapse runs in a single
  transaction so a crash mid-repair can't leave the DB half-collapsed.
- Tests cover multi-row repair (survivor selection, orphan cleanup, tie
  breaking, initDb integration) and rescue-then-hatch (the rescued buddy
  survives; hatch is refused while it exists).
@TheMichaelFeng TheMichaelFeng force-pushed the fix/hatch-companion-persistence branch from e4a69a0 to 793c6f5 Compare July 2, 2026 02:28
@TheMichaelFeng TheMichaelFeng changed the title fix: clear existing companion before inserting new hatch fix: guard buddy_hatch + repair duplicate companion rows Jul 2, 2026
@TheMichaelFeng

Copy link
Copy Markdown
Author

I have reworked it and pushed to the same branch:

1: buddy_hatch no longer replaces silently. It refuses when a companion already exists and points the user to buddy_respawn first, so the rescue-then-hatch onboarding flow can't wipe a rescued buddy.

2: Legacy dupes are handled separately. A new repairDuplicateCompanions() startup migration in initDb() collapses all rows (not just one) down to the most-progressed buddy; highest XP, ties broken toward the earliest row.

3: The collapse is transactional. It runs inside db.transaction(), clearing legacy child rows before their parent (those tables have no ON DELETE CASCADE).

4: Added tests for the multi-row repair (survivor selection, orphan cleanup, tie-breaking, initDb() integration) and rescue-then-hatch (rescued buddy survives, hatch refused while it exists).

The PR description has the full rundown. Heads-up: there are bout 9 pre-existing test failures on master (Penguin animation width, doctor, a couple reasoning detectors) unrelated to this change: I confirmed they fail on a clean master too. Happy to open a separate issue for those if useful.

@TheMichaelFeng

TheMichaelFeng commented Jul 2, 2026 via email

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants