@agent-native/core 0.139.0 → 0.141.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
  3. package/corpus/templates/analytics/changelog/2026-08-05-historical-analytics-rollups-stay-complete-and-atomic.md +6 -0
  4. package/corpus/templates/analytics/server/lib/dashboard-report.ts +0 -2
  5. package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +11 -6
  6. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +8 -18
  7. package/corpus/templates/analytics/server/plugins/db.ts +174 -0
  8. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +38 -38
  9. package/corpus/templates/calendar/changelog/2026-08-05-out-of-office-blocks-now-open-from-their-scheduled-time-rang.md +6 -0
  10. package/corpus/templates/calendar/server/lib/booking-emails.ts +70 -154
  11. package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +28 -64
  12. package/corpus/templates/clips/actions/get-recording-player-data.ts +1 -0
  13. package/corpus/templates/clips/actions/invite-member.ts +16 -39
  14. package/corpus/templates/clips/app/components/player/comments-panel.tsx +2 -3
  15. package/corpus/templates/clips/app/components/player/reaction-emojis.ts +10 -0
  16. package/corpus/templates/clips/app/components/player/reactions-tray.tsx +2 -2
  17. package/corpus/templates/clips/app/components/player/video-player.tsx +20 -6
  18. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +33 -0
  19. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +5 -1
  20. package/corpus/templates/clips/app/routes/share.$shareId.tsx +27 -1
  21. package/corpus/templates/clips/changelog/2026-08-05-comment-and-video-reaction-pickers-now-include-eyes-and-mind.md +6 -0
  22. package/corpus/templates/clips/changelog/2026-08-05-fresh-recordings-now-open-with-the-complete-video.md +6 -0
  23. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +0 -24
  24. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
  25. package/corpus/templates/content/server/lib/comment-notifications.ts +26 -57
  26. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +16 -3
  27. package/corpus/templates/forms/server/lib/response-email.ts +0 -2
  28. package/corpus/templates/plan/actions/request-plan-access.ts +12 -33
  29. package/corpus/templates/plan/server/lib/comment-notifications.ts +27 -53
  30. package/corpus/templates/slides/server/lib/comment-notifications.ts +21 -44
  31. package/dist/a2a/handlers.js +9 -10
  32. package/dist/collab/routes.d.ts +1 -1
  33. package/dist/db/client.js +8 -8
  34. package/dist/db/migrations.js +39 -19
  35. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  36. package/dist/localization/default-messages.d.ts +0 -45
  37. package/dist/localization/default-messages.js +0 -45
  38. package/dist/notifications/routes.d.ts +5 -5
  39. package/dist/observability/routes.d.ts +3 -3
  40. package/dist/org/handlers.js +1 -8
  41. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  42. package/dist/provider-api/actions/provider-api.d.ts +12 -12
  43. package/dist/secrets/routes.d.ts +9 -9
  44. package/dist/server/action-discovery.js +0 -22
  45. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  46. package/dist/server/auth.js +3 -1
  47. package/dist/server/better-auth-instance.js +2 -17
  48. package/dist/server/email.d.ts +0 -9
  49. package/dist/server/email.js +3 -44
  50. package/dist/server/index.d.ts +0 -1
  51. package/dist/server/index.js +0 -1
  52. package/dist/server/transcribe-voice.d.ts +1 -1
  53. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +3 -2
  54. package/docs/content/authentication.mdx +25 -19
  55. package/package.json +2 -1
  56. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +3 -2
  57. package/corpus/templates/analytics/server/lib/emails.ts +0 -51
  58. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +0 -5
  59. package/corpus/templates/calendar/server/lib/emails.ts +0 -167
  60. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +0 -5
  61. package/corpus/templates/clips/server/lib/emails.ts +0 -256
  62. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +0 -5
  63. package/corpus/templates/content/server/lib/emails.ts +0 -70
  64. package/corpus/templates/content/server/plugins/transactional-emails.ts +0 -5
  65. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +0 -4
  66. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +0 -4
  67. package/corpus/templates/forms/server/lib/emails.ts +0 -52
  68. package/corpus/templates/forms/server/plugins/transactional-emails.ts +0 -5
  69. package/corpus/templates/plan/server/lib/emails.ts +0 -67
  70. package/corpus/templates/plan/server/plugins/transactional-emails.ts +0 -5
  71. package/corpus/templates/slides/server/lib/emails.ts +0 -40
  72. package/corpus/templates/slides/server/plugins/transactional-emails.ts +0 -5
  73. package/dist/email-catalog/actions/list-email-activity.d.ts +0 -6
  74. package/dist/email-catalog/actions/list-email-activity.js +0 -15
  75. package/dist/email-catalog/actions/list-email-engagement.d.ts +0 -6
  76. package/dist/email-catalog/actions/list-email-engagement.js +0 -15
  77. package/dist/email-catalog/actions/list-email-log.d.ts +0 -8
  78. package/dist/email-catalog/actions/list-email-log.js +0 -22
  79. package/dist/email-catalog/actions/list-transactional-emails.d.ts +0 -23
  80. package/dist/email-catalog/actions/list-transactional-emails.js +0 -61
  81. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +0 -10
  82. package/dist/email-catalog/actions/render-transactional-email-preview.js +0 -24
  83. package/dist/email-catalog/authorize.d.ts +0 -2
  84. package/dist/email-catalog/authorize.js +0 -10
  85. package/dist/email-catalog/log.d.ts +0 -57
  86. package/dist/email-catalog/log.js +0 -117
  87. package/dist/email-catalog/provider-metrics.d.ts +0 -34
  88. package/dist/email-catalog/provider-metrics.js +0 -165
  89. package/dist/email-catalog/registry.d.ts +0 -79
  90. package/dist/email-catalog/registry.js +0 -71
  91. package/dist/email-catalog/schema.d.ts +0 -212
  92. package/dist/email-catalog/schema.js +0 -49
  93. package/dist/email-catalog/system-emails.d.ts +0 -14
  94. package/dist/email-catalog/system-emails.js +0 -68
@@ -293,6 +293,8 @@ const CLIPS_AGENT_DOCS_URL =
293
293
  "https://www.agent-native.com/docs/template-clips#agent-readable-clips";
294
294
  const UPLOAD_STUCK_TIMEOUT_MS = 5 * 60 * 1000;
295
295
  const PROCESSING_STUCK_TIMEOUT_MS = 12 * 60 * 1000;
296
+ const READY_MEDIA_SETTLE_POLL_MS = 20 * 1000;
297
+ const READY_MEDIA_SETTLE_POLL_INTERVAL_MS = 1000;
296
298
 
297
299
  type ViewerPlatform = "mac" | "windows" | "linux";
298
300
 
@@ -404,6 +406,7 @@ export default function ShareRoute() {
404
406
  }, [recordingId, attribution.ref, attribution.via]);
405
407
 
406
408
  const playerRef = useRef<VideoPlayerHandle | null>(null);
409
+ const readyMediaPollRef = useRef<{ key: string; until: number } | null>(null);
407
410
  const [password, setPassword] = useState<string | null>(() => {
408
411
  if (typeof window === "undefined" || !shareId) return null;
409
412
  try {
@@ -463,7 +466,26 @@ export default function ShareRoute() {
463
466
  // page auto-upgrades from "Processing" to the real player the moment
464
467
  // the server flips status to 'ready' and writes videoUrl. Mirrors
465
468
  // r.$recordingId.tsx's playerDataQ.refetchInterval.
466
- if (rec.status !== "ready" || !rec.videoUrl) return 2000;
469
+ if (rec.status !== "ready" || !rec.videoUrl) {
470
+ readyMediaPollRef.current = null;
471
+ return 2000;
472
+ }
473
+ const mediaKey = [
474
+ rec.id,
475
+ rec.durationMs ?? "",
476
+ rec.videoSizeBytes ?? "",
477
+ rec.videoFormat ?? "",
478
+ ].join(":");
479
+ const now = Date.now();
480
+ if (readyMediaPollRef.current?.key !== mediaKey) {
481
+ readyMediaPollRef.current = {
482
+ key: mediaKey,
483
+ until: now + READY_MEDIA_SETTLE_POLL_MS,
484
+ };
485
+ }
486
+ if (now < readyMediaPollRef.current.until) {
487
+ return READY_MEDIA_SETTLE_POLL_INTERVAL_MS;
488
+ }
467
489
  // Also keep polling while a transcript is pending so "Transcribing…"
468
490
  // auto-flips to the ready transcript (or to the failure card). The
469
491
  // public payload has no transcript.cleanup field (that's authenticated
@@ -985,6 +1007,10 @@ export default function ShareRoute() {
985
1007
  onVideoElementChange={setTrackedVideoEl}
986
1008
  recordingId={recording.id}
987
1009
  videoUrl={recording.videoUrl}
1010
+ mediaVersion={[
1011
+ recording.videoSizeBytes ?? "",
1012
+ recording.updatedAt ?? "",
1013
+ ].join(":")}
988
1014
  videoFormat={recording.videoFormat}
989
1015
  embedProvider={isLoomEmbedBacked ? "loom" : null}
990
1016
  durationMs={recording.durationMs}
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-08-05
4
+ ---
5
+
6
+ Comment and video reaction pickers now include eyes and mind-blown reactions consistently.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-05
4
+ ---
5
+
6
+ Fresh recordings now open with the complete video
@@ -86,29 +86,6 @@ export type ClipsTransactionalEmailInput =
86
86
  videoTimestampMs?: number | null;
87
87
  });
88
88
 
89
- export const CLIPS_FIRST_VIEW_EMAIL_ID = "clips.first-view";
90
- export const CLIPS_UNVIEWED_REMINDER_EMAIL_ID = "clips.unviewed-reminder";
91
- export const CLIPS_FIRST_AGENT_VIEW_EMAIL_ID = "clips.first-agent-view";
92
- export const CLIPS_FIRST_IMPORT_EMAIL_ID = "clips.first-import";
93
- export const CLIPS_MONTHLY_RECAP_EMAIL_ID = "clips.monthly-recap";
94
- export const CLIPS_TWO_CLIPS_EMAIL_ID = "clips.two-clips";
95
- export const CLIPS_ACTIVITY_COMMENT_EMAIL_ID = "clips.activity-comment";
96
- export const CLIPS_ACTIVITY_REACTION_EMAIL_ID = "clips.activity-reaction";
97
-
98
- export const CLIPS_EMAIL_ID_BY_KIND: Record<
99
- ClipsTransactionalEmailInput["kind"],
100
- string
101
- > = {
102
- "first-view": CLIPS_FIRST_VIEW_EMAIL_ID,
103
- "unviewed-reminder": CLIPS_UNVIEWED_REMINDER_EMAIL_ID,
104
- "first-agent-view": CLIPS_FIRST_AGENT_VIEW_EMAIL_ID,
105
- "first-import": CLIPS_FIRST_IMPORT_EMAIL_ID,
106
- "monthly-recap": CLIPS_MONTHLY_RECAP_EMAIL_ID,
107
- "two-clips": CLIPS_TWO_CLIPS_EMAIL_ID,
108
- "activity-comment": CLIPS_ACTIVITY_COMMENT_EMAIL_ID,
109
- "activity-reaction": CLIPS_ACTIVITY_REACTION_EMAIL_ID,
110
- };
111
-
112
89
  export interface RecapTopClipInput {
113
90
  recordingId: string;
114
91
  title?: string | null;
@@ -636,7 +613,6 @@ export async function sendClipsTransactionalEmail(
636
613
  ? (validReplyTo(input.senderEmail) ?? FRIENDLY_REPLY_TO)
637
614
  : FRIENDLY_REPLY_TO,
638
615
  timeoutMs: EMAIL_SEND_TIMEOUT_MS,
639
- templateId: CLIPS_EMAIL_ID_BY_KIND[input.kind],
640
616
  });
641
617
  }
642
618
 
@@ -453,6 +453,7 @@ export default defineEventHandler(async (event) => {
453
453
  editsJson: rec.editsJson,
454
454
  videoUrl: playbackVideoUrl,
455
455
  videoFormat: rec.videoFormat,
456
+ videoSizeBytes: rec.videoSizeBytes ?? null,
456
457
  width: rec.width,
457
458
  height: rec.height,
458
459
  hasAudio: Boolean(rec.hasAudio),
@@ -21,10 +21,6 @@ import { and, eq } from "drizzle-orm";
21
21
 
22
22
  import { CONTENT_USER_PREFS_KEY } from "../../shared/content-user-prefs.js";
23
23
  import { getDb, schema } from "../db/index.js";
24
- import {
25
- CONTENT_DOCUMENT_COMMENT_EMAIL_ID,
26
- CONTENT_DOCUMENT_MENTION_EMAIL_ID,
27
- } from "./emails.js";
28
24
 
29
25
  export type DocumentCommentNotificationResult = ActivityNotificationResult;
30
26
 
@@ -74,48 +70,6 @@ export interface DocumentCommentNotificationInput {
74
70
  isReply: boolean;
75
71
  }
76
72
 
77
- export function renderDocumentCommentEmail({
78
- actor,
79
- title,
80
- url,
81
- content,
82
- isReply,
83
- wasMentioned,
84
- }: {
85
- actor: string;
86
- title: string;
87
- url: string;
88
- content: string;
89
- isReply: boolean;
90
- wasMentioned: boolean;
91
- }) {
92
- const lead = wasMentioned
93
- ? `${emailStrong(actor)} mentioned you in a comment on ${emailStrong(title)}.`
94
- : isReply
95
- ? `${emailStrong(actor)} replied in a comment thread on ${emailStrong(title)}.`
96
- : `${emailStrong(actor)} commented on ${emailStrong(title)}.`;
97
-
98
- return {
99
- subject: wasMentioned
100
- ? `${actor} mentioned you on "${title}"`
101
- : isReply
102
- ? `${actor} replied to a comment on "${title}"`
103
- : `${actor} commented on "${title}"`,
104
- ...renderEmail({
105
- preheader: `${actor} commented on ${title}.`,
106
- heading: wasMentioned
107
- ? "You were mentioned"
108
- : isReply
109
- ? "New reply on your document"
110
- : "New comment",
111
- paragraphs: [lead, `"${excerpt(content)}"`],
112
- cta: { label: "Open document", url },
113
- footer:
114
- "You received this because you own, were mentioned in, or participated in this thread. Turn these off in Documents settings.",
115
- }),
116
- };
117
- }
118
-
119
73
  export async function notifyDocumentComment(
120
74
  input: DocumentCommentNotificationInput,
121
75
  ): Promise<DocumentCommentNotificationResult> {
@@ -158,19 +112,34 @@ async function deliverDocumentCommentEmails(
158
112
  logLabel: LOG_LABEL,
159
113
  send: async (to) => {
160
114
  const wasMentioned = mentioned.has(to);
115
+ const lead = wasMentioned
116
+ ? `${emailStrong(actor)} mentioned you in a comment on ${emailStrong(title)}.`
117
+ : input.isReply
118
+ ? `${emailStrong(actor)} replied in a comment thread on ${emailStrong(title)}.`
119
+ : `${emailStrong(actor)} commented on ${emailStrong(title)}.`;
120
+
121
+ const { html, text } = renderEmail({
122
+ preheader: `${actor} commented on ${title}.`,
123
+ heading: wasMentioned
124
+ ? "You were mentioned"
125
+ : input.isReply
126
+ ? "New reply on your document"
127
+ : "New comment",
128
+ paragraphs: [lead, `"${excerpt(input.content)}"`],
129
+ cta: { label: "Open document", url },
130
+ footer:
131
+ "You received this because you own, were mentioned in, or participated in this thread. Turn these off in Documents settings.",
132
+ });
133
+
161
134
  await sendEmail({
162
- ...renderDocumentCommentEmail({
163
- actor,
164
- title,
165
- url,
166
- content: input.content,
167
- isReply: input.isReply,
168
- wasMentioned,
169
- }),
170
135
  to,
171
- templateId: wasMentioned
172
- ? CONTENT_DOCUMENT_MENTION_EMAIL_ID
173
- : CONTENT_DOCUMENT_COMMENT_EMAIL_ID,
136
+ subject: wasMentioned
137
+ ? `${actor} mentioned you on "${title}"`
138
+ : input.isReply
139
+ ? `${actor} replied to a comment on "${title}"`
140
+ : `${actor} commented on "${title}"`,
141
+ html,
142
+ text,
174
143
  });
175
144
  },
176
145
  });
@@ -162,7 +162,11 @@ function StrokeLayerControl({
162
162
  <div className="min-w-0 flex-1">
163
163
  <ColorInput
164
164
  label=""
165
- value={cssColorOrFallback(color, "#000000")}
165
+ value={cssColorOrFallback(
166
+ color,
167
+ /* guard:allow-raw-color - ColorInput needs a concrete swatch fallback. */
168
+ "#000000",
169
+ )}
166
170
  onChange={(value, meta) =>
167
171
  onStyleChange(`${prefix}Color`, value, meta)
168
172
  }
@@ -449,7 +453,11 @@ export function StrokeProperties({
449
453
  <StrokeLayerControl
450
454
  kind="border"
451
455
  visible={borderVisible}
452
- color={styles.borderColor || "#000000"}
456
+ color={
457
+ styles.borderColor ||
458
+ /* guard:allow-raw-color - a new stroke needs a concrete color. */
459
+ "#000000"
460
+ }
453
461
  width={styles.borderWidth || "0px"}
454
462
  styleValue={styles.borderStyle || "none"}
455
463
  onStyleChange={onStyleChange}
@@ -470,7 +478,12 @@ export function StrokeProperties({
470
478
  <StrokeLayerControl
471
479
  kind="outline"
472
480
  visible={outlineVisible}
473
- color={styles.outlineColor || styles.borderColor || "#000000"}
481
+ color={
482
+ styles.outlineColor ||
483
+ styles.borderColor ||
484
+ /* guard:allow-raw-color - a new stroke needs a concrete color. */
485
+ "#000000"
486
+ }
474
487
  width={styles.outlineWidth || "0px"}
475
488
  styleValue={styles.outlineStyle || "solid"}
476
489
  outlineOffset={styles.outlineOffset || "0px"}
@@ -1,7 +1,6 @@
1
1
  import { emailStrong, renderEmail, sendEmail } from "@agent-native/core/server";
2
2
 
3
3
  import type { FormField } from "../../shared/types.js";
4
- import { FORMS_NEW_RESPONSE_EMAIL_ID } from "./emails.js";
5
4
 
6
5
  export interface NewResponseEmailArgs {
7
6
  to: string;
@@ -62,6 +61,5 @@ export async function sendNewResponseEmail(
62
61
  await sendEmail({
63
62
  to: args.to,
64
63
  ...renderNewResponseEmail(args),
65
- templateId: FORMS_NEW_RESPONSE_EMAIL_ID,
66
64
  });
67
65
  }
@@ -22,8 +22,6 @@ import {
22
22
  } from "../server/lib/local-identity.js";
23
23
  import { newId, nowIso, planPath, writeEvent } from "../server/plans.js";
24
24
 
25
- export const PLAN_ACCESS_REQUEST_EMAIL_ID = "plan.access-request";
26
-
27
25
  function httpError(message: string, statusCode: number): Error {
28
26
  return Object.assign(new Error(message), { statusCode });
29
27
  }
@@ -51,28 +49,6 @@ function absolutePlanUrl(planId: string, kind: "plan" | "recap"): string {
51
49
  }
52
50
  }
53
51
 
54
- export function renderPlanAccessRequestEmail(input: {
55
- requesterName: string;
56
- requesterEmail: string;
57
- planTitle: string;
58
- url: string;
59
- }) {
60
- const subject = `${input.requesterName} requested access to "${input.planTitle}"`;
61
- return {
62
- subject,
63
- ...renderEmail({
64
- preheader: subject,
65
- heading: "Access request",
66
- paragraphs: [
67
- `${emailStrong(input.requesterName)} (${emailStrong(input.requesterEmail)}) requested access to ${emailStrong(input.planTitle)}.`,
68
- "Open the plan and use Share to grant access if this request should be approved.",
69
- ],
70
- cta: { label: "Open plan", url: input.url },
71
- footer: "You received this because you own this Agent-Native Plan.",
72
- }),
73
- };
74
- }
75
-
76
52
  async function notifyOwner(input: {
77
53
  planId: string;
78
54
  planKind: "plan" | "recap";
@@ -86,16 +62,19 @@ async function notifyOwner(input: {
86
62
  return false;
87
63
  }
88
64
 
89
- await sendEmail({
90
- ...renderPlanAccessRequestEmail({
91
- requesterName: input.requesterName,
92
- requesterEmail: input.requesterEmail,
93
- planTitle: input.planTitle,
94
- url: absolutePlanUrl(input.planId, input.planKind),
95
- }),
96
- to: input.ownerEmail,
97
- templateId: PLAN_ACCESS_REQUEST_EMAIL_ID,
65
+ const url = absolutePlanUrl(input.planId, input.planKind);
66
+ const subject = `${input.requesterName} requested access to "${input.planTitle}"`;
67
+ const { html, text } = renderEmail({
68
+ preheader: subject,
69
+ heading: "Access request",
70
+ paragraphs: [
71
+ `${emailStrong(input.requesterName)} (${emailStrong(input.requesterEmail)}) requested access to ${emailStrong(input.planTitle)}.`,
72
+ "Open the plan and use Share to grant access if this request should be approved.",
73
+ ],
74
+ cta: { label: "Open plan", url },
75
+ footer: "You received this because you own this Agent-Native Plan.",
98
76
  });
77
+ await sendEmail({ to: input.ownerEmail, subject, html, text });
99
78
  return true;
100
79
  }
101
80
 
@@ -14,8 +14,6 @@ import {
14
14
  import type { PlanBundle, PlanComment } from "../../shared/types.js";
15
15
  import { getDb, schema } from "../db/index.js";
16
16
 
17
- export const PLAN_COMMENT_EMAIL_ID = "plan.plan-comment";
18
-
19
17
  type CommentNotificationInput = {
20
18
  bundle: PlanBundle;
21
19
  insertedCommentIds: string[];
@@ -207,64 +205,40 @@ export function planCommentNotificationRecipients(input: {
207
205
  return Array.from(recipients.values());
208
206
  }
209
207
 
210
- export function renderPlanCommentEmail(input: {
211
- actor: string;
212
- app: string;
213
- planTitle: string;
214
- planUrl: string;
215
- message: string;
216
- isReply: boolean;
217
- reason: NotificationRecipient["reason"];
218
- }) {
219
- const actionText =
220
- input.reason === "mention"
221
- ? "mentioned you in a comment on"
222
- : input.reason === "plan-owner"
223
- ? "left a comment on your plan"
224
- : "replied in a comment thread you participated in";
225
- return {
226
- subject: input.isReply
227
- ? `${input.actor} replied to a comment on "${input.planTitle}"`
228
- : `${input.actor} commented on "${input.planTitle}"`,
229
- ...renderEmail({
230
- preheader: `${input.actor} ${actionText} on ${input.app}.`,
231
- heading: input.isReply
232
- ? "New reply on your plan"
233
- : "New comment on your plan",
234
- paragraphs: [
235
- `${emailStrong(input.actor)} ${actionText} ${emailStrong(input.planTitle)}.`,
236
- `Comment: "${commentExcerpt(input.message)}"`,
237
- ],
238
- cta: { label: "Open plan", url: input.planUrl },
239
- footer:
240
- input.reason === "plan-owner"
241
- ? "You received this because you own this plan."
242
- : input.reason === "mention"
243
- ? "You received this because you were mentioned in this comment."
244
- : "You received this because you participated in this comment thread.",
245
- }),
246
- };
247
- }
248
-
249
208
  async function sendPlanCommentNotification(input: {
250
209
  recipient: NotificationRecipient;
251
210
  comment: PlanComment;
252
211
  planTitle: string;
253
212
  planId: string;
254
213
  }) {
255
- await sendEmail({
256
- ...renderPlanCommentEmail({
257
- actor: actorName(input.comment),
258
- app: appName(),
259
- planTitle: input.planTitle,
260
- planUrl: planUrl(input.planId),
261
- message: input.comment.message,
262
- isReply: Boolean(input.comment.parentCommentId),
263
- reason: input.recipient.reason,
264
- }),
265
- to: input.recipient.email,
266
- templateId: PLAN_COMMENT_EMAIL_ID,
214
+ const actor = actorName(input.comment);
215
+ const app = appName();
216
+ const isReply = Boolean(input.comment.parentCommentId);
217
+ const subject = isReply
218
+ ? `${actor} replied to a comment on "${input.planTitle}"`
219
+ : `${actor} commented on "${input.planTitle}"`;
220
+ const actionText =
221
+ input.recipient.reason === "mention"
222
+ ? "mentioned you in a comment on"
223
+ : input.recipient.reason === "plan-owner"
224
+ ? "left a comment on your plan"
225
+ : "replied in a comment thread you participated in";
226
+ const { html, text } = renderEmail({
227
+ preheader: `${actor} ${actionText} on ${app}.`,
228
+ heading: isReply ? "New reply on your plan" : "New comment on your plan",
229
+ paragraphs: [
230
+ `${emailStrong(actor)} ${actionText} ${emailStrong(input.planTitle)}.`,
231
+ `Comment: "${commentExcerpt(input.comment.message)}"`,
232
+ ],
233
+ cta: { label: "Open plan", url: planUrl(input.planId) },
234
+ footer:
235
+ input.recipient.reason === "plan-owner"
236
+ ? "You received this because you own this plan."
237
+ : input.recipient.reason === "mention"
238
+ ? "You received this because you were mentioned in this comment."
239
+ : "You received this because you participated in this comment thread.",
267
240
  });
241
+ await sendEmail({ to: input.recipient.email, subject, html, text });
268
242
  }
269
243
 
270
244
  export async function notifyPlanCommentRecipients({
@@ -21,7 +21,6 @@ import { and, eq } from "drizzle-orm";
21
21
  import { getDeckUrl } from "../../actions/_app-url.js";
22
22
  import { SLIDES_USER_PREFS_KEY } from "../../shared/slides-user-prefs.js";
23
23
  import { getDb, schema } from "../db/index.js";
24
- import { SLIDES_DECK_COMMENT_EMAIL_ID } from "./emails.js";
25
24
 
26
25
  /**
27
26
  * `deck-missing` stays distinct from `no-recipients`: one means the deck could
@@ -100,41 +99,6 @@ async function threadParticipants(
100
99
  return rows.map((row) => row.authorEmail);
101
100
  }
102
101
 
103
- export function renderDeckCommentEmail({
104
- actor,
105
- title,
106
- url,
107
- content,
108
- isReply,
109
- }: {
110
- actor: string;
111
- title: string;
112
- url: string;
113
- content: string;
114
- isReply: boolean;
115
- }) {
116
- return {
117
- subject: isReply
118
- ? `${actor} replied to a comment on "${title}"`
119
- : `${actor} commented on "${title}"`,
120
- ...renderEmail({
121
- preheader: isReply
122
- ? `${actor} replied to a comment on ${title}.`
123
- : `${actor} commented on ${title}.`,
124
- heading: isReply ? "New reply on your deck" : "New comment",
125
- paragraphs: [
126
- isReply
127
- ? `${emailStrong(actor)} replied in a comment thread on ${emailStrong(title)}.`
128
- : `${emailStrong(actor)} commented on ${emailStrong(title)}.`,
129
- `"${excerpt(content)}"`,
130
- ],
131
- cta: { label: "Open deck", url },
132
- footer:
133
- "You received this because you own or participated in this thread. Turn these off in Slides settings.",
134
- }),
135
- };
136
- }
137
-
138
102
  export async function notifyDeckComment(input: {
139
103
  deckId: string;
140
104
  slideId: string;
@@ -189,16 +153,29 @@ async function deliverDeckCommentEmails(input: {
189
153
  preferenceKey: SLIDES_USER_PREFS_KEY,
190
154
  logLabel: LOG_LABEL,
191
155
  send: async (to) => {
156
+ const { html, text } = renderEmail({
157
+ preheader: input.isReply
158
+ ? `${actor} replied to a comment on ${deck.title}.`
159
+ : `${actor} commented on ${deck.title}.`,
160
+ heading: input.isReply ? "New reply on your deck" : "New comment",
161
+ paragraphs: [
162
+ input.isReply
163
+ ? `${emailStrong(actor)} replied in a comment thread on ${emailStrong(deck.title)}.`
164
+ : `${emailStrong(actor)} commented on ${emailStrong(deck.title)}.`,
165
+ `"${excerpt(input.content)}"`,
166
+ ],
167
+ cta: { label: "Open deck", url },
168
+ footer:
169
+ "You received this because you own or participated in this thread. Turn these off in Slides settings.",
170
+ });
171
+
192
172
  await sendEmail({
193
- ...renderDeckCommentEmail({
194
- actor,
195
- title: deck.title,
196
- url,
197
- content: input.content,
198
- isReply: input.isReply,
199
- }),
200
173
  to,
201
- templateId: SLIDES_DECK_COMMENT_EMAIL_ID,
174
+ subject: input.isReply
175
+ ? `${actor} replied to a comment on "${deck.title}"`
176
+ : `${actor} commented on "${deck.title}"`,
177
+ html,
178
+ text,
202
179
  });
203
180
  },
204
181
  });
@@ -583,17 +583,16 @@ async function handleSend(params, config, event) {
583
583
  if (!asyncMode)
584
584
  idempotencyKey = undefined;
585
585
  if (asyncMode) {
586
- // Refuse async mode entirely when no auth is configured in production.
587
- // The async dispatch path self-fires the `_process-task` route, which
588
- // accepts unsigned dispatches when A2A_SECRET is unset — that combined
589
- // with the lack of caller identity here would let any unauthenticated
590
- // attacker queue and trigger handler runs. In production, require some
591
- // form of auth so the verifiedEmail is bound to the task.
592
- const hasA2ASecret = hasConfiguredA2ASecret();
593
- const hasApiKey = !!(config.apiKeyEnv && process.env[config.apiKeyEnv]);
594
- if (isA2AProductionRuntime() && !hasA2ASecret && !hasApiKey) {
586
+ // Refuse async mode entirely without the shared secret in production.
587
+ // The async dispatch path self-fires the `_process-task` route, which is
588
+ // authenticated with an internal HMAC signed by A2A_SECRET. A legacy
589
+ // apiKeyEnv can authenticate the JSON-RPC request, but it cannot
590
+ // authenticate that internal handoff. Accepting it here would create a
591
+ // working task that can never start and leaves the caller polling until
592
+ // its timeout.
593
+ if (isA2AProductionRuntime() && !hasConfiguredA2ASecret()) {
595
594
  return {
596
- ...jsonRpcError(0, -32001, "A2A async mode is not available — A2A_SECRET or apiKeyEnv must be configured."),
595
+ ...jsonRpcError(0, -32001, "A2A async mode requires A2A_SECRET for internal processor dispatch; apiKeyEnv only supports synchronous A2A."),
597
596
  _id: 0,
598
597
  };
599
598
  }
@@ -26,8 +26,8 @@ export declare const getCollabState: import("h3").EventHandlerWithFetch<import("
26
26
  * Body: { update: string (base64), requestSource?: string }
27
27
  */
28
28
  export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
29
- error: string;
30
29
  ok?: undefined;
30
+ error: string;
31
31
  } | {
32
32
  error?: undefined;
33
33
  ok: boolean;
package/dist/db/client.js CHANGED
@@ -804,19 +804,19 @@ export function pgPoolOptions(url) {
804
804
  export function neonPoolMax() {
805
805
  if (!isServerlessRuntime())
806
806
  return 20;
807
- // The durable background-function worker is a SINGLE process per run (unlike
808
- // the many warm request-instances the foreground serverless has), so it can
809
- // safely hold a larger pool without risking Neon's connection cap. The agent's
807
+ // Netlify can run several background workers concurrently; a background
808
+ // worker is not a global singleton and must not hold a larger pool than the
809
+ // foreground path. The agent's
810
810
  // pre-send setup fires ~6 concurrent DB reads in parallel; with only 2
811
811
  // connections that burst exhausts the pool and a single stalled connection
812
812
  // freezes the worker before it can claim — observed on analytics' heavier
813
813
  // action surface, where the worker froze right after `model_done` and never
814
- // recorded `env_config`/`presend`, while the foreground (10-connection pool)
815
- // ran the identical code in ~2s. Give the bg worker enough connections for the
816
- // burst; keep the foreground serverless pool tiny to avoid "Max client
817
- // connections reached" across many warm instances.
814
+ // recorded `env_config`/`presend`, while the foreground path ran the
815
+ // identical code in ~2s. Four connections preserve the concurrent
816
+ // read burst while keeping foreground and background caps aligned, avoiding
817
+ // "Max client connections reached" across many warm instances.
818
818
  if (isBackgroundFunctionPoolContext())
819
- return 8;
819
+ return 4;
820
820
  return 4;
821
821
  }
822
822
  /**