Documentation / guides
Idempotency
Prevent duplicate command effects and duplicate records.
Idempotency protects the journal from replayed delivery and ambiguous caller retries. The identity is checked under the same lock that serializes appends, so an identical retry returns the original sequence without writing a second frame. A reused ID with different durable bytes fails closed.
Append result
The optional extension is:
type AppendResult struct {
Sequence uint64
Appended bool
}
type IdempotentJournal interface {
SessionJournal
AppendIdempotent(context.Context, JournalRecord) (AppendResult, error)
}
Appended is true only when this call created a new frame. A duplicate returns
Appended: false and the original sequence. SessionJournal.Append remains
available for callers that only need a sequence and error.
Fingerprint rule
The journal compares the persisted kind plus codec body, not the whole Go
wrapper. CommandRecord’s transient session and loop route therefore do not
change the fingerprint. The public constructor is:
type Fingerprint struct { /* kind and SHA-256 are package-private */ }
func NewFingerprint(kind string, body []byte) Fingerprint
Two records are identical only when the backend would persist byte-identical kind and payload. If the same ID has a different fingerprint, the journal returns:
type IdempotencyCollisionError struct {
ID string
Seq uint64
}
The error names the original sequence and is discoverable with errors.As.
Index contract
The in-memory index is deliberately not concurrency-safe on its own:
type IdempotencyIndex struct { /* entries are package-private */ }
func NewIdempotencyIndex() *IdempotencyIndex
func (*IdempotencyIndex) Observe(id string, seq uint64, fp Fingerprint)
func (*IdempotencyIndex) Check(id string, fp Fingerprint) (seq uint64, duplicate bool, err error)
The storage journal hydrates it from the complete ledger after the opening fence. New records are observed only after their CAS append commits. This ordering prevents a failed append from poisoning the retry decision.
Delegate phases
Machine delegate delivery has a stronger state machine than ordinary retries:
intentmust append first.fallback_queuedmay append once, with the same phase-normalized payload.- A fallback before intent, changed payload, duplicate phase, or route mismatch
returns
*journal.DeliveryTransitionErroror*journal.CommandRouteMismatchError.
The fallback physical ID has a typed suffix, but its normalized fingerprint
clears only DelegateDeliveryPhase; blocks, target loop, agency, hand-back,
timestamps, and other durable fields remain part of the comparison.
%%{init: {"theme":"dark"}}%%
stateDiagram-v2
[*] --> Intent
Intent --> FallbackQueued: same logical ID + same normalized payload
Intent --> Collision: different payload
FallbackQueued --> Duplicate: identical retry
FallbackQueued --> Collision: changed payload
Intent --> Duplicate: identical intent retry
Retry example
func appendOnce(ctx context.Context, j journal.SessionJournal, rec journal.JournalRecord) (uint64, error) {
idem, ok := j.(journal.IdempotentJournal)
if !ok {
return j.Append(ctx, rec)
}
result, err := idem.AppendIdempotent(ctx, rec)
if err != nil {
var collision *journal.IdempotencyCollisionError
if errors.As(err, &collision) {
return 0, fmt.Errorf("do not retry forged id %q: %w", collision.ID, err)
}
return 0, err
}
if !result.Appended {
log.Printf("deduplicated retry at sequence %d", result.Sequence)
}
return result.Sequence, nil
}
The event appender uses this result to skip a second live broadcast. A retry that is durable but deduplicated is success, not a persistence fault.