@agent-native/core 0.92.2 → 0.92.3

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 (170) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +26 -0
  3. package/corpus/core/docs/content/locales/ar-SA/server.mdx +35 -0
  4. package/corpus/core/docs/content/locales/de-DE/server.mdx +35 -0
  5. package/corpus/core/docs/content/locales/es-ES/server.mdx +35 -0
  6. package/corpus/core/docs/content/locales/fr-FR/server.mdx +35 -0
  7. package/corpus/core/docs/content/locales/hi-IN/server.mdx +35 -0
  8. package/corpus/core/docs/content/locales/ja-JP/server.mdx +35 -0
  9. package/corpus/core/docs/content/locales/ko-KR/server.mdx +35 -0
  10. package/corpus/core/docs/content/locales/pt-BR/server.mdx +35 -0
  11. package/corpus/core/docs/content/locales/zh-CN/server.mdx +35 -0
  12. package/corpus/core/docs/content/locales/zh-TW/server.mdx +35 -0
  13. package/corpus/core/docs/content/server.mdx +35 -0
  14. package/corpus/core/package.json +1 -1
  15. package/corpus/core/src/agent/engine/ai-sdk-engine.ts +14 -4
  16. package/corpus/core/src/agent/engine/anthropic-engine.ts +15 -3
  17. package/corpus/core/src/agent/engine/builder-engine.ts +30 -4
  18. package/corpus/core/src/agent/engine/registry.ts +63 -2
  19. package/corpus/core/src/agent/model-config.ts +0 -2
  20. package/corpus/core/src/agent/production-agent.ts +12 -1
  21. package/corpus/core/src/agent/run-manager.ts +57 -36
  22. package/corpus/core/src/agent/run-store.ts +81 -2
  23. package/corpus/core/src/agent/thread-data-builder.ts +33 -2
  24. package/corpus/core/src/client/AssistantChat.tsx +314 -96
  25. package/corpus/core/src/client/agent-chat-adapter.ts +17 -1
  26. package/corpus/core/src/client/chat/repo-helpers.ts +45 -0
  27. package/corpus/core/src/client/chat/tool-call-display.tsx +36 -27
  28. package/corpus/core/src/client/chat-model-groups.ts +1 -5
  29. package/corpus/core/src/client/composer/TiptapComposer.tsx +0 -2
  30. package/corpus/core/src/client/embed-auth.ts +20 -0
  31. package/corpus/core/src/client/settings/SettingsSection.tsx +66 -5
  32. package/corpus/core/src/client/sse-event-processor.ts +49 -17
  33. package/corpus/core/src/client/use-db-sync.ts +31 -9
  34. package/corpus/core/src/collab/agent-presence.ts +6 -1
  35. package/corpus/core/src/collab/awareness-store.ts +185 -0
  36. package/corpus/core/src/collab/awareness.ts +58 -2
  37. package/corpus/core/src/scripts/agent-engines/manage-agent-engine.ts +7 -8
  38. package/corpus/core/src/scripts/agent-engines/set-agent-engine.ts +7 -18
  39. package/corpus/core/src/server/core-routes-plugin.ts +21 -2
  40. package/corpus/core/src/server/embed-session.ts +20 -4
  41. package/corpus/core/src/styles/agent-native.css +51 -0
  42. package/corpus/core/src/tracking/providers.ts +33 -2
  43. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +8 -1
  44. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +9 -1
  45. package/corpus/templates/analytics/app/pages/monitoring/uptime/MonitorDetail.tsx +41 -11
  46. package/corpus/templates/analytics/app/pages/monitoring/uptime/types.ts +44 -0
  47. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-action-menus-now-close-cleanly-before-opening-hist.md +6 -0
  48. package/corpus/templates/analytics/changelog/2026-07-09-dashboard-email-reports-still-send-when-the-screenshot-captu.md +6 -0
  49. package/corpus/templates/analytics/changelog/2026-07-09-uptime-alerts-now-suppress-recovery-noise-during-flapping-an.md +6 -0
  50. package/corpus/templates/analytics/changelog/2026-07-09-uptime-check-history-now-shows-timing-diagnostics-so-slow-or.md +6 -0
  51. package/corpus/templates/analytics/changelog/2026-07-09-uptime-recovery-alerts-now-send-when-the-original-outage-not.md +6 -0
  52. package/corpus/templates/analytics/docs/uptime-monitoring.md +29 -16
  53. package/corpus/templates/analytics/netlify.toml +3 -0
  54. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +126 -0
  55. package/corpus/templates/analytics/server/db/schema-monitoring.ts +5 -0
  56. package/corpus/templates/analytics/server/jobs/uptime-monitors.ts +5 -1
  57. package/corpus/templates/analytics/server/lib/uptime-monitors.ts +305 -31
  58. package/corpus/templates/analytics/server/plugins/db.ts +15 -0
  59. package/corpus/templates/analytics/server/plugins/uptime-monitor-jobs.ts +1 -1
  60. package/corpus/templates/analytics/server/routes/api/uptime-monitors/run.post.ts +57 -0
  61. package/corpus/templates/clips/actions/list-meetings.ts +12 -0
  62. package/corpus/templates/clips/changelog/2026-07-09-meeting-reminders-now-ignore-obvious-solo-personal-calendar-.md +6 -0
  63. package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +1 -0
  64. package/corpus/templates/clips/server/lib/calendar-event-classification.ts +79 -0
  65. package/dist/agent/engine/ai-sdk-engine.d.ts.map +1 -1
  66. package/dist/agent/engine/ai-sdk-engine.js +13 -4
  67. package/dist/agent/engine/ai-sdk-engine.js.map +1 -1
  68. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  69. package/dist/agent/engine/anthropic-engine.js +14 -3
  70. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  71. package/dist/agent/engine/builder-engine.d.ts +1 -1
  72. package/dist/agent/engine/builder-engine.d.ts.map +1 -1
  73. package/dist/agent/engine/builder-engine.js +26 -4
  74. package/dist/agent/engine/builder-engine.js.map +1 -1
  75. package/dist/agent/engine/registry.d.ts.map +1 -1
  76. package/dist/agent/engine/registry.js +47 -2
  77. package/dist/agent/engine/registry.js.map +1 -1
  78. package/dist/agent/model-config.d.ts +2 -2
  79. package/dist/agent/model-config.d.ts.map +1 -1
  80. package/dist/agent/model-config.js +0 -2
  81. package/dist/agent/model-config.js.map +1 -1
  82. package/dist/agent/production-agent.d.ts.map +1 -1
  83. package/dist/agent/production-agent.js +12 -1
  84. package/dist/agent/production-agent.js.map +1 -1
  85. package/dist/agent/run-manager.d.ts.map +1 -1
  86. package/dist/agent/run-manager.js +52 -33
  87. package/dist/agent/run-manager.js.map +1 -1
  88. package/dist/agent/run-store.d.ts +24 -0
  89. package/dist/agent/run-store.d.ts.map +1 -1
  90. package/dist/agent/run-store.js +64 -2
  91. package/dist/agent/run-store.js.map +1 -1
  92. package/dist/agent/thread-data-builder.d.ts.map +1 -1
  93. package/dist/agent/thread-data-builder.js +28 -2
  94. package/dist/agent/thread-data-builder.js.map +1 -1
  95. package/dist/client/AssistantChat.d.ts +6 -0
  96. package/dist/client/AssistantChat.d.ts.map +1 -1
  97. package/dist/client/AssistantChat.js +243 -36
  98. package/dist/client/AssistantChat.js.map +1 -1
  99. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  100. package/dist/client/agent-chat-adapter.js +15 -1
  101. package/dist/client/agent-chat-adapter.js.map +1 -1
  102. package/dist/client/chat/repo-helpers.d.ts.map +1 -1
  103. package/dist/client/chat/repo-helpers.js +43 -0
  104. package/dist/client/chat/repo-helpers.js.map +1 -1
  105. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  106. package/dist/client/chat/tool-call-display.js +4 -4
  107. package/dist/client/chat/tool-call-display.js.map +1 -1
  108. package/dist/client/chat-model-groups.d.ts.map +1 -1
  109. package/dist/client/chat-model-groups.js +1 -3
  110. package/dist/client/chat-model-groups.js.map +1 -1
  111. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  112. package/dist/client/composer/TiptapComposer.js +0 -2
  113. package/dist/client/composer/TiptapComposer.js.map +1 -1
  114. package/dist/client/embed-auth.d.ts.map +1 -1
  115. package/dist/client/embed-auth.js +14 -0
  116. package/dist/client/embed-auth.js.map +1 -1
  117. package/dist/client/settings/SettingsSection.d.ts.map +1 -1
  118. package/dist/client/settings/SettingsSection.js +32 -3
  119. package/dist/client/settings/SettingsSection.js.map +1 -1
  120. package/dist/client/sse-event-processor.d.ts.map +1 -1
  121. package/dist/client/sse-event-processor.js +44 -17
  122. package/dist/client/sse-event-processor.js.map +1 -1
  123. package/dist/client/use-db-sync.d.ts.map +1 -1
  124. package/dist/client/use-db-sync.js +29 -9
  125. package/dist/client/use-db-sync.js.map +1 -1
  126. package/dist/collab/agent-presence.d.ts.map +1 -1
  127. package/dist/collab/agent-presence.js +5 -2
  128. package/dist/collab/agent-presence.js.map +1 -1
  129. package/dist/collab/awareness-store.d.ts +1 -1
  130. package/dist/collab/awareness-store.d.ts.map +1 -1
  131. package/dist/collab/awareness-store.js +0 -0
  132. package/dist/collab/awareness-store.js.map +1 -1
  133. package/dist/collab/awareness.d.ts +2 -0
  134. package/dist/collab/awareness.d.ts.map +1 -1
  135. package/dist/collab/awareness.js +46 -2
  136. package/dist/collab/awareness.js.map +1 -1
  137. package/dist/collab/routes.d.ts +1 -1
  138. package/dist/notifications/routes.d.ts +1 -1
  139. package/dist/observability/routes.d.ts +5 -5
  140. package/dist/progress/routes.d.ts +1 -1
  141. package/dist/scripts/agent-engines/manage-agent-engine.d.ts.map +1 -1
  142. package/dist/scripts/agent-engines/manage-agent-engine.js +6 -6
  143. package/dist/scripts/agent-engines/manage-agent-engine.js.map +1 -1
  144. package/dist/scripts/agent-engines/set-agent-engine.d.ts.map +1 -1
  145. package/dist/scripts/agent-engines/set-agent-engine.js +5 -13
  146. package/dist/scripts/agent-engines/set-agent-engine.js.map +1 -1
  147. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  148. package/dist/server/core-routes-plugin.d.ts +5 -0
  149. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  150. package/dist/server/core-routes-plugin.js +10 -3
  151. package/dist/server/core-routes-plugin.js.map +1 -1
  152. package/dist/server/embed-session.d.ts.map +1 -1
  153. package/dist/server/embed-session.js +17 -4
  154. package/dist/server/embed-session.js.map +1 -1
  155. package/dist/styles/agent-native.css +51 -0
  156. package/dist/tracking/providers.d.ts.map +1 -1
  157. package/dist/tracking/providers.js +23 -5
  158. package/dist/tracking/providers.js.map +1 -1
  159. package/docs/content/locales/ar-SA/server.mdx +35 -0
  160. package/docs/content/locales/de-DE/server.mdx +35 -0
  161. package/docs/content/locales/es-ES/server.mdx +35 -0
  162. package/docs/content/locales/fr-FR/server.mdx +35 -0
  163. package/docs/content/locales/hi-IN/server.mdx +35 -0
  164. package/docs/content/locales/ja-JP/server.mdx +35 -0
  165. package/docs/content/locales/ko-KR/server.mdx +35 -0
  166. package/docs/content/locales/pt-BR/server.mdx +35 -0
  167. package/docs/content/locales/zh-CN/server.mdx +35 -0
  168. package/docs/content/locales/zh-TW/server.mdx +35 -0
  169. package/docs/content/server.mdx +35 -0
  170. package/package.json +1 -1
