@agent-native/core 0.109.1 → 0.109.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 (135) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +15 -0
  3. package/corpus/core/docs/content/integrations.mdx +87 -253
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/cli/create.ts +18 -12
  6. package/corpus/core/src/client/blocks/library/diagram.config.ts +10 -1
  7. package/corpus/core/src/client/blocks/library/diagram.tsx +9 -4
  8. package/corpus/core/src/client/require-session.tsx +19 -3
  9. package/corpus/core/src/client/use-action.ts +145 -2
  10. package/corpus/core/src/client/use-session.ts +79 -48
  11. package/corpus/core/src/db/client.ts +13 -7
  12. package/corpus/core/src/db/create-get-db.ts +20 -13
  13. package/corpus/core/src/db/request-telemetry.ts +154 -0
  14. package/corpus/core/src/deploy/build.ts +63 -3
  15. package/corpus/core/src/integrations/index.ts +1 -0
  16. package/corpus/core/src/integrations/plugin.ts +9 -3
  17. package/corpus/core/src/integrations/remote-push-delivery-job.ts +48 -0
  18. package/corpus/core/src/integrations/remote-push-delivery.ts +370 -0
  19. package/corpus/core/src/integrations/remote-push-store.ts +275 -26
  20. package/corpus/core/src/server/better-auth-instance.ts +11 -3
  21. package/corpus/core/src/server/core-routes-plugin.ts +36 -14
  22. package/corpus/core/src/server/framework-request-handler.ts +11 -1
  23. package/corpus/core/src/server/http-response-telemetry.ts +233 -31
  24. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
  25. package/corpus/templates/clips/.agents/skills/dictate/SKILL.md +28 -5
  26. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +7 -0
  27. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +23 -0
  28. package/corpus/templates/clips/AGENTS.md +10 -0
  29. package/corpus/templates/clips/actions/create-dictation.ts +29 -4
  30. package/corpus/templates/clips/actions/finalize-recording.ts +1 -1
  31. package/corpus/templates/clips/actions/get-recording-player-data.ts +16 -6
  32. package/corpus/templates/clips/actions/set-mobile-capture-state.ts +40 -0
  33. package/corpus/templates/clips/actions/update-dictation.ts +9 -1
  34. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -6
  35. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -2
  36. package/corpus/templates/clips/app/routes/share.$shareId.tsx +13 -2
  37. package/corpus/templates/clips/changelog/2026-07-17-capture-dictation-background-meeting-audio-and-camera-videos.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-17-delayed-upload-verification-keeps-local-backups-retryable.md +6 -0
  39. package/corpus/templates/clips/changelog/2026-07-18-new-recording-is-now-emphasized-as-the-primary-action-in-the.md +6 -0
  40. package/corpus/templates/clips/changelog/2026-07-18-recordings-now-remain-safely-recoverable-until-the-stored-me.md +6 -0
  41. package/corpus/templates/clips/desktop/src/app.tsx +17 -0
  42. package/corpus/templates/clips/desktop/src/lib/processing-backup-recovery.ts +19 -0
  43. package/corpus/templates/clips/desktop/src/lib/recorder.ts +60 -35
  44. package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +12 -0
  45. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  46. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +14 -0
  47. package/corpus/templates/clips/server/db/schema.ts +9 -1
  48. package/corpus/templates/clips/server/lib/builder-media-compression.ts +59 -7
  49. package/corpus/templates/clips/server/lib/media-verification-state.ts +17 -0
  50. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +7 -0
  51. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +22 -1
  52. package/corpus/templates/plan/changelog/2026-07-17-diagrams-can-opt-into-a-polished-clean-rendering-mode-withou.md +6 -0
  53. package/corpus/templates/plan/shared/plan-content.ts +3 -0
  54. package/corpus/templates/tasks/AGENTS.md +3 -0
  55. package/corpus/templates/tasks/actions/render-task-list-inline.ts +202 -0
  56. package/corpus/templates/tasks/changelog/2026-07-18-task-lists-can-now-appear-as-interactive-widgets-inline-in-c.md +6 -0
  57. package/corpus/templates/tasks/docs/features/f8-chat.mdx +2 -0
  58. package/corpus/templates/tasks/server/plugins/agent-chat.ts +2 -0
  59. package/dist/cli/create.d.ts +10 -1
  60. package/dist/cli/create.d.ts.map +1 -1
  61. package/dist/cli/create.js +17 -13
  62. package/dist/cli/create.js.map +1 -1
  63. package/dist/client/blocks/library/diagram.config.d.ts +2 -0
  64. package/dist/client/blocks/library/diagram.config.d.ts.map +1 -1
  65. package/dist/client/blocks/library/diagram.config.js +8 -1
  66. package/dist/client/blocks/library/diagram.config.js.map +1 -1
  67. package/dist/client/blocks/library/diagram.d.ts.map +1 -1
  68. package/dist/client/blocks/library/diagram.js +9 -4
  69. package/dist/client/blocks/library/diagram.js.map +1 -1
  70. package/dist/client/require-session.d.ts.map +1 -1
  71. package/dist/client/require-session.js +6 -3
  72. package/dist/client/require-session.js.map +1 -1
  73. package/dist/client/use-action.d.ts.map +1 -1
  74. package/dist/client/use-action.js +117 -1
  75. package/dist/client/use-action.js.map +1 -1
  76. package/dist/client/use-session.d.ts.map +1 -1
  77. package/dist/client/use-session.js +72 -50
  78. package/dist/client/use-session.js.map +1 -1
  79. package/dist/collab/routes.d.ts +1 -1
  80. package/dist/db/client.d.ts.map +1 -1
  81. package/dist/db/client.js +7 -1
  82. package/dist/db/client.js.map +1 -1
  83. package/dist/db/create-get-db.d.ts +3 -6
  84. package/dist/db/create-get-db.d.ts.map +1 -1
  85. package/dist/db/create-get-db.js +9 -2
  86. package/dist/db/create-get-db.js.map +1 -1
  87. package/dist/db/request-telemetry.d.ts +23 -0
  88. package/dist/db/request-telemetry.d.ts.map +1 -0
  89. package/dist/db/request-telemetry.js +95 -0
  90. package/dist/db/request-telemetry.js.map +1 -0
  91. package/dist/deploy/build.d.ts +3 -0
  92. package/dist/deploy/build.d.ts.map +1 -1
  93. package/dist/deploy/build.js +45 -1
  94. package/dist/deploy/build.js.map +1 -1
  95. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  96. package/dist/integrations/index.d.ts +1 -0
  97. package/dist/integrations/index.d.ts.map +1 -1
  98. package/dist/integrations/index.js +1 -0
  99. package/dist/integrations/index.js.map +1 -1
  100. package/dist/integrations/plugin.d.ts.map +1 -1
  101. package/dist/integrations/plugin.js +8 -3
  102. package/dist/integrations/plugin.js.map +1 -1
  103. package/dist/integrations/remote-push-delivery-job.d.ts +4 -0
  104. package/dist/integrations/remote-push-delivery-job.d.ts.map +1 -0
  105. package/dist/integrations/remote-push-delivery-job.js +44 -0
  106. package/dist/integrations/remote-push-delivery-job.js.map +1 -0
  107. package/dist/integrations/remote-push-delivery.d.ts +11 -0
  108. package/dist/integrations/remote-push-delivery.d.ts.map +1 -0
  109. package/dist/integrations/remote-push-delivery.js +265 -0
  110. package/dist/integrations/remote-push-delivery.js.map +1 -0
  111. package/dist/integrations/remote-push-store.d.ts +33 -0
  112. package/dist/integrations/remote-push-store.d.ts.map +1 -1
  113. package/dist/integrations/remote-push-store.js +195 -26
  114. package/dist/integrations/remote-push-store.js.map +1 -1
  115. package/dist/observability/routes.d.ts +5 -5
  116. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  117. package/dist/resources/handlers.d.ts +1 -1
  118. package/dist/server/better-auth-instance.js +7 -3
  119. package/dist/server/better-auth-instance.js.map +1 -1
  120. package/dist/server/core-routes-plugin.d.ts +8 -0
  121. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  122. package/dist/server/core-routes-plugin.js +23 -8
  123. package/dist/server/core-routes-plugin.js.map +1 -1
  124. package/dist/server/framework-request-handler.d.ts.map +1 -1
  125. package/dist/server/framework-request-handler.js +9 -1
  126. package/dist/server/framework-request-handler.js.map +1 -1
  127. package/dist/server/http-response-telemetry.d.ts +3 -2
  128. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  129. package/dist/server/http-response-telemetry.js +160 -27
  130. package/dist/server/http-response-telemetry.js.map +1 -1
  131. package/dist/server/transcribe-voice.d.ts +1 -1
  132. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
  133. package/docs/content/integrations.mdx +87 -253
  134. package/package.json +1 -1
  135. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
