Files
signed/docs/repo-updated-timestamp.md
T
2026-09-25 17:34:39 +07:00

7.1 KiB

Repository card "Updated" timestamps are stale until first open

Feedback under investigation:

My repositories that I updated yesterday show up as being updated like 6 months ago, but then when I open them and they get cloned that timestamp is fixed, which means you're not reading the repository state event, only the date of the repository announcement event and the git history?

Summary

The reviewer is essentially right. The backend has logic to include kind 30618 repository state events in the "last activity" timestamp, but that logic only reads events that are already present in the local nostr database. The app never globally fetches state events for repositories the user has not opened, so for those the timestamp falls back to the announcement's created_at — the repository's creation date. Opening (and cloning) the repository fetches the state event, which is why the timestamp corrects itself afterwards.

The "git history" part of the hypothesis is not accurate for this label: no git-derived timestamp feeds the repository card. The only git-derived times in the UI are commit timestamps in commit lists and diffs, which are unrelated.

This is a data-coverage bug, not a missing computation. The code that aggregates state events is correct; it is simply starved of input on the explore list.

Where the timestamp comes from

The label is rendered in crates/workspace/src/views/repo_list.rs (L209-212):

let activity = last_activity
    .map(relative_time)
    .map(|label| SharedString::from(format!("Updated {label}")))

last_activity is computed in crates/signed_state/src/repos.rs, RepoListStore::run_refresh (L192-235), as a max over three sources:

Source Code Availability
announcement.created_at (kind 30617) L195-198 always; seeded for every listed repo
kind 30618 repository state events L200-213 only events already in the local database
NIP-34 activity (issues, PRs, patches, statuses, comments) L215-235 only local database, windowed to 90 days (ACTIVITY_WINDOW, L13-14)

The critical gap is the global sync scope. RepoListStore::subscribe_remote (crates/signed_state/src/repos.rs L131-139) only syncs:

backend.sync_bootstrap(filters::all_announcements(), cx);  // kind 30617 only
backend.sync_bootstrap(filters::deletions(), cx);

State events are fetched per repository, on open, via RepoStore:

  • repo_filters (crates/signed_state/src/repo.rs L254-270) queries [GitRepoAnnouncement, RepoState] plus activity and deletions.
  • subscribe_remote (L296-306) runs those filters against the bootstrap relays.
  • connect_announced_relays (L272-294) runs a negentropy sync of the same filters against the relays in the announcement's NIP-34 relays tag.
  • Both fire from RepoStore::new's deferred initialization (L93-112).

Publishes only ever create a state event, never re-publish the announcement: sign_state_event in crates/signed_state/src/backend.rs (L1655-1682) and the fan-out at L907-918. A repository created 6 months ago and pushed yesterday therefore has an announcement dated 6 months ago and a state event dated yesterday.

flowchart TD
    A[App start / login] --> B[RepoListStore sync: 30617 announcements + deletions]
    B --> C[Explore card: Updated = announcement created_at]
    C --> D{Repo opened?}
    D -- no --> C
    D -- yes --> E[RepoStore fetches 30617 + 30618 from bootstrap + announced relays]
    E --> F[State event stored in local DB]
    F --> G[BackendEvent NostrUpdate RepoState]
    G --> H[RepoListStore refreshes, last_activity recomputed]
    H --> I[Card shows push date]

Why the report reproduces exactly

  1. The announcement (kind 30617) is addressable and effectively static; pushes replace only the state event (kind 30618).
  2. On a database that has never fetched that repository (fresh install, another device, or wasm where the database is in-memory per session), only the announcement is known, so the card shows the creation date.
  3. Opening the repository triggers the per-repo fetch; the state event lands in the database, and RepoListStore does react to RepoState updates (crates/signed_state/src/repos.rs L69-82), so the card corrects itself.

Secondary issues found in the same area

  1. RepoStore ignores live state events. subscribe_backend (crates/signed_state/src/repo.rs L195-239) matches relay updates via Update.coordinate, which is built from a tags only (crates/signed_nostr/src/update.rs L13-21). A kind 30618 event carries d, not a, and the kind check only accepts GitRepoAnnouncement. A state update arriving from a relay therefore does not refresh an open repository panel's head. RepoListStore is unaffected because its check is kind-based.

  2. Locally published state events refresh nothing. BackendEvent::Published is ignored for RepoState by both stores (crates/signed_state/src/repos.rs L83-92 and crates/signed_state/src/repo.rs L221-231). The comment in crates/signed_state/src/backend.rs L907-909 ("publishing notifies the repository views") is inaccurate: after an in-app push, the explore card stays stale until the next unrelated refresh, sync or restart, even though the event is in the local database.

  3. Usage counts are similarly under-scoped. The Popular ranking (crates/signed_state/src/repos.rs L239-261) counts only issues, PRs and patches present locally, i.e. mostly repositories the user has opened.

  4. The Recent tab is not "recently updated". RepoFilter::Recent (crates/workspace/src/views/repo_list.rs L73-84) truncates the announcement-sorted list (crates/signed_state/src/repos.rs L189-190), so it orders by creation, not by last_activity.

  5. The 90-day activity window is deliberate. Activity-driven bumps decay after ACTIVITY_WINDOW; only state events would bump indefinitely once fetched (L13-14).

Remediation options

  • A. Global state sync (smallest change). Add sync_bootstrap(Filter::new().kind(Kind::RepoState)) alongside the announcement sync in RepoListStore::subscribe_remote. Kind 30618 is addressable, so relays keep one event per repository — volume comparable to announcements. Caveat: coverage depends on whether state events reach the bootstrap relays; if a grasp server is the only holder, those repositories remain unseen.

  • B. Targeted fetch for the user's own repositories. In sync_inbox (crates/signed_state/src/backend.rs L1126-1152), where the user's own repositories' relays are already connected, also sync filters::state(addr) per own repository. This directly addresses "my repositories updated elsewhere" at low cost. Note the existing race: sync_inbox reads the announcement list right after login, which may not be loaded yet.

  • C. Reactivity fixes. Include Kind::RepoState in the Published refresh conditions, and in RepoStore::subscribe_backend either match kind plus author, or extend Update to carry the d-tag identifier.

Suggested combination: A + C, plus B if own-repository freshness on fresh profiles matters.