You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ActivityLogGuard.begin() blocks a genuinely in-flight parallel duplicate as if it crashed #442
When two identical irreversible actions (e.g. two send_message calls with the same inputs) land in the same parallel batch, ActivityLogGuard.begin() records INTENT for the first one, then sees that same INTENT row for the second call and treats it as a crashed prior attempt. It downgrades the row to FAILED and blocks the second call with "a previous attempt was interrupted ... may have already happened," even though the first call is still genuinely running, not crashed.
begin()'s DONE branch already has an age check (DONE_DEDUP_WINDOW_SECONDS) before treating a completed row as stale enough to let a repeat through. The INTENT branch (the final else in begin(), app/triggers/activity_log.py) has no equivalent check, so it can't tell "this row belongs to a batch mate still running" from "this row is an attempt that crashed minutes or hours ago."
What I expected
A parallel duplicate that's still genuinely executing shouldn't be told it might have already fired with no record. Only a row old enough to plausibly be a crash should get the "may have happened, verify" treatment.
Steps to reproduce
Driving ActivityLog/ActivityLogGuard directly, matching how execute_parallel dispatches (both begin() calls fire before either action's complete() is reached):
guard.begin("send_message", {"to": "alice", "text": "hi"}, "sess-1") returns GuardDecision(proceed=True, ...) and records INTENT.
Immediately after, before anything calls guard.complete(...) for call 1, the same call again: guard.begin("send_message", {"to": "alice", "text": "hi"}, "sess-1") returns GuardDecision(proceed=False, note="... was NOT executed. A previous attempt ... was interrupted ...").
Call 1 hasn't crashed, it's still in flight, but the second legitimate parallel call gets blocked and the LLM is told the action may have already gone out.
Environment
Reproduced against dev @ 55b12be2 in a clean python:3.12-slim container, driving the real app/triggers/activity_log.py module directly, no mocks.
Logs / screenshots
Call A: GuardDecision(proceed=True, idem_key='sess-1:send_message:82ccaf390655b439', stored_output=None, note=None)
Call B: GuardDecision(proceed=False, idem_key='sess-1:send_message:82ccaf390655b439', stored_output=None,
note="send_message was NOT executed. A previous attempt with these exact inputs was interrupted
before its outcome could be recorded ...")
ActivityLogGuard now tracks idem_keys with an INTENT this process recorded and hasn't complete()d, so begin() can tell a live batch mate apart from a crash-then-restart — a restart builds a fresh guard with an empty set, which age alone could never distinguish.
Verified rather than assumed: the regression test fails against the pre-fix code and passes with it, on both branches. Full V1.4.3 suite 1312 passed, 0 failed.
This stayed open because "Closes #442" only auto-closes on a merge into the default branch, and #443 targeted dev.
Activity
fix #442: don't flag an in-flight parallel duplicate as crashed
korivi-CraftOS commentedon Sep 23, 2026
Fixed and merged on both active branches.
dev— fix: don't flag an in-flight parallel duplicate as crashed #443 (the original fix)V1.4.3— fix: don't flag an in-flight parallel duplicate as crashed (port of #443) #462 (port; V1.4.3'sactivity_log.pywas byte-identical to dev's pre-fix version, so it carried the same bug)ActivityLogGuardnow tracks idem_keys with an INTENT this process recorded and hasn'tcomplete()d, sobegin()can tell a live batch mate apart from a crash-then-restart — a restart builds a fresh guard with an empty set, which age alone could never distinguish.Verified rather than assumed: the regression test fails against the pre-fix code and passes with it, on both branches. Full V1.4.3 suite 1312 passed, 0 failed.
This stayed open because "Closes #442" only auto-closes on a merge into the default branch, and #443 targeted
dev.