@@ -88,7 +88,9 @@ function HtmlDiagram({
88
88
  }) {
89
89
  const ref = useRef<HTMLDivElement>(null);
90
90
  const isDark = useIsDark();
91
- const style = useWireframeStyle();
91
+ const preferredStyle = useWireframeStyle();
92
+ const designMode = data.renderMode === "design";
93
+ const style = designMode ? "clean" : preferredStyle;
92
94
  const showFrame = resolveVisualFrame(data.frame, ctx);
93
95
  const scopeId = useId().replace(/[^a-zA-Z0-9_-]/g, "");
94
96
  // Sanitize author HTML/CSS at the render point (defense-in-depth against
@@ -127,7 +129,7 @@ function HtmlDiagram({
127
129
  </div>
128
130
  <RoughOverlay
129
131
  scopeRef={ref}
130
- enabled={style === "sketchy"}
132
+ enabled={!designMode && style === "sketchy"}
131
133
  drawFrame={showFrame}
132
134
  selector={DIAGRAM_ROUGH_SELECTOR}
133
135
  />
@@ -650,7 +652,8 @@ function ExpandableDiagramBody({
650
652
  compact?: boolean;
651
653
  }) {
652
654
  const [expanded, setExpanded] = useState(false);
653
- const supportsStyleToggle = Boolean(data.html);
655
+ const designMode = data.renderMode === "design";
656
+ const supportsStyleToggle = Boolean(data.html) && !designMode;
654
657
  const style = useWireframeStyle();
655
658
  const copy = useBlockCopy();
656
659
  const sketchy = style === "sketchy";
@@ -794,7 +797,9 @@ export function DiagramEdit({
794
797
  onChange({
795
798
  html: html.trim() || undefined,
796
799
  css: css.trim() || undefined,
800
+ renderMode: data.renderMode,
797
801
  caption: caption.trim() || undefined,
802
+ frame: data.frame,
798
803
  nodes: data.nodes,
799
804
  edges: data.edges,
800
805
  notes: data.notes,
@@ -946,7 +951,7 @@ export const diagramBlock = defineBlock<DiagramData>({
946
951
  editSurface: "panel",
947
952
  label: "Diagram",
948
953
  description:
949
- "A flexible inline architecture/code diagram. Prefer html/css with SVG or semantic HTML for polished two-dimensional layouts; use .diagram-* primitives and --wf-* tokens for theme/sketch compatibility. Legacy nodes/edges are only for simple previews.",
954
+ "A flexible inline architecture/code diagram. Prefer html/css with SVG or semantic HTML for polished two-dimensional layouts; use renderMode design for clean presentation or .diagram-* primitives and --wf-* tokens for theme/sketch compatibility. Legacy nodes/edges are only for simple previews.",
950
955
  // Seed the legacy fallback shape so a fresh block validates while agents can
951
956
  // replace it with html/css when layout quality matters.
952
957
  empty: () => ({ nodes: [{ id: "n1", label: "Module" }], edges: [] }),
@@ -95,14 +95,31 @@ export function RequireSession({
95
95
  signedOut,
96
96
  bypass = false,
97
97
  }: RequireSessionProps) {
98
+ if (bypass) return <>{children}</>;
99
+ return (
100
+ <ResolvedSessionGate
101
+ fallback={fallback}
102
+ redirect={redirect}
103
+ signedOut={signedOut}
104
+ >
105
+ {children}
106
+ </ResolvedSessionGate>
107
+ );
108
+ }
109
+
110
+ function ResolvedSessionGate({
111
+ children,
112
+ fallback,
113
+ redirect = true,
114
+ signedOut,
115
+ }: Omit<RequireSessionProps, "bypass">) {
98
116
  const { session, isLoading } = useSession();
99
117
  // Guard against firing the redirect more than once (effect re-runs, React
100
118
  // StrictMode double-invoke) — a second navigation while the first is in
101
119
  // flight is harmless but noisy.
102
120
  const redirectedRef = useRef(false);
103
121
 
104
- const mustRedirect =
105
- !bypass && !isLoading && !session && redirect && !isOnSignInPage();
122
+ const mustRedirect = !isLoading && !session && redirect && !isOnSignInPage();
106
123
 
107
124
  useEffect(() => {
108
125
  if (!mustRedirect) return;
@@ -114,7 +131,6 @@ export function RequireSession({
114
131
  window.location.replace(buildSignInReturnHref());
115
132
  }, [mustRedirect]);
116
133
 
117
- if (bypass) return <>{children}</>;
118
134
  // Still resolving, or redirect already in flight: show the loading fallback
119
135
  // rather than flashing app chrome the visitor can't use.
120
136
  if (isLoading) return <>{fallback ?? <DefaultSpinner />}</>;
@@ -27,6 +27,7 @@ import type {
27
27
  UseMutationOptions,
28
28
  } from "@tanstack/react-query";
29
29
 
30
+ import { trackEvent } from "./analytics.js";
30
31
  import { agentNativePath } from "./api-path.js";
31
32
  import { getBrowserTabId } from "./browser-tab-id.js";
32
33
  import { ensureEmbedAuthFetchInterceptor } from "./embed-auth.js";
@@ -221,6 +222,10 @@ export interface ActionFetchOptions {
221
222
  includeRequestSource?: boolean;
222
223
  }
223
224
 
225
+ type InternalActionFetchOptions = ActionFetchOptions & {
226
+ onResponse?: (response: Response) => void;
227
+ };
228
+
224
229
  /**
225
230
  * Conservative per-document keepalive body budget. Browsers commonly enforce
226
231
  * an approximately 64 KiB aggregate limit across every in-flight keepalive
@@ -251,11 +256,11 @@ function utf8ByteLength(value: string): number {
251
256
  return bytes;
252
257
  }
253
258
 
254
- async function actionFetch<T>(
259
+ async function performActionFetch<T>(
255
260
  name: string,
256
261
  method: string,
257
262
  params?: Record<string, any>,
258
- options?: ActionFetchOptions,
263
+ options?: InternalActionFetchOptions,
259
264
  ): Promise<T> {
260
265
  ensureEmbedAuthFetchInterceptor();
261
266
  let url = `${ACTION_PREFIX}/${name}`;
@@ -332,6 +337,7 @@ async function actionFetch<T>(
332
337
  try {
333
338
  try {
334
339
  res = await fetch(url, init);
340
+ options?.onResponse?.(res);
335
341
  } catch (err) {
336
342
  if (timedOut) throwTimeout();
337
343
  // Caller-initiated cancellation — rethrow untouched so React Query
@@ -418,6 +424,143 @@ async function actionFetch<T>(
418
424
  return (data ?? (null as unknown)) as T;
419
425
  }
420
426
 
427
+ function actionTelemetryNow(): number {
428
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
429
+ }
430
+
431
+ function parseServerTiming(
432
+ response: Response | undefined,
433
+ ): Map<string, number> {
434
+ const timings = new Map<string, number>();
435
+ const value = response?.headers.get("server-timing");
436
+ if (!value) return timings;
437
+
438
+ for (const entry of value.split(",")) {
439
+ const [rawName, ...params] = entry.trim().split(";");
440
+ const name = rawName?.trim();
441
+ if (!name) continue;
442
+ const duration = params
443
+ .map((param) => /^dur=(.+)$/i.exec(param.trim())?.[1])
444
+ .find(Boolean);
445
+ const parsed = duration === undefined ? NaN : Number(duration);
446
+ if (Number.isFinite(parsed)) timings.set(name, parsed);
447
+ }
448
+ return timings;
449
+ }
450
+
451
+ function shouldTrackActionResponse(
452
+ error: unknown,
453
+ durationMs: number,
454
+ response: Response | undefined,
455
+ ): boolean {
456
+ if (error || durationMs >= 1_000) return true;
457
+ if (response && response.status >= 400 && response.status < 500) return true;
458
+ if (
459
+ /\bstartup(?:-db)?\s*;/i.test(response?.headers.get("server-timing") ?? "")
460
+ ) {
461
+ return true;
462
+ }
463
+ const raw = (import.meta.env as Record<string, string | undefined>)
464
+ ?.VITE_AGENT_NATIVE_ACTION_TELEMETRY_SAMPLE_RATE;
465
+ const parsed = raw === undefined ? 0.1 : Number(raw);
466
+ const rate = Number.isFinite(parsed) ? Math.max(0, Math.min(1, parsed)) : 0.1;
467
+ return Math.random() < rate;
468
+ }
469
+
470
+ async function actionFetch<T>(
471
+ name: string,
472
+ method: string,
473
+ params?: Record<string, any>,
474
+ options?: ActionFetchOptions,
475
+ ): Promise<T> {
476
+ const startedAt = actionTelemetryNow();
477
+ let response: Response | undefined;
478
+ let responseAt: number | undefined;
479
+ let error: unknown;
480
+
481
+ try {
482
+ return await performActionFetch<T>(name, method, params, {
483
+ ...options,
484
+ onResponse: (nextResponse) => {
485
+ response = nextResponse;
486
+ responseAt = actionTelemetryNow();
487
+ },
488
+ });
489
+ } catch (caught) {
490
+ error = caught;
491
+ throw caught;
492
+ } finally {
493
+ try {
494
+ const completedAt = actionTelemetryNow();
495
+ const durationMs = Math.max(0, completedAt - startedAt);
496
+ if (shouldTrackActionResponse(error, durationMs, response)) {
497
+ const ttfbMs =
498
+ responseAt === undefined
499
+ ? undefined
500
+ : Math.max(0, responseAt - startedAt);
501
+ const serverTiming = parseServerTiming(response);
502
+ const serverDurationMs = serverTiming.get("app");
503
+ const errorStatus = Number(
504
+ (error as { status?: unknown } | undefined)?.status,
505
+ );
506
+ const statusCode =
507
+ response?.status ??
508
+ (Number.isFinite(errorStatus) ? errorStatus : undefined);
509
+ const timedOut =
510
+ (error as { timedOut?: unknown } | undefined)?.timedOut === true;
511
+ const cancelled = options?.signal?.aborted === true && !timedOut;
512
+ const contentLength = Number(response?.headers.get("content-length"));
513
+
514
+ trackEvent("action.response", {
515
+ request_id:
516
+ response?.headers.get("x-agent-native-request-id") ?? undefined,
517
+ action: name,
518
+ method,
519
+ status_code: statusCode,
520
+ status_class:
521
+ statusCode === undefined
522
+ ? "network"
523
+ : `${Math.floor(statusCode / 100)}xx`,
524
+ success: !error,
525
+ outcome: !error
526
+ ? "success"
527
+ : timedOut
528
+ ? "timeout"
529
+ : cancelled
530
+ ? "cancelled"
531
+ : response
532
+ ? "http-error"
533
+ : "network-error",
534
+ duration_ms: Math.round(durationMs),
535
+ ttfb_ms: ttfbMs === undefined ? undefined : Math.round(ttfbMs),
536
+ body_ms:
537
+ ttfbMs === undefined ? undefined : Math.round(durationMs - ttfbMs),
538
+ server_duration_ms:
539
+ serverDurationMs === undefined
540
+ ? undefined
541
+ : Math.round(serverDurationMs),
542
+ network_overhead_ms:
543
+ ttfbMs === undefined || serverDurationMs === undefined
544
+ ? undefined
545
+ : Math.max(0, Math.round(ttfbMs - serverDurationMs)),
546
+ framework_ready_wait_ms: serverTiming.get("startup"),
547
+ db_operation_wall_ms: serverTiming.get("db"),
548
+ db_connect_total_ms: serverTiming.get("db-connect"),
549
+ db_slowest_operation_ms: serverTiming.get("db-slowest"),
550
+ startup_db_operation_wall_ms: serverTiming.get("startup-db"),
551
+ startup_db_connect_total_ms: serverTiming.get("startup-db-connect"),
552
+ response_bytes:
553
+ Number.isFinite(contentLength) && contentLength >= 0
554
+ ? contentLength
555
+ : undefined,
556
+ });
557
+ }
558
+ } catch {
559
+ // Performance telemetry must never change the action result.
560
+ }
561
+ }
562
+ }
563
+
421
564
  /**
422
565
  * Imperatively call an action from browser/client code.
423
566
  *
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useRef } from "react";
1
+ import { useEffect, useState } from "react";
2
2
 
3
3
  import type { AuthSession } from "../server/auth.js";
4
4
  import { setSentryUser, trackSessionStatus } from "./analytics.js";
@@ -11,6 +11,65 @@ interface UseSessionResult {
11
11
  isLoading: boolean;
12
12
  }
13
13
 
14
+ const SESSION_CACHE_TTL_MS = 30_000;
15
+ const SESSION_RETRY_DELAY_MS = 1_000;
16
+ let cachedSession: AuthSession | null | undefined;
17
+ let cachedSessionAt = 0;
18
+ let sessionRequest: Promise<AuthSession | null | undefined> | undefined;
19
+ let trackedSessionIdentity: string | null | undefined;
20
+
21
+ function hasFreshSessionCache(): boolean {
22
+ return (
23
+ cachedSession !== undefined &&
24
+ Date.now() - cachedSessionAt < SESSION_CACHE_TTL_MS
25
+ );
26
+ }
27
+
28
+ function publishSessionIdentity(session: AuthSession | null): void {
29
+ const identity = session?.userId ?? session?.email ?? null;
30
+ if (trackedSessionIdentity !== identity) {
31
+ trackedSessionIdentity = identity;
32
+ if (session) {
33
+ setSentryUser(
34
+ {
35
+ id: session.userId,
36
+ email: session.email,
37
+ username: session.name,
38
+ },
39
+ session.orgId ?? null,
40
+ );
41
+ } else {
42
+ setSentryUser(null, null);
43
+ }
44
+ }
45
+ trackSessionStatus(Boolean(session));
46
+ }
47
+
48
+ function fetchSharedSession(): Promise<AuthSession | null | undefined> {
49
+ if (hasFreshSessionCache()) return Promise.resolve(cachedSession ?? null);
50
+ if (sessionRequest) return sessionRequest;
51
+
52
+ sessionRequest = (async () => {
53
+ try {
54
+ const res = await fetch(agentNativePath("/_agent-native/auth/session"));
55
+ if (!res.ok) return undefined;
56
+
57
+ const data = await res.json();
58
+ const session = data.error ? null : (data as AuthSession);
59
+ cachedSession = session;
60
+ cachedSessionAt = Date.now();
61
+ publishSessionIdentity(session);
62
+ return session;
63
+ } catch {
64
+ return undefined;
65
+ }
66
+ })().finally(() => {
67
+ sessionRequest = undefined;
68
+ });
69
+
70
+ return sessionRequest;
71
+ }
72
+
14
73
  /**
15
74
  * Client-side hook to get the current auth session.
16
75
  *
@@ -21,61 +80,33 @@ interface UseSessionResult {
21
80
  * Templates should use this instead of building their own auth context.
22
81
  */
23
82
  export function useSession(): UseSessionResult {
24
- const [session, setSession] = useState<AuthSession | null>(null);
25
- const [isLoading, setIsLoading] = useState(true);
26
- const trackedRef = useRef(false);
83
+ const cached = hasFreshSessionCache() ? (cachedSession ?? null) : null;
84
+ const [session, setSession] = useState<AuthSession | null>(cached);
85
+ const [isLoading, setIsLoading] = useState(!hasFreshSessionCache());
27
86
 
28
87
  useEffect(() => {
29
88
  let cancelled = false;
89
+ let retryTimer: ReturnType<typeof setTimeout> | undefined;
30
90
 
31
- async function fetchSession() {
32
- let signedIn = false;
33
- let resolved: AuthSession | null = null;
34
- try {
35
- const res = await fetch(agentNativePath("/_agent-native/auth/session"));
36
- if (!res.ok) {
37
- setSession(null);
38
- return;
39
- }
40
- const data = await res.json();
41
- if (!cancelled) {
42
- // The endpoint returns { error: "..." } when not authenticated
43
- if (data.error) {
44
- setSession(null);
45
- } else {
46
- resolved = data as AuthSession;
47
- setSession(resolved);
48
- signedIn = true;
49
- }
50
- }
51
- } catch {
52
- if (!cancelled) setSession(null);
53
- } finally {
54
- if (!cancelled) {
55
- setIsLoading(false);
56
- if (resolved) {
57
- setSentryUser(
58
- {
59
- id: resolved.userId,
60
- email: resolved.email,
61
- username: resolved.name,
62
- },
63
- resolved.orgId ?? null,
64
- );
65
- } else {
66
- setSentryUser(null, null);
67
- }
68
- if (!trackedRef.current) {
69
- trackedRef.current = true;
70
- trackSessionStatus(signedIn);
71
- }
72
- }
91
+ const resolveSession = async () => {
92
+ const resolved = await fetchSharedSession();
93
+ if (cancelled) return;
94
+
95
+ if (resolved === undefined) {
96
+ retryTimer = setTimeout(() => {
97
+ void resolveSession();
98
+ }, SESSION_RETRY_DELAY_MS);
99
+ return;
73
100
  }
74
- }
75
101
 
76
- fetchSession();
102
+ setSession(resolved);
103
+ setIsLoading(false);
104
+ };
105
+
106
+ void resolveSession();
77
107
  return () => {
78
108
  cancelled = true;
109
+ if (retryTimer) clearTimeout(retryTimer);
79
110
  };
80
111
  }, []);
81
112
 
@@ -10,6 +10,11 @@
10
10
  */
11
11
  import path from "path";
12
12
 
13
+ import {
14
+ beginDatabaseOperation,
15
+ recordDatabaseRetry,
16
+ } from "./request-telemetry.js";
17
+
13
18
  const recyclingPostgresPools = new WeakSet<object>();
14
19
  const loggedNeonPools = new WeakSet<object>();
15
20
 
@@ -735,6 +740,7 @@ export async function retryOnConnectionError<T>(
735
740
  } catch (e) {
736
741
  last = e;
737
742
  if (!isConnectionError(e) || attempt === maxAttempts - 1) throw e;
743
+ recordDatabaseRetry();
738
744
  await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
739
745
  }
740
746
  }
@@ -790,6 +796,9 @@ export async function withDbTimeout<T>(
790
796
  ): Promise<T> {
791
797
  let timer: ReturnType<typeof setTimeout> | undefined;
792
798
  let settled = false;
799
+ const finishTelemetry = beginDatabaseOperation(
800
+ op === "connect" ? "connect" : "query",
801
+ );
793
802
 
794
803
  const runCleanup = async () => {
795
804
  if (!onTimeout) return;
@@ -811,12 +820,14 @@ export async function withDbTimeout<T>(
811
820
  if (settled) return;
812
821
  settled = true;
813
822
  if (timer) clearTimeout(timer);
823
+ finishTelemetry("success");
814
824
  complete(value);
815
825
  };
816
826
  const fail = (err: unknown) => {
817
827
  if (settled) return;
818
828
  settled = true;
819
829
  if (timer) clearTimeout(timer);
830
+ finishTelemetry("error");
820
831
  reject(err);
821
832
  };
822
833
 
@@ -825,6 +836,7 @@ export async function withDbTimeout<T>(
825
836
  settled = true;
826
837
  void (async () => {
827
838
  await runCleanup();
839
+ finishTelemetry("timeout");
828
840
  reject(new DbTimeoutError(op, ms));
829
841
  })();
830
842
  }, ms);
@@ -1171,13 +1183,7 @@ async function createDbExecInternal(
1171
1183
  return retryOnConnectionError<{
1172
1184
  rows: unknown[];
1173
1185
  rowsAffected: number;
1174
- }>(() =>
1175
- withDbTimeout(
1176
- "http-query",
1177
- () => queryNeonClient(pool, sql),
1178
- dbOpTimeoutMs(),
1179
- ),
1180
- );
1186
+ }>(() => queryNeonClient(pool, sql));
1181
1187
  }
1182
1188
  const result = await retryOnConnectionError<{
1183
1189
  rows: unknown[];
@@ -84,23 +84,28 @@ export function isSqlRead(sql: string): boolean {
84
84
  * connection the transaction needs. The pool-level error logger still fires
85
85
  * on idle-client drops inside transactions.
86
86
  */
87
- export function buildResilientNeonPool(pool: {
88
- connect(): Promise<any>;
89
- query(
90
- sql: string,
91
- args?: any[],
92
- ): Promise<{ rows: unknown[]; rowCount?: number }>;
93
- end(): Promise<void>;
94
- on(event: string, listener: (...args: any[]) => void): unknown;
95
- }): typeof pool {
87
+ export function buildResilientNeonPool<
88
+ T extends {
89
+ connect(): Promise<any>;
90
+ query(...args: any[]): Promise<any>;
91
+ end(): Promise<void>;
92
+ on(event: string, listener: (...args: any[]) => void): unknown;
93
+ },
94
+ >(pool: T): T {
96
95
  // Preserve all original pool methods and properties; only override `connect`
97
96
  // and `query` at the Pool level (used by drizzle's neon-serverless adapter
98
97
  // when it calls pool.query() directly, e.g. outside a transaction).
99
98
  const resilientQuery = async (
100
- sql: string,
99
+ sql: string | { text?: unknown },
101
100
  args?: any[],
102
101
  ): Promise<{ rows: unknown[]; rowCount?: number }> => {
103
- const isRead = isSqlRead(sql);
102
+ const sqlText =
103
+ typeof sql === "string"
104
+ ? sql
105
+ : typeof sql?.text === "string"
106
+ ? sql.text
107
+ : "";
108
+ const isRead = isSqlRead(sqlText);
104
109
 
105
110
  const runAttempt = async (): Promise<{
106
111
  rows: unknown[];
@@ -135,7 +140,9 @@ export function buildResilientNeonPool(pool: {
135
140
  const result = await withDbTimeout(
136
141
  "query",
137
142
  () =>
138
- client.query(sql, args ?? []) as Promise<{
143
+ (args === undefined
144
+ ? client.query(sql)
145
+ : client.query(sql, args)) as Promise<{
139
146
  rows: unknown[];
140
147
  rowCount?: number;
141
148
  }>,
@@ -180,7 +187,7 @@ export function buildResilientNeonPool(pool: {
180
187
  const val = (target as any)[prop];
181
188
  return typeof val === "function" ? val.bind(target) : val;
182
189
  },
183
- });
190
+ }) as T;
184
191
  }
185
192
 
186
193
  /**