Target undo op not found

I use undo_entry = mw.col.undo_status().last_step to get the undo entry of answer card for merge_undo_entries(undo_entry). But it will cause an error when other add-on call add_custom_undo_entry():

Error
An error occurred. Please start Anki while holding down the shift key, which will temporarily disable the add-ons you have installed.
If the issue only occurs when add-ons are enabled, please use the Tools > Add-ons menu item to disable some add-ons and restart Anki, repeating until you discover the add-on that is causing the problem.
When you've discovered the add-on that is causing the problem, please report the issue to the add-on author.
Debug info:
Anki 2.1.65 (aa9a734f) Python 3.9.15 Qt 6.5.0 PyQt 6.5.0
Platform: macOS-13.3.1-arm64-arm-64bit
Flags: frz=True ao=True sv=3
Add-ons, last update check: 2023-08-26 16:56:26
Add-ons possibly involved: <U+2068>FSRS4Anki Helper<U+2069>

Caught exception:
Traceback (most recent call last):
  File "aqt.taskman", line 122, in _on_closures_pending
  File "aqt.taskman", line 71, in <lambda>
  File "aqt.taskman", line 90, in wrapped_done
  File "aqt.operations", line 125, in wrapped_done
  File "aqt.reviewer", line 460, in after_answer
  File "aqt.reviewer", line 473, in _after_answering
  File "_aqt.hooks", line 3982, in __call__
  File "/Users/jarrettye/Library/Application Support/Anki2/addons21/759844606/schedule/__init__.py", line 8, in reschedule_and_disperse_siblings_when_review
    undo_entry = reschedule_when_review(reviewer, card, ease)
  File "/Users/jarrettye/Library/Application Support/Anki2/addons21/759844606/schedule/reschedule.py", line 381, in reschedule_when_review
    mw.col.merge_undo_entries(undo_entry)
  File "anki.collection", line 1054, in merge_undo_entries
  File "anki._backend_generated", line 1823, in merge_undo_entries
  File "anki._backend", line 156, in _run_command
anki.errors.InvalidInput: target undo op not found

I find that merge_undo_entries in another add-on will modify last_step. It induces the error.

Are both your add-on and their add-on calling these routines inside a CollectionOp?

My add-on doesn’t use collection op. I have two feature appended to the hook of reviewer_did_answer_card. And I want to merge them into Answer Card (Anki build-in undo entry).

Here is the code of another add-on:

def on_did_answer_card(reviewer: Reviewer, card: Card, ease: Literal[1, 2, 3, 4]) -> None:
    """Bury card if it was answered 'again' too many times within the specified time."""

    # only care about failed cards
    if ease != 1:
        return

    if config['ignore_new_cards'] is True and card.type <= TYPE_LEARNING:
        return

    agains = agains_in_the_timeframe(card.id)
    passed = time_passed().hours()

    if agains >= threshold(card):
        if config['tag'] or config['flag'] or config.action != Action.No:
            CollectionOp(
                parent=cast(QWidget, reviewer.web), op=lambda col: act_on_card(col, card)
            ).success(
                lambda out: notify(action_msg(agains, passed)) if out else None
            ).run_in_background()
    elif config['again_notify'] is True:
        notify(info_msg(card, agains, passed))


def init():
    gui_hooks.reviewer_did_answer_card.append(on_did_answer_card)
def act_on_card(col: Collection, card: Card) -> ResultWithChanges:
    pos = col.add_custom_undo_entry("Mortician: modify difficult card")

    if config['tag'] and not (note := card.note()).has_tag(config['tag']):
        note.add_tag(config['tag'])
        col.update_note(note)

    if Color.No != config.flag != Color(card.user_flag()):
        col.set_user_flag_for_cards(config.flag.value, cids=[card.id, ])

    if config.action == Action.Bury:
        col.sched.bury_cards(ids=[card.id, ], manual=False)
        sched_reset(col)
    elif config.action == Action.Suspend:
        col.sched.suspend_cards(ids=[card.id, ])
        sched_reset(col)

    return col.merge_undo_entries(pos)

https://ankiweb.net/shared/info/1255924302

IIRC CollectionOps are serialized, so if you wrap your code in it, it should prevent other operations from interleaving with yours. That will also avoid hanging the UI if it takes too long.

Please note that changing the cards like this during review (whether in a CollectionOp or not) is going to waste a fair few CPU cycles and lead to slower display of the next card, as it will cause the queues to be rebuilt after every card is answered. A more efficient approach would be to do this processing in bulk, when the user is not in the middle of reviews.

So is it possible to merge my operations to the last other’s undo entry?

If the other add-on executed first and they merged their changes with the review entry, then your code should merge the changes with the review entry too.

But my add-on doesn’t know the action executed by other add-ons. They could use add_custom_undo_entry or merge into the col.undo_status().last_step.

True, but if they’re occurring at the time you run the hook, chances are they’re also using the hook, and if so, they should probably also be merging their changes into it, or it will make it hard for the user to undo reviews.

Fine. I have rewritten my code and use add_custom_undo_entry().

Sorry, I think we may have crossed-wires here. If you want to continue making changes on every review, I was suggesting you continue using your existing approach (merging into whatever happened before), as chances are that’ll either be the last review, or another add-on that (should have) modified it.

My original approach works in the fronter case, but causes error in the latter case. That is the main topic in this post.

For example, I find that these code will induce target undo op not found after user presses the rating button:

@reviewer_did_answer_card.append
def feature_one():
    undo_entry = mw.col.undo_status().last_step
    ......
    mw.col.merge_undo_entries(undo_entry)

@reviewer_did_answer_card.append
def feature_two():
    undo_entry = mw.col.undo_status().last_step
    ......
    mw.col.merge_undo_entries(undo_entry)

What happens when you run that code in a CollectionOp?

The Error will be displayed in a popup

image

Can you link to the code you’re testing with?

This popup occurs less frequently then before.

I’m trying to make a minimal reproducible case.

Collection for reproduction: reproduction.colpkg - Google Drive

Steps to reproduce the error:

  1. install AJT Mortician and fsrs4anki-helper.ankiaddon - Google Drive

  2. enable auto-disperse and auto-reschedule

  1. enable auto-bury

  1. enter into Default deck and press again: