fix(claw): dispatch reason-less forge notifications to the thread's agent

Gitea's notifications feed names no per-row reason, so the adapter
collapses every row under one fixed NotificationReason::Other spelling
— and the claw's triage mapped every Other to Surface: the notification
reached the device queue and never a dispatch. A comment on a thread
the account authored could therefore never wake the agent on a
reason-less forge, whatever the watches said.

The adapter now publishes that spelling as gitea::COLLAPSED_REASON —
it is the seam's contract, not a detail — and the triage treats it as
the known shape it is: a notification on a thread of the account's own
dispatches the implementer. A spelling no adapter owns still surfaces;
an unknown shape remains the user's to judge, never the agent's to
guess at.
This commit is contained in:
vhaudiquet committed 2026-10-19 19:40:44 +02:00
1 parent 81da26b955
commit 04549a7df5
2 files changed
+42 -3

No files matched your search

@@ -455,10 +455,18 @@ fn parent_number(url: &str) -> Option<u64> {
url.trim_end_matches('/').rsplit('/').next()?.parse().ok()
}
/// The fixed [`NotificationReason::Other`] spelling every reason-less
/// notifications row maps to (see [`notification_event`]). The
/// consumer's triage matches this spelling to act on the row —
/// an `Other` it does not know only surfaces — so the constant is
/// the seam's contract, not a detail of this module.
pub const COLLAPSED_REASON: &str = "gitea";
/// The notification a notifications row carries. Gitea's feed names no
/// per-row reason — the thread subject is all a row carries — so every
/// notification maps to [`NotificationReason::Other`] with a fixed
/// spelling, and the consumer's triage treats it as its default role.
/// notification maps to [`NotificationReason::Other`] under
/// [`COLLAPSED_REASON`], and the consumer's triage acts on that
/// spelling as its default role.
fn notification_event(row: &WireNotification) -> Result<Option<Notification>, ForgeError> {
let id = required(row.id, "notification id")?;
let updated = required(row.updated_at.clone(), "notification updated_at")?;
@@ -478,7 +486,7 @@ fn notification_event(row: &WireNotification) -> Result<Option<Notification>, Fo
})?;
Ok(Some(Notification {
id: format!("gitea:notification:{id}@{}", updated.raw()),
reason: NotificationReason::Other("gitea".to_owned()),
reason: NotificationReason::Other(COLLAPSED_REASON.to_owned()),
repo: RepoRef::new(owner, name),
number,
is_pr: subject.kind.as_deref() == Some("Pull"),