7.3 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.rsL254-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-34relaystag.- 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
- The announcement (kind 30617) is addressable and effectively static; pushes replace only the state event (kind 30618).
- 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.
- Opening the repository triggers the per-repo fetch; the state event lands in the
database, and
RepoListStoredoes react toRepoStateupdates (crates/signed_state/src/repos.rsL69-82), so the card corrects itself.
Secondary issues found in the same area
-
RepoStoreignores live state events.subscribe_backend(crates/signed_state/src/repo.rsL195-239) matches relay updates viaUpdate.coordinate, which is built fromatags only (crates/signed_nostr/src/update.rsL13-21). A kind30618event carriesd, nota, and the kind check only acceptsGitRepoAnnouncement. A state update arriving from a relay therefore does not refresh an open repository panel'shead.RepoListStoreis unaffected because its check is kind-based. -
Locally published state events refresh nothing.
BackendEvent::Publishedis ignored forRepoStateby both stores (crates/signed_state/src/repos.rsL83-92 andcrates/signed_state/src/repo.rsL221-231). The comment incrates/signed_state/src/backend.rsL907-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. -
Usage counts are similarly under-scoped. The Popular ranking (
crates/signed_state/src/repos.rsL239-261) counts only issues, PRs and patches present locally, i.e. mostly repositories the user has opened. -
The Recent tab is not "recently updated".
RepoFilter::Recent(crates/workspace/src/views/repo_list.rsL73-84) truncates the announcement-sorted list (crates/signed_state/src/repos.rsL189-190), so it orders by creation, not bylast_activity. -
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). — implemented.
RepoListStore::subscribe_remotenow also syncsfilters::all_states()(Filter::new().kind(Kind::RepoState)) alongside the announcement sync. Kind30618is 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. — implemented.
RepoListStorefetches each own repository's state event from the relays that repository announces, once the list discovers it. Driving this from the list rather thansync_inboxsidesteps the login race described above: whenever the announcement list is (re)loaded, any own repository not yet fetched is covered. The fetched-set is cleared onSignerChanged, so a login or account switch re-fetches. -
C. Reactivity fixes. — implemented.
Kind::RepoStateis now included in theBackendEvent::Publishedrefresh condition of bothRepoListStoreandRepoStore.RepoStore::subscribe_backendalso matches relay state updates by owner plus kind — state events carryd, nota, so coordinate matching misses them — and locally published state updates by owner plus thedidentifier.
A + B + C are implemented.