Summary
persist_command_event applies NIP-33 coordinate replacement to every command event that carries a d tag, gated on the tag's presence rather than on the kind being parameterized-replaceable. created_at is second-resolution, so when two command events for the same coordinate land in the same wall-clock second, the stale-write guard breaks the tie on event id and the loser is discarded — reported as accepted: true, exit 0.
Two consequences:
- Workflow definition updates are lost. A
workflows update issued in the same second as the preceding create/update is dropped before upsert_workflow runs, so the definition event and the scheduler both keep the old YAML. The executor then runs the stale steps.
- Manual triggers are swallowed. Kind 46020 is not a replaceable kind, but it carries
d=<workflow_id>, so it goes through the same coordinate dedup. Repeat triggers within a second are silent no-ops. Same for 46030/46031 (approval grant/deny).
Environment
- Buzz Desktop 0.5.0, macOS 26.2 (Apple Silicon)
- Relay: a self-hosted deployment of this repo, NIP-11
version: 0.2.0
- Source read at
6300a6b1d03e32c473c7b6568df663c8927565cf (main)
Reproduction
Create then update in the same second:
$ buzz workflows create --channel <ch> --yaml - # text: "EXEC-V1"
{"accepted":true,"event_id":"7fefe8fa…","message":"response:{…\"workflow_id\":\"867b6baa-…\"}"}
$ buzz workflows update --channel <ch> --workflow 867b6baa-… --yaml - # text: "EXEC-V2"
{"accepted":true,"message":"duplicate: already processed"} # exit 0
$ buzz workflows trigger --workflow 867b6baa-…
$ buzz messages get --channel <ch>
EXEC-V1 # the definition the operator replaced
With a >1s gap the same sequence works. Twelve updates in a tight loop lost 3 of 12, depending only on whether the new event id sorts below the stored one. Five manual triggers in one second produced 2 runs.
Root cause
crates/buzz-relay/src/handlers/command_executor.rs:128-190, inside persist_command_event:
let d_tag = buzz_db::event::extract_d_tag(event);
if let Some(ref d_tag) = d_tag {
…
let dominated = created_at < existing_ts
|| (created_at == existing_ts && incoming_id >= existing_id.as_slice());
if dominated {
return Ok(PersistResult::Duplicate);
}
- The gate is
d_tag.is_some(), not is_parameterized_replaceable(kind). buzz_core::kind::is_parameterized_replaceable already exists and is used for exactly this in ingest.rs:2420 and side_effects.rs:2159.
- For 30620, where replacement is correct, nothing prevents the same-second tie.
PersistResult::Duplicate returns at command_executor.rs:754, before upsert_workflow at :784, so the control-plane row is not updated either.
The tie-break itself matches NIP-01. The bug is that nothing prevents the tie, and that losing it is reported as success.
Related
The block was added by #1369 (merged 2026-06-30), which moved the NIP-33 stale-write guard inline into persist_command_event; #1340 notes the same move. Searched open and closed issues and PRs for persist_command_event, coordinate dedup, and same-second created_at ties — nothing else found for the tie-break itself.
Prior art
The same failure was fixed on the DM-visibility path — side_effects.rs:3182-3206 forces created_at strictly past any prior snapshot, with a regression test at crates/buzz-test-client/tests/e2e_nostr_interop.rs:1324. Those events are relay-signed. Kind 30620 is client-signed, so that guard cannot be applied relay-side without breaking the signature.
Suggested fix
- Gate the dedup block on
is_parameterized_replaceable(kind_u32) && d_tag.is_some(). Fixes triggers and approvals, which never belonged in this path.
- Distinguish "this exact event id was already stored" from "a newer write lost the coordinate tie and was discarded". Today both return
accepted: true, so a client cannot tell a replay from data loss; the latter should be a rejection with a non-zero exit.
- With (2) in place,
cmd_update_workflow can sign with custom_created_at(max(now, prior + 1)) — the DM-path guard, moved client-side because the event is client-signed.
(1) and (2) are load-bearing; (3) makes back-to-back scripted updates reliable rather than merely honest about failing.
Scripted setup hits this constantly, because batched calls land in the same second by construction.
Summary
persist_command_eventapplies NIP-33 coordinate replacement to every command event that carries adtag, gated on the tag's presence rather than on the kind being parameterized-replaceable.created_atis second-resolution, so when two command events for the same coordinate land in the same wall-clock second, the stale-write guard breaks the tie on event id and the loser is discarded — reported asaccepted: true, exit 0.Two consequences:
workflows updateissued in the same second as the preceding create/update is dropped beforeupsert_workflowruns, so the definition event and the scheduler both keep the old YAML. The executor then runs the stale steps.d=<workflow_id>, so it goes through the same coordinate dedup. Repeat triggers within a second are silent no-ops. Same for 46030/46031 (approval grant/deny).Environment
version: 0.2.06300a6b1d03e32c473c7b6568df663c8927565cf(main)Reproduction
Create then update in the same second:
With a >1s gap the same sequence works. Twelve updates in a tight loop lost 3 of 12, depending only on whether the new event id sorts below the stored one. Five manual triggers in one second produced 2 runs.
Root cause
crates/buzz-relay/src/handlers/command_executor.rs:128-190, insidepersist_command_event:d_tag.is_some(), notis_parameterized_replaceable(kind).buzz_core::kind::is_parameterized_replaceablealready exists and is used for exactly this iningest.rs:2420andside_effects.rs:2159.PersistResult::Duplicatereturns atcommand_executor.rs:754, beforeupsert_workflowat:784, so the control-plane row is not updated either.The tie-break itself matches NIP-01. The bug is that nothing prevents the tie, and that losing it is reported as success.
Related
The block was added by #1369 (merged 2026-06-30), which moved the NIP-33 stale-write guard inline into
persist_command_event; #1340 notes the same move. Searched open and closed issues and PRs forpersist_command_event, coordinate dedup, and same-secondcreated_atties — nothing else found for the tie-break itself.Prior art
The same failure was fixed on the DM-visibility path —
side_effects.rs:3182-3206forcescreated_atstrictly past any prior snapshot, with a regression test atcrates/buzz-test-client/tests/e2e_nostr_interop.rs:1324. Those events are relay-signed. Kind 30620 is client-signed, so that guard cannot be applied relay-side without breaking the signature.Suggested fix
is_parameterized_replaceable(kind_u32) && d_tag.is_some(). Fixes triggers and approvals, which never belonged in this path.accepted: true, so a client cannot tell a replay from data loss; the latter should be a rejection with a non-zero exit.cmd_update_workflowcan sign withcustom_created_at(max(now, prior + 1))— the DM-path guard, moved client-side because the event is client-signed.(1) and (2) are load-bearing; (3) makes back-to-back scripted updates reliable rather than merely honest about failing.
Scripted setup hits this constantly, because batched calls land in the same second by construction.