Skip to content

fix: make rename replay idempotent#7

Open
Matt-Reason wants to merge 1 commit intoMartian-Engineering:masterfrom
Matt-Reason:fix/idempotent-rename-replay
Open

fix: make rename replay idempotent#7
Matt-Reason wants to merge 1 commit intoMartian-Engineering:masterfrom
Matt-Reason:fix/idempotent-rename-replay

Conversation

@Matt-Reason
Copy link

Problem

Every write operation (create, close, update, init) fails with:

Error: rename target matches current id

Despite the error, the operation succeeds (events are appended to events.jsonl), but the CLI exits with code 1. This breaks scripting and CI usage.

Root Cause

When events.jsonl contains duplicate rename events, RebuildCache() fails during replay. Duplicate renames occur when pb update --parent= is called on an issue that was already renamed to a parent-child suffix ID. The guard HasParentChildSuffix checks the current ID, but lookup by original ID causes the check to pass and a duplicate rename event to be emitted.

During cache rebuild replay:

  1. First rename of old-id -> new-id succeeds and adds a mapping to the renames table
  2. Duplicate rename of old-id -> new-id calls resolveIssueID(old-id) which follows the rename chain to new-id
  3. Now resolvedOldID == newID, triggering the error

Since AppendEvent() runs before RebuildCache(), the write is persisted but the exit code is 1. Every subsequent operation also fails because the stale cache triggers a rebuild that hits the same duplicate.

Fix

One-line change in applyRename(): treat resolvedOldID == newID as a no-op (rename already applied) instead of an error. This makes event replay idempotent — the expected property of an event-sourced system.

// Before:
if resolvedOldID == newID {
    return fmt.Errorf("rename target matches current id")
}

// After:
if resolvedOldID == newID {
    return nil // already applied; idempotent replay
}

Workaround for Affected Users

Deduplicate rename events in events.jsonl and rebuild:

python3 -c "
import json
lines = open('.pebbles/events.jsonl').readlines()
seen = set()
kept = []
for line in lines:
    line = line.strip()
    if not line:
        continue
    evt = json.loads(line)
    if evt.get('type') == 'rename':
        key = (evt['issue_id'], evt['payload'].get('new_id', ''))
        if key in seen:
            continue
        seen.add(key)
    kept.append(line + '\n')
open('.pebbles/events.jsonl', 'w').writelines(kept)
"
rm .pebbles/pebbles.db
pb init --prefix <your-prefix>

Separate Issue: Duplicate Rename Emission

This fix makes replay tolerant of duplicates, but the root emission of duplicate renames from pb update --parent= should also be addressed separately. When an issue has already been renamed to a parent-child ID, re-running --parent= should not emit another rename event.

When events.jsonl contains duplicate rename events (e.g. from repeated
`pb update --parent=` operations on already-renamed issues), every write
operation fails with "rename target matches current id" during cache
rebuild.

The sequence:
1. `pb update <id> --parent=<epic>` emits a rename event
2. A subsequent `pb update <id> --parent=<epic>` on the same issue
   emits another identical rename event
3. On cache rebuild, the first rename succeeds and adds a mapping
   to the renames table (old_id -> new_id)
4. The duplicate rename calls resolveIssueID(old_id) which follows
   the chain to new_id. Now resolvedOldID == newID, triggering the
   error

Since events are appended to events.jsonl before RebuildCache runs,
the write succeeds but the CLI exits with code 1. Every subsequent
operation also fails because the stale cache triggers a rebuild that
hits the same duplicate rename.

The fix treats "resolvedOldID == newID" as a no-op (the rename was
already applied) instead of an error, making event replay idempotent.

Workaround for affected users: deduplicate rename events in
events.jsonl and re-run `pb init`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

1 participant