Skip to content

fix(cli): stamp repo announcement updates with max(head+1, now) - #4509

Open
redirwin wants to merge 1 commit into
block:mainfrom
redirwin:fix/repo-update-stale-timestamp
Open

fix(cli): stamp repo announcement updates with max(head+1, now)#4509
redirwin wants to merge 1 commit into
block:mainfrom
redirwin:fix/repo-update-stale-timestamp

Conversation

@redirwin

@redirwin redirwin commented Aug 3, 2026

Copy link
Copy Markdown

Summary

build_updated_repo_announcement stamps replacement announcements at the observed head's created_at + 1 (an anti-leapfrog guard), while the relay's ingest gate rejects any event more than ±15 minutes from server time (buzz-relay/src/handlers/ingest.rs). Once an announcement is older than that window, every update built from it — repos protect set, repos protect remove, and repos bind — is rejected with event timestamp too far from server time, so branch protection becomes permanently unchangeable 15 minutes after a repo is announced.

This stamps updates with max(head + 1, now) instead: still strictly after the head the update was derived from (a peer-clock-ahead head within the drift window keeps winning by +1), but never in the past, so the relay accepts it.

Reproduced live today on a 0.4.x relay: a byte-fresh protect set against a day-old announcement was refused; re-announcing via repos create (which stamps wall clock) and re-running protect set within the window succeeded — consistent with the diagnosis in #2876.

Related issue

Fixes #2876
Fixes #4431
Fixes #4432

No existing fix PR found (searched open PRs for "timestamp announcement" / "created_at repos").

Testing

  • Adjusted the two unit tests that pinned the old head + 1 behavior on stale heads (created_at 100) to assert wall-clock stamping.
  • Added update_of_fresh_head_stays_monotonic_over_it: a head 100 s in the future still yields head + 1, preserving the monotonicity guarantee the original comment protects.
  • Local cargo test -p buzz-cli was not runnable on the authoring machine (Windows, no Rust toolchain; hermit shims are POSIX-only) — relying on CI for the run. The change is confined to one timestamp expression plus tests; flagging this explicitly rather than claiming a local pass.

build_updated_repo_announcement stamps replacement announcements at the
observed head's created_at + 1 to stay monotonic over the head it was
derived from. The relay's ingest gate rejects events more than ±15
minutes from server time, so once an announcement is older than that
window every update built from it — repos protect set, protect remove,
and bind — is rejected with 'event timestamp too far from server time',
making protection rules permanently unchangeable.

Stamp updates with max(head + 1, now) instead: still strictly after the
observed head (a peer-clock-ahead head within the drift window keeps
winning by +1), but never in the past, so the relay accepts it.

Adjusts the two tests that pinned the old head+1 behavior on stale heads
and adds a regression test for the future-head monotonicity case.

Fixes block#2876
Fixes block#4431
Fixes block#4432

Signed-off-by: David Irwin <david@redirwin.com>
@redirwin
redirwin requested a review from a team as a code owner August 3, 2026 10:37

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 699773eae2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

.as_secs()
.checked_add(1)
.ok_or_else(|| CliError::Other("repository timestamp cannot be advanced".into()))?;
let next_created_at = bumped_head.max(Timestamp::now().as_secs());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the observed-head fence when refreshing timestamps

When a CLI process builds from a stale announcement—such as after a delayed query response, replica lag, or a concurrent update—using its later wall-clock time makes the reconstructed event newer than the intervening head. replace_parameterized_event only compares (created_at, id), so the relay accepts this event instead of returning the conflict handled by validate_write_response; because the tags were copied from the stale event, this can silently erase newly added protection rules or restore an obsolete channel binding. Refreshing an old timestamp therefore needs a server-side conditional/CAS mechanism rather than allowing now to leapfrog an unobserved head.

Useful? React with 👍 / 👎.

@Chessing234

Copy link
Copy Markdown
Contributor

max(head+1, now) is the right stamp for the relay drift window — the fresh-head monotonic test covers the leapfrog case well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants