feat(agent): preview public replies in push notifications
TestFlight / Build and upload (push) Successful in 1m53s

This commit is contained in:
2026-10-14 23:36:38 -07:00
parent b9a6f5dd7d
commit 7510603b77
14 changed files with 335 additions and 32 deletions
+11 -5
View File
@@ -17,6 +17,8 @@ self.addEventListener("fetch", (event) => {
// Safari requires a visible notification for each valid message push. Reads are
// reconciled only by foreground page messages, never an invisible Web Push.
const PUSH_TITLE = "Sybil Agent", PUSH_BODY = "You have a new message.", PUSH_URL = "/?agent=root";
const bytes = value => new TextEncoder().encode(value).length;
const preview = value => typeof value === "string" && value.trim().length > 0 && bytes(value) <= 3000;
const sourceId = value => typeof value === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
const cursor = value => typeof value === "string" && /^\d{1,16}$/.test(value) && Number.isSafeInteger(Number(value));
const messageId = value => typeof value === "string" && /^[A-Za-z0-9:_-]{1,200}$/.test(value);
@@ -67,20 +69,24 @@ async function announce(type) {
self.addEventListener("push", event => {
event.waitUntil(serial(async () => {
let value;
try { const raw = event.data?.text(); if (!raw || raw.length > 4096) return; value = JSON.parse(raw); } catch { return; }
try { const raw = event.data?.text(); if (!raw || bytes(raw) > 3993) return; value = JSON.parse(raw); } catch { return; }
if (!owned(value) || value.url !== PUSH_URL || value.tag !== `sybil-agent:${value.sourceId}`
|| value.title !== PUSH_TITLE || value.body !== PUSH_BODY) return;
|| value.title !== PUSH_TITLE || !preview(value.body)) return;
const previous = await stored(value.sourceId).catch(() => null);
// Re-delivery replaces the same owned tag without regressing its cursor.
// Still show it visibly: local read state is not a reason for an invisible push.
const identity = owned(previous?.latest) && BigInt(previous.latest.cursor) > BigInt(value.cursor) ? previous.latest : {
const stale = owned(previous?.latest) && BigInt(previous.latest.cursor) > BigInt(value.cursor);
const identity = stale ? previous.latest : {
version: 1, kind: "message", sourceId: value.sourceId, messageId: value.messageId, cursor: BigInt(value.cursor).toString(),
};
// Keep the displayed text paired with the retained identity across late pushes/restarts.
// Older workers persisted identity only, so their retained notifications use the fallback.
const body = stale ? preview(previous.latestBody) ? previous.latestBody : PUSH_BODY : value.body;
await self.registration.showNotification(PUSH_TITLE, {
body: PUSH_BODY, tag: `sybil-agent:${identity.sourceId}`, icon: "/icons/icon-192.png", badge: "/icons/icon-192.png",
body, tag: `sybil-agent:${identity.sourceId}`, icon: "/icons/icon-192.png", badge: "/icons/icon-192.png",
renotify: false, data: identity,
});
await save({ ...previous, sourceId: identity.sourceId, latest: identity }).catch(() => undefined);
await save({ ...previous, sourceId: identity.sourceId, latest: identity, latestBody: body }).catch(() => undefined);
await announce("sybil-push-shown");
}));
});