Skip to content

Commit 412e502

Browse files
simonovic86claude
andcommitted
fix(sdk): allow Compensate from InFlight for known failures
Compensate previously only accepted Unresolved state, but agents that observe an action fail during InFlight (e.g., HTTP error, payment rejected) need to mark the intent as terminal immediately. Without this, failed intents remain InFlight, Compensate silently fails, and Prune cannot remove them — leading to stale non-terminal intents accumulating in effect state. Widen Compensate to accept both InFlight and Unresolved: - InFlight → Compensated: agent observed the failure directly - Unresolved → Compensated: agent investigated after crash Fixes the deployer and x402buyer error paths which already called Compensate after Begin but got silent errInvalidTransition errors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3963f6c commit 412e502

2 files changed

Lines changed: 26 additions & 6 deletions

File tree

sdk/igor/effects.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,16 @@ func (e *EffectLog) Confirm(id []byte) error {
9494
return nil
9595
}
9696

97-
// Compensate transitions an intent from Unresolved to Compensated.
97+
// Compensate transitions an intent from InFlight or Unresolved to Compensated.
9898
// Call this after determining the action did not happen or was rolled back.
99+
// From InFlight: the agent observed the action fail (e.g., HTTP error, payment rejected).
100+
// From Unresolved: the agent investigated after a crash and determined the action didn't complete.
99101
func (e *EffectLog) Compensate(id []byte) error {
100102
intent := e.find(id)
101103
if intent == nil {
102104
return errIntentNotFound
103105
}
104-
if intent.State != Unresolved {
106+
if intent.State != InFlight && intent.State != Unresolved {
105107
return errInvalidTransition
106108
}
107109
intent.State = Compensated

sdk/igor/effects_test.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,6 @@ func TestEffectLog_InvalidTransitions(t *testing.T) {
7878
if err := e.Begin(id); err == nil {
7979
t.Fatal("expected error: Begin from InFlight")
8080
}
81-
// Can't Compensate from InFlight (only from Unresolved).
82-
if err := e.Compensate(id); err == nil {
83-
t.Fatal("expected error: Compensate from InFlight")
84-
}
8581

8682
e.Confirm(id)
8783

@@ -164,6 +160,28 @@ func TestEffectLog_Compensate(t *testing.T) {
164160
}
165161
}
166162

163+
func TestEffectLog_CompensateFromInFlight(t *testing.T) {
164+
var e EffectLog
165+
id := []byte("tx")
166+
e.Record(id, nil)
167+
e.Begin(id)
168+
169+
// Agent observes the action fail (e.g., HTTP error, payment rejected).
170+
// It knows the action didn't complete, so it compensates directly.
171+
if err := e.Compensate(id); err != nil {
172+
t.Fatalf("Compensate from InFlight: %v", err)
173+
}
174+
if e.Get(id).State != Compensated {
175+
t.Fatal("expected Compensated")
176+
}
177+
178+
// Compensated is terminal — prune should remove it.
179+
removed := e.Prune()
180+
if removed != 1 {
181+
t.Fatalf("expected 1 pruned, got %d", removed)
182+
}
183+
}
184+
167185
func TestEffectLog_ConfirmFromUnresolved(t *testing.T) {
168186
var e EffectLog
169187
id := []byte("tx")

0 commit comments

Comments
 (0)