Summary
Two critical protocol paths write to multiple stores without transactional guarantees, risking inconsistent state on partial failure.
1. Handoff creation on non-SQLite stores
contributeOperation in src/core/operations/contribute.ts has two code paths for handoff creation:
- Atomic path: When the store supports
putWithCowrite (duck-typed SQLite), handoff creation runs inside the same transaction as the contribution write. Safe.
- Non-atomic path: When
putWithCowrite is not available (e.g., Nexus HTTP stores), the contribution is written first, then handoff creation runs separately. If handoff creation fails, the contribution exists but downstream agents never get notified.
The non-atomic path also writes debug output to /tmp/grove-debug.log — operational noise in library code.
2. Bounty claim is two separate store calls
claimBountyOperation in src/core/operations/bounty.ts calls:
claimStore.claimOrRenew(claim) — creates the claim
bountyStore.claimBounty(bountyId, claim.claimId) — updates the bounty
If step 2 fails, we have a dangling claim with no corresponding bounty update. Manual repair required.
Proposed fixes
Short term
- Wrap the non-atomic handoff path in try/catch with rollback (delete the contribution if handoff creation fails)
- Add a
claimBountyAtomic that either wraps both calls in a single SQLite transaction or uses a compensating action pattern
- Remove the
/tmp/grove-debug.log write
Long term
- Consider a saga pattern for multi-store operations
- Or ensure all store combinations support a "co-write" primitive
Files
src/core/operations/contribute.ts — non-atomic handoff branch
src/core/operations/bounty.ts — claimBountyOperation
Summary
Two critical protocol paths write to multiple stores without transactional guarantees, risking inconsistent state on partial failure.
1. Handoff creation on non-SQLite stores
contributeOperationinsrc/core/operations/contribute.tshas two code paths for handoff creation:putWithCowrite(duck-typed SQLite), handoff creation runs inside the same transaction as the contribution write. Safe.putWithCowriteis not available (e.g., Nexus HTTP stores), the contribution is written first, then handoff creation runs separately. If handoff creation fails, the contribution exists but downstream agents never get notified.The non-atomic path also writes debug output to
/tmp/grove-debug.log— operational noise in library code.2. Bounty claim is two separate store calls
claimBountyOperationinsrc/core/operations/bounty.tscalls:claimStore.claimOrRenew(claim)— creates the claimbountyStore.claimBounty(bountyId, claim.claimId)— updates the bountyIf step 2 fails, we have a dangling claim with no corresponding bounty update. Manual repair required.
Proposed fixes
Short term
claimBountyAtomicthat either wraps both calls in a single SQLite transaction or uses a compensating action pattern/tmp/grove-debug.logwriteLong term
Files
src/core/operations/contribute.ts— non-atomic handoff branchsrc/core/operations/bounty.ts—claimBountyOperation