Skip to content
Source
Sign inSign up
Open History to see the last commit

fcmPayloads.ts

82 lines3.3 KB

1import type { RelayAgentActivityAggregateState } from "@t3tools/contracts/relay";
2import {
3 activityPhasePriority,
4 TERMINAL_AGENT_ACTIVITY_DISPLAY_TTL_MS,
5} from "./agentActivityAggregate.ts";
6import { agentActivityExpiresAt } from "./agentActivityPayloads.ts";
7​
8export function androidActivityData(aggregate: RelayAgentActivityAggregateState | null) {
9 const rows = [...(aggregate?.activities ?? [])].sort(
10 (a, b) => activityPhasePriority(a.phase) - activityPhasePriority(b.phase),
11 );
12 const activeCount = aggregate?.activeCount ?? 0;
13 const attentionCount = rows.filter((row) => activityPhasePriority(row.phase) === 0).length;
14 const failed = rows.some((row) => row.phase === "failed");
15 const clean = (value: string) => value.replace(/\s+/g, " ").trim();
16 const lines = rows.map((row) =>
17 [row.status, clean(row.threadTitle), clean(row.projectTitle)].join("\t"),
18 );
19 const hero = rows[0];
20 const title =
21 activeCount > 0
22 ? `${activeCount} active agent${activeCount === 1 ? "" : "s"}${attentionCount ? ` · ${attentionCount} need${attentionCount === 1 ? "s" : ""} attention` : ""}`
23 : failed
24 ? "Agent work failed"
25 : "Agent work completed";
26 const expiresAt = Math.max(
27 0,
28 ...rows.map((row) =>
29 row.phase === "completed" || row.phase === "failed"
30 ? Date.parse(row.updatedAt) + TERMINAL_AGENT_ACTIVITY_DISPLAY_TTL_MS
31 : agentActivityExpiresAt(row),
32 ),
33 );
34 return {
35 active: String(activeCount > 0),
36 activity_chip: activeCount > 0 ? (attentionCount > 0 ? "Review" : "Active") : "",
37 activity_title: title,
38 activity_phase: hero?.phase ?? "",
39 activity_active_count: String(activeCount),
40 activity_attention_count: String(attentionCount),
41 activity_body: hero
42 ? `${hero.status}: ${clean(hero.threadTitle)} · ${clean(hero.projectTitle)}`
43 : "",
44 ...Object.fromEntries(lines.map((line, index) => [`activity_line_${index}`, line])),
45 activity_path: rows[0]?.deepLink ?? "/",
46 activity_expires_at: String(expiresAt),
47 };
49​
50/** Keep Unicode, escaping and grouped alerts within FCM's 4 KB data budget. */
51export function fitFcmData(input: Readonly<Record<string, string>>): Record<string, string> {
52 const data = { ...input };
53 const encoder = new TextEncoder();
54 const textKeys = Object.keys(data).filter(
55 (key) => key.endsWith("_body") || key.endsWith("_title") || key.startsWith("activity_line_"),
56 );
57 while (encoder.encode(JSON.stringify(data)).length > 3800) {
58 const key = textKeys.sort(
59 (a, b) => encoder.encode(data[b]!).length - encoder.encode(data[a]!).length,
60 )[0];
61 if (!key) break;
62 if (data[key]!.length <= 8) {
63 textKeys.splice(textKeys.indexOf(key), 1);
64 continue;
65 }
66 const parts = key.startsWith("activity_line_") ? data[key]!.split("\t") : [data[key]!];
67 const part = parts.length === 3 ? (parts[1]!.length > parts[2]!.length ? 1 : 2) : 0;
68 const characters = Array.from(parts[part]!);
69 // Five characters would become four plus the ellipsis and never shrink.
70 if (characters.length <= 5) {
71 textKeys.splice(textKeys.indexOf(key), 1);
72 continue;
73 }
74 parts[part] =
75 characters
76 .slice(0, Math.floor(characters.length * 0.8))
77 .join("")
78 .trimEnd() + "…";
79 data[key] = parts.join("\t");
80 }
81 return data;