@@ -44,6 +44,10 @@ import {
44
44
 
45
45
  import { getDb, schema } from "../db/index.js";
46
46
 
47
+ declare global {
48
+ var __AGENT_NATIVE_UPTIME_MONITOR_SCHEDULED_RUNTIME__: boolean | undefined;
49
+ }
50
+
47
51
  // ---------------------------------------------------------------------------
48
52
  // Types
49
53
  // ---------------------------------------------------------------------------
@@ -158,6 +162,52 @@ export interface MonitorCheckResult {
158
162
  latencyMs: number | null;
159
163
  error: string | null;
160
164
  failedAssertions: string[];
165
+ diagnostics: MonitorCheckDiagnostics;
166
+ }
167
+
168
+ export type MonitorCheckSource =
169
+ | "netlify-scheduled"
170
+ | "netlify-runtime"
171
+ | "in-process"
172
+ | "manual"
173
+ | "unknown";
174
+
175
+ export interface MonitorCheckDiagnostics {
176
+ source: MonitorCheckSource;
177
+ runtime: {
178
+ nodeEnv?: string;
179
+ netlify?: boolean;
180
+ deployId?: string;
181
+ deployContext?: string;
182
+ commitRef?: string;
183
+ functionName?: string;
184
+ region?: string;
185
+ };
186
+ request: {
187
+ method: MonitorMethod;
188
+ timeoutMs: number;
189
+ followRedirects: boolean;
190
+ assertionTypes: AssertionType[];
191
+ bodyReadRequired: boolean;
192
+ allowPrivateHosts: boolean;
193
+ };
194
+ timings: {
195
+ totalMs?: number;
196
+ ssrfSetupMs?: number;
197
+ requestMs?: number;
198
+ bodyReadMs?: number;
199
+ };
200
+ response?: {
201
+ finalUrl?: string;
202
+ finalHost?: string;
203
+ statusCode?: number;
204
+ headers?: Record<string, string>;
205
+ };
206
+ error?: {
207
+ kind: "config" | "timeout" | "network" | "body-timeout";
208
+ name?: string;
209
+ message: string;
210
+ };
161
211
  }
162
212
 
163
213
  export interface MonitorIncident {
@@ -170,6 +220,7 @@ export interface MonitorIncident {
170
220
  cause: string;
171
221
  lastError: string | null;
172
222
  notificationId: string | null;
223
+ notificationDelivered: boolean;
173
224
  checksFailed: number;
174
225
  createdAt: string;
175
226
  }
@@ -183,6 +234,7 @@ export interface CheckOutcome {
183
234
  latencyMs: number | null;
184
235
  error: string | null;
185
236
  failedAssertions: string[];
237
+ diagnostics: MonitorCheckDiagnostics;
186
238
  }
187
239
 
188
240
  export interface AssertionFailure {
@@ -212,6 +264,7 @@ const MAX_REQUEST_BODY_BYTES = 16 * 1024;
212
264
  const MAX_ASSERTIONS_PER_MONITOR = 20;
213
265
  const MAX_ASSERTION_VALUE_BYTES = 2048;
214
266
  const MAX_ASSERTION_HEADER_LENGTH = 128;
267
+ const MAX_MONITOR_DIAGNOSTICS_BYTES = 4096;
215
268
 
216
269
  const MIN_INTERVAL_SECONDS = 30;
217
270
  const MAX_INTERVAL_SECONDS = 24 * 60 * 60;
@@ -235,6 +288,152 @@ function nowIso(): string {
235
288
  return new Date().toISOString();
236
289
  }
237
290
 
291
+ function detectMonitorCheckSource(): MonitorCheckSource {
292
+ if (globalThis.__AGENT_NATIVE_UPTIME_MONITOR_SCHEDULED_RUNTIME__ === true) {
293
+ return "netlify-scheduled";
294
+ }
295
+ if (process.env.NETLIFY === "true") return "netlify-runtime";
296
+ if (process.env.NODE_ENV === "production") return "in-process";
297
+ return "unknown";
298
+ }
299
+
300
+ function monitorCheckRuntimeDiagnostics(): MonitorCheckDiagnostics["runtime"] {
301
+ return {
302
+ nodeEnv: process.env.NODE_ENV,
303
+ netlify: process.env.NETLIFY === "true" || undefined,
304
+ deployId: process.env.DEPLOY_ID,
305
+ deployContext: process.env.CONTEXT,
306
+ commitRef: process.env.COMMIT_REF,
307
+ functionName:
308
+ process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.NETLIFY_FUNCTION_NAME,
309
+ region: process.env.AWS_REGION,
310
+ };
311
+ }
312
+
313
+ function safeResponseHeaders(
314
+ headers: Record<string, string>,
315
+ ): Record<string, string> | undefined {
316
+ const picked: Record<string, string> = {};
317
+ for (const key of [
318
+ "server",
319
+ "x-nf-request-id",
320
+ "cache-status",
321
+ "cdn-cache-control",
322
+ "content-type",
323
+ ]) {
324
+ const value = headers[key];
325
+ if (value) picked[key] = value.slice(0, 300);
326
+ }
327
+ return Object.keys(picked).length ? picked : undefined;
328
+ }
329
+
330
+ function finalUrlDiagnostics(url: string | undefined): {
331
+ finalUrl?: string;
332
+ finalHost?: string;
333
+ } {
334
+ if (!url) return {};
335
+ try {
336
+ const parsed = new URL(url);
337
+ return {
338
+ finalHost: parsed.host,
339
+ };
340
+ } catch {
341
+ return {};
342
+ }
343
+ }
344
+
345
+ function defaultMonitorCheckDiagnostics(): MonitorCheckDiagnostics {
346
+ return {
347
+ source: "unknown",
348
+ runtime: {},
349
+ request: {
350
+ method: "GET",
351
+ timeoutMs: DEFAULT_MONITOR_TIMEOUT_MS,
352
+ followRedirects: true,
353
+ assertionTypes: [],
354
+ bodyReadRequired: false,
355
+ allowPrivateHosts: false,
356
+ },
357
+ timings: {},
358
+ };
359
+ }
360
+
361
+ function normalizeMonitorCheckDiagnostics(
362
+ raw: unknown,
363
+ ): MonitorCheckDiagnostics {
364
+ const fallback = defaultMonitorCheckDiagnostics();
365
+ if (!raw || typeof raw !== "object") return fallback;
366
+ const parsed = raw as Partial<MonitorCheckDiagnostics>;
367
+ return {
368
+ ...fallback,
369
+ ...parsed,
370
+ runtime:
371
+ parsed.runtime && typeof parsed.runtime === "object"
372
+ ? parsed.runtime
373
+ : fallback.runtime,
374
+ request:
375
+ parsed.request && typeof parsed.request === "object"
376
+ ? { ...fallback.request, ...parsed.request }
377
+ : fallback.request,
378
+ timings:
379
+ parsed.timings && typeof parsed.timings === "object"
380
+ ? parsed.timings
381
+ : fallback.timings,
382
+ response:
383
+ parsed.response && typeof parsed.response === "object"
384
+ ? {
385
+ finalHost: parsed.response.finalHost,
386
+ statusCode: parsed.response.statusCode,
387
+ headers: parsed.response.headers,
388
+ }
389
+ : undefined,
390
+ error:
391
+ parsed.error && typeof parsed.error === "object"
392
+ ? parsed.error
393
+ : undefined,
394
+ };
395
+ }
396
+
397
+ function compactDiagnostics(
398
+ diagnostics: MonitorCheckDiagnostics,
399
+ ): MonitorCheckDiagnostics {
400
+ return {
401
+ ...diagnostics,
402
+ runtime: Object.fromEntries(
403
+ Object.entries(diagnostics.runtime).filter(([, value]) => value != null),
404
+ ) as MonitorCheckDiagnostics["runtime"],
405
+ response: diagnostics.response
406
+ ? {
407
+ ...diagnostics.response,
408
+ headers: diagnostics.response.headers,
409
+ }
410
+ : undefined,
411
+ error: diagnostics.error
412
+ ? {
413
+ ...diagnostics.error,
414
+ message: diagnostics.error.message.slice(0, 500),
415
+ }
416
+ : undefined,
417
+ };
418
+ }
419
+
420
+ function serializeMonitorDiagnostics(
421
+ diagnostics: MonitorCheckDiagnostics,
422
+ ): string {
423
+ const compact = compactDiagnostics(diagnostics);
424
+ const serialized = JSON.stringify(compact);
425
+ if (byteLength(serialized) <= MAX_MONITOR_DIAGNOSTICS_BYTES) {
426
+ return serialized;
427
+ }
428
+ return JSON.stringify({
429
+ ...compact,
430
+ response: compact.response
431
+ ? { ...compact.response, headers: undefined }
432
+ : undefined,
433
+ truncated: true,
434
+ });
435
+ }
436
+
238
437
  function safeJsonParse<T>(raw: unknown, fallback: T): T {
239
438
  if (typeof raw !== "string" || !raw.trim()) return fallback;
240
439
  try {
@@ -893,17 +1092,48 @@ export async function runMonitorCheck(
893
1092
  | "assertions"
894
1093
  | "followRedirects"
895
1094
  >,
896
- opts: { allowPrivateHosts?: boolean } = {},
1095
+ opts: { allowPrivateHosts?: boolean; source?: MonitorCheckSource } = {},
897
1096
  ): Promise<CheckOutcome> {
898
1097
  const checkedAt = nowIso();
899
1098
  const matcher = monitor.expectedStatus;
900
1099
  const assertions = monitor.assertions;
901
1100
  const allowPrivateHosts =
902
1101
  opts.allowPrivateHosts ?? monitorAllowPrivateHosts();
1102
+ const timeoutMs = clampInt(
1103
+ monitor.timeoutMs,
1104
+ MIN_TIMEOUT_MS,
1105
+ MAX_TIMEOUT_MS,
1106
+ DEFAULT_MONITOR_TIMEOUT_MS,
1107
+ );
1108
+ const method = normalizeMethod(monitor.method);
1109
+ const diagnostics: MonitorCheckDiagnostics = {
1110
+ source: opts.source ?? detectMonitorCheckSource(),
1111
+ runtime: monitorCheckRuntimeDiagnostics(),
1112
+ request: {
1113
+ method,
1114
+ timeoutMs,
1115
+ followRedirects: monitor.followRedirects,
1116
+ assertionTypes: assertions.map((assertion) => assertion.type),
1117
+ bodyReadRequired: method !== "HEAD" && needsResponseBody(assertions),
1118
+ allowPrivateHosts,
1119
+ },
1120
+ timings: {},
1121
+ };
1122
+ const totalStart = Date.now();
1123
+ const finish = <T extends Omit<CheckOutcome, "diagnostics">>(
1124
+ outcome: T,
1125
+ ): T & { diagnostics: MonitorCheckDiagnostics } => {
1126
+ diagnostics.timings.totalMs = Date.now() - totalStart;
1127
+ return { ...outcome, diagnostics };
1128
+ };
903
1129
 
904
1130
  // Fast, deterministic pre-flight guard (scheme + literal private hosts).
905
1131
  if (!allowPrivateHosts && isBlockedExtensionUrl(monitor.url)) {
906
- return {
1132
+ diagnostics.error = {
1133
+ kind: "config",
1134
+ message: "SSRF blocked: private/internal or non-http(s) address",
1135
+ };
1136
+ return finish({
907
1137
  checkedAt,
908
1138
  statusCode: null,
909
1139
  latencyMs: null,
@@ -918,26 +1148,27 @@ export async function runMonitorCheck(
918
1148
  errorKind: "config",
919
1149
  }),
920
1150
  error: "SSRF blocked: private/internal or non-http(s) address",
921
- };
1151
+ });
922
1152
  }
923
1153
 
924
- const timeoutMs = clampInt(
925
- monitor.timeoutMs,
926
- MIN_TIMEOUT_MS,
927
- MAX_TIMEOUT_MS,
928
- DEFAULT_MONITOR_TIMEOUT_MS,
929
- );
930
-
931
1154
  let dispatcher: unknown | undefined;
932
1155
  try {
1156
+ const ssrfStart = Date.now();
933
1157
  ({ dispatcher } = await prepareMonitorFetch(monitor.url, {
934
1158
  allowPrivateHosts,
935
1159
  }));
1160
+ diagnostics.timings.ssrfSetupMs = Date.now() - ssrfStart;
936
1161
  } catch (err) {
937
1162
  const message =
938
1163
  err instanceof Error ? err.message : String(err ?? "check failed");
939
1164
  const isConfig = message.startsWith("SSRF blocked");
940
1165
  const errorText = message.slice(0, 500);
1166
+ diagnostics.timings.ssrfSetupMs = Date.now() - totalStart;
1167
+ diagnostics.error = {
1168
+ kind: isConfig ? "config" : "network",
1169
+ name: err instanceof Error ? err.name : undefined,
1170
+ message: errorText,
1171
+ };
941
1172
  const outcome = evaluateCheck({
942
1173
  statusCode: null,
943
1174
  latencyMs: null,
@@ -948,7 +1179,7 @@ export async function runMonitorCheck(
948
1179
  fetchError: errorText,
949
1180
  errorKind: isConfig ? "config" : "network",
950
1181
  });
951
- return {
1182
+ return finish({
952
1183
  checkedAt,
953
1184
  statusCode: null,
954
1185
  latencyMs: null,
@@ -956,7 +1187,7 @@ export async function runMonitorCheck(
956
1187
  ok: outcome.ok,
957
1188
  failedAssertions: outcome.failedAssertions,
958
1189
  error: errorText,
959
- };
1190
+ });
960
1191
  }
961
1192
 
962
1193
  const controller = new AbortController();
@@ -965,10 +1196,9 @@ export async function runMonitorCheck(
965
1196
  timedOut = true;
966
1197
  controller.abort();
967
1198
  }, timeoutMs);
968
- const start = Date.now();
1199
+ const requestStart = Date.now();
969
1200
 
970
1201
  try {
971
- const method = normalizeMethod(monitor.method);
972
1202
  const hasBody =
973
1203
  method !== "GET" &&
974
1204
  method !== "HEAD" &&
@@ -988,25 +1218,37 @@ export async function runMonitorCheck(
988
1218
  dispatcher,
989
1219
  initialDnsChecked: Boolean(dispatcher),
990
1220
  });
1221
+ diagnostics.timings.requestMs = Date.now() - requestStart;
991
1222
 
992
1223
  // Stop the abort timer as soon as headers arrive so a slow/optional body
993
1224
  // read cannot be mislabeled as a request timeout with a null status code.
994
1225
  clearTimeout(timer);
995
1226
 
996
- const latencyMs = Date.now() - start;
1227
+ const latencyMs = diagnostics.timings.requestMs;
997
1228
  const statusCode = response.status;
998
1229
  const headers = headersToObject(response.headers);
1230
+ diagnostics.response = {
1231
+ ...finalUrlDiagnostics(response.url),
1232
+ statusCode,
1233
+ headers: safeResponseHeaders(headers),
1234
+ };
999
1235
  let bodyText = "";
1000
1236
  let bodyReadError: string | null = null;
1001
1237
  if (method !== "HEAD" && needsResponseBody(assertions)) {
1238
+ const bodyReadStart = Date.now();
1002
1239
  const body = await readCappedText(
1003
1240
  response,
1004
1241
  MAX_RESPONSE_BODY_BYTES,
1005
1242
  timeoutMs,
1006
1243
  );
1244
+ diagnostics.timings.bodyReadMs = Date.now() - bodyReadStart;
1007
1245
  bodyText = body.text;
1008
1246
  if (body.timedOut) {
1009
1247
  bodyReadError = `Response body read timed out after ${timeoutMs}ms`;
1248
+ diagnostics.error = {
1249
+ kind: "body-timeout",
1250
+ message: bodyReadError,
1251
+ };
1010
1252
  }
1011
1253
  } else {
1012
1254
  await cancelResponseBody(response);
@@ -1025,7 +1267,7 @@ export async function runMonitorCheck(
1025
1267
  : outcome.failedAssertions;
1026
1268
  const status = bodyReadError ? "down" : outcome.status;
1027
1269
 
1028
- return {
1270
+ return finish({
1029
1271
  checkedAt,
1030
1272
  statusCode,
1031
1273
  latencyMs,
@@ -1033,7 +1275,7 @@ export async function runMonitorCheck(
1033
1275
  ok: status === "up" && outcome.ok,
1034
1276
  failedAssertions,
1035
1277
  error: failedAssertions.length ? failedAssertions.join("; ") : null,
1036
- };
1278
+ });
1037
1279
  } catch (err) {
1038
1280
  const message =
1039
1281
  err instanceof Error ? err.message : String(err ?? "check failed");
@@ -1044,6 +1286,12 @@ export async function runMonitorCheck(
1044
1286
  const errorText = isTimeout
1045
1287
  ? `Timed out after ${timeoutMs}ms`
1046
1288
  : message.slice(0, 500);
1289
+ diagnostics.timings.requestMs = Date.now() - requestStart;
1290
+ diagnostics.error = {
1291
+ kind: isConfig ? "config" : isTimeout ? "timeout" : "network",
1292
+ name: err instanceof Error ? err.name : undefined,
1293
+ message: errorText,
1294
+ };
1047
1295
  const outcome = evaluateCheck({
1048
1296
  statusCode: null,
1049
1297
  latencyMs: isTimeout ? timeoutMs : null,
@@ -1054,7 +1302,7 @@ export async function runMonitorCheck(
1054
1302
  fetchError: errorText,
1055
1303
  errorKind: isConfig ? "config" : "network",
1056
1304
  });
1057
- return {
1305
+ return finish({
1058
1306
  checkedAt,
1059
1307
  statusCode: null,
1060
1308
  latencyMs: isTimeout ? timeoutMs : null,
@@ -1062,7 +1310,7 @@ export async function runMonitorCheck(
1062
1310
  ok: outcome.ok,
1063
1311
  failedAssertions: outcome.failedAssertions,
1064
1312
  error: errorText,
1065
- };
1313
+ });
1066
1314
  } finally {
1067
1315
  clearTimeout(timer);
1068
1316
  }
@@ -1122,6 +1370,9 @@ function rowToResult(row: any): MonitorCheckResult {
1122
1370
  latencyMs: row.latencyMs ?? null,
1123
1371
  error: row.error ?? null,
1124
1372
  failedAssertions: safeJsonParse<string[]>(row.failedAssertions, []),
1373
+ diagnostics: normalizeMonitorCheckDiagnostics(
1374
+ safeJsonParse<unknown>(row.diagnostics, {}),
1375
+ ),
1125
1376
  };
1126
1377
  }
1127
1378
 
@@ -1136,6 +1387,10 @@ function rowToIncident(row: any): MonitorIncident {
1136
1387
  cause: row.cause ?? "",
1137
1388
  lastError: row.lastError ?? null,
1138
1389
  notificationId: row.notificationId ?? null,
1390
+ notificationDelivered:
1391
+ row.notificationDelivered === true ||
1392
+ row.notificationDelivered === 1 ||
1393
+ Boolean(row.notificationId),
1139
1394
  checksFailed: Number(row.checksFailed ?? 1),
1140
1395
  createdAt: row.createdAt,
1141
1396
  };
@@ -1548,6 +1803,7 @@ export async function recordMonitorResult(
1548
1803
  latencyMs: outcome.latencyMs,
1549
1804
  error: outcome.error,
1550
1805
  failedAssertions: JSON.stringify(outcome.failedAssertions),
1806
+ diagnostics: serializeMonitorDiagnostics(outcome.diagnostics),
1551
1807
  createdAt: outcome.checkedAt,
1552
1808
  ownerEmail: monitor.ownerEmail,
1553
1809
  orgId: monitor.orgId,
@@ -1600,12 +1856,24 @@ function isTransientNoResponseFailure(outcome: CheckOutcome): boolean {
1600
1856
  );
1601
1857
  }
1602
1858
 
1859
+ function isTransientDegradedFailure(outcome: CheckOutcome): boolean {
1860
+ return (
1861
+ outcome.status === "degraded" &&
1862
+ outcome.failedAssertions.some((assertion) =>
1863
+ assertion.startsWith("Response took "),
1864
+ )
1865
+ );
1866
+ }
1867
+
1603
1868
  export function shouldOpenMonitorIncident(
1604
1869
  outcome: CheckOutcome,
1605
1870
  priorConsecutiveFailures: number,
1606
1871
  confirmationChecks = transientFailureConfirmationChecks(),
1607
1872
  ): boolean {
1608
- const needed = isTransientNoResponseFailure(outcome)
1873
+ const needsConfirmation =
1874
+ isTransientNoResponseFailure(outcome) ||
1875
+ isTransientDegradedFailure(outcome);
1876
+ const needed = needsConfirmation
1609
1877
  ? Math.max(1, Math.min(10, Math.floor(confirmationChecks)))
1610
1878
  : 1;
1611
1879
  return Math.max(0, Math.floor(priorConsecutiveFailures)) + 1 >= needed;
@@ -1799,10 +2067,13 @@ export async function evaluateAndNotifyMonitor(
1799
2067
 
1800
2068
  const suppressed = await recentlyResolvedWithinCooldown(monitor, ctx, now);
1801
2069
  let notificationId: string | undefined;
2070
+ let notificationDelivered = false;
1802
2071
  if (!suppressed) {
1803
2072
  try {
1804
2073
  const delivery = await notifyMonitorDown(monitor, outcome);
1805
2074
  notificationId = delivery.notification?.id;
2075
+ notificationDelivered =
2076
+ Boolean(notificationId) || delivery.deliveredChannels.length > 0;
1806
2077
  } catch (err) {
1807
2078
  console.error(
1808
2079
  `[uptime-monitors] notify failed for ${monitor.id}:`,
@@ -1825,6 +2096,7 @@ export async function evaluateAndNotifyMonitor(
1825
2096
  cause,
1826
2097
  lastError: outcome.error,
1827
2098
  notificationId: notificationId ?? null,
2099
+ notificationDelivered,
1828
2100
  checksFailed: nextChecksFailed,
1829
2101
  createdAt: outcome.checkedAt,
1830
2102
  ownerEmail: monitor.ownerEmail,
@@ -1833,7 +2105,7 @@ export async function evaluateAndNotifyMonitor(
1833
2105
  return {
1834
2106
  status: outcome.status,
1835
2107
  incidentId,
1836
- notified: Boolean(notificationId),
2108
+ notified: notificationDelivered,
1837
2109
  };
1838
2110
  }
1839
2111
 
@@ -1844,14 +2116,16 @@ export async function evaluateAndNotifyMonitor(
1844
2116
  .set({ resolvedAt: outcome.checkedAt })
1845
2117
  .where(eq(schema.monitorIncidents.id, open.id));
1846
2118
  let notified = false;
1847
- try {
1848
- await notifyMonitorRecovered(monitor, outcome, open);
1849
- notified = true;
1850
- } catch (err) {
1851
- console.error(
1852
- `[uptime-monitors] recovery notify failed for ${monitor.id}:`,
1853
- err,
1854
- );
2119
+ if (open.notificationDelivered) {
2120
+ try {
2121
+ await notifyMonitorRecovered(monitor, outcome, open);
2122
+ notified = true;
2123
+ } catch (err) {
2124
+ console.error(
2125
+ `[uptime-monitors] recovery notify failed for ${monitor.id}:`,
2126
+ err,
2127
+ );
2128
+ }
1855
2129
  }
1856
2130
  return { status: "up", incidentId: open.id, notified, recovered: true };
1857
2131
  }
@@ -1866,7 +2140,7 @@ export async function evaluateAndNotifyMonitor(
1866
2140
  export async function runAndProcessMonitor(
1867
2141
  monitor: Monitor,
1868
2142
  ctx: AccessCtx,
1869
- opts: { allowPrivateHosts?: boolean } = {},
2143
+ opts: { allowPrivateHosts?: boolean; source?: MonitorCheckSource } = {},
1870
2144
  ): Promise<CheckOutcome> {
1871
2145
  const outcome = await runMonitorCheck(monitor, opts);
1872
2146
  // recordMonitorResult() emits the "monitors" change for the UI.
@@ -1897,7 +2171,7 @@ export async function runMonitorNow(
1897
2171
  statusCode: 409,
1898
2172
  });
1899
2173
  }
1900
- return runAndProcessMonitor(monitor, ctx);
2174
+ return runAndProcessMonitor(monitor, ctx, { source: "manual" });
1901
2175
  }
1902
2176
 
1903
2177
  // ---------------------------------------------------------------------------
@@ -999,6 +999,7 @@ const runAnalyticsMigrations = runMigrations(
999
999
  cause TEXT NOT NULL DEFAULT '',
1000
1000
  last_error TEXT,
1001
1001
  notification_id TEXT,
1002
+ notification_delivered BOOLEAN NOT NULL DEFAULT false,
1002
1003
  checks_failed INTEGER NOT NULL DEFAULT 1,
1003
1004
  created_at TEXT NOT NULL DEFAULT (now()::text),
1004
1005
  owner_email TEXT NOT NULL DEFAULT 'local@localhost',
@@ -1015,6 +1016,7 @@ const runAnalyticsMigrations = runMigrations(
1015
1016
  cause TEXT NOT NULL DEFAULT '',
1016
1017
  last_error TEXT,
1017
1018
  notification_id TEXT,
1019
+ notification_delivered INTEGER NOT NULL DEFAULT 0,
1018
1020
  checks_failed INTEGER NOT NULL DEFAULT 1,
1019
1021
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
1020
1022
  owner_email TEXT NOT NULL DEFAULT 'local@localhost',
@@ -1234,6 +1236,19 @@ const runAnalyticsMigrations = runMigrations(
1234
1236
  `,
1235
1237
  },
1236
1238
  },
1239
+ {
1240
+ version: 116,
1241
+ name: "uptime-monitor-check-diagnostics",
1242
+ sql: `ALTER TABLE monitor_check_results ADD COLUMN IF NOT EXISTS diagnostics TEXT NOT NULL DEFAULT '{}'`,
1243
+ },
1244
+ {
1245
+ version: 117,
1246
+ name: "uptime-monitor-incident-notification-delivered",
1247
+ sql: {
1248
+ postgres: `ALTER TABLE monitor_incidents ADD COLUMN IF NOT EXISTS notification_delivered BOOLEAN NOT NULL DEFAULT false`,
1249
+ sqlite: `ALTER TABLE monitor_incidents ADD COLUMN IF NOT EXISTS notification_delivered INTEGER NOT NULL DEFAULT 0`,
1250
+ },
1251
+ },
1237
1252
  ],
1238
1253
  { table: "analytics_migrations" },
1239
1254
  );
@@ -54,7 +54,7 @@ export default function registerUptimeMonitorJobs(): void {
54
54
 
55
55
  const ms = intervalMs();
56
56
  setInterval(() => {
57
- runDueMonitorsOnce().catch((err) =>
57
+ runDueMonitorsOnce({ source: "in-process" }).catch((err) =>
58
58
  console.error("[uptime-monitors] interval failed:", err),
59
59
  );
60
60
  }, ms);
@@ -0,0 +1,57 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+
3
+ import { createError, defineEventHandler, getHeader } from "h3";
4
+
5
+ import { runDueMonitorsOnce } from "../../../jobs/uptime-monitors";
6
+
7
+ declare global {
8
+ var __AGENT_NATIVE_UPTIME_MONITOR_SCHEDULED_RUNTIME__: boolean | undefined;
9
+ }
10
+
11
+ function productionLike(): boolean {
12
+ return (
13
+ process.env.NODE_ENV === "production" ||
14
+ process.env.NETLIFY === "true" ||
15
+ process.env.VERCEL === "1"
16
+ );
17
+ }
18
+
19
+ function scheduledFunctionRuntime(): boolean {
20
+ return globalThis.__AGENT_NATIVE_UPTIME_MONITOR_SCHEDULED_RUNTIME__ === true;
21
+ }
22
+
23
+ function cronSecret(): string | null {
24
+ const secret = process.env.UPTIME_MONITORS_CRON_SECRET?.trim();
25
+ return secret ? secret : null;
26
+ }
27
+
28
+ function headerMatchesSecret(
29
+ header: string | undefined,
30
+ secret: string,
31
+ ): boolean {
32
+ const expected = `Bearer ${secret}`;
33
+ const value = header?.trim() ?? "";
34
+ if (value.length !== expected.length) return false;
35
+ return timingSafeEqual(Buffer.from(value), Buffer.from(expected));
36
+ }
37
+
38
+ export default defineEventHandler(async (event) => {
39
+ const secret = cronSecret();
40
+ const scheduledRuntime = scheduledFunctionRuntime();
41
+ if (!secret && productionLike() && !scheduledRuntime) {
42
+ throw createError({
43
+ statusCode: 503,
44
+ statusMessage: "UPTIME_MONITORS_CRON_SECRET is required",
45
+ });
46
+ }
47
+
48
+ if (
49
+ secret &&
50
+ !scheduledRuntime &&
51
+ !headerMatchesSecret(getHeader(event, "authorization"), secret)
52
+ ) {
53
+ throw createError({ statusCode: 401, statusMessage: "Unauthorized" });
54
+ }
55
+
56
+ return runDueMonitorsOnce();
57
+ });
@@ -31,6 +31,7 @@ import {
31
31
  import { z } from "zod";
32
32
 
33
33
  import { getDb, schema } from "../server/db/index.js";
34
+ import { isPersonalSoloCalendarEvent } from "../server/lib/calendar-event-classification.js";
34
35
  import {
35
36
  calendarEventToMeetingView,
36
37
  eventEndIso,
@@ -82,6 +83,11 @@ export default defineAction({
82
83
  .describe(
83
84
  "Also include meetings that started within this many minutes (desktop reminder hold window). Default 0.",
84
85
  ),
86
+ excludePersonalSoloEvents: booleanParam
87
+ .default(false)
88
+ .describe(
89
+ "Exclude obvious solo personal calendar blocks such as Gym or Dinner. Used by desktop meeting reminders.",
90
+ ),
85
91
  }),
86
92
  http: { method: "GET" },
87
93
  run: async (args) => {
@@ -263,6 +269,12 @@ export default defineAction({
263
269
  for (const event of items) {
264
270
  if (!event.id || event.status === "cancelled") continue;
265
271
  if (!isTimedCalendarEvent(event)) continue;
272
+ if (
273
+ args.excludePersonalSoloEvents &&
274
+ isPersonalSoloCalendarEvent({ account, event })
275
+ ) {
276
+ continue;
277
+ }
266
278
  const startIso = eventStartIso(event);
267
279
  const endIso = eventEndIso(event);
268
280
  if (!startIso || !endIso) continue;