@agent-native/core 0.140.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 (128) 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/.agents/skills/design-systems/SKILL.md +1 -44
  27. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +1 -265
  28. package/corpus/templates/design/actions/create-design-system.ts +10 -9
  29. package/corpus/templates/design/actions/get-design-system.ts +0 -53
  30. package/corpus/templates/design/actions/import-design-tokens.ts +71 -5
  31. package/corpus/templates/design/actions/index-design-tokens.ts +80 -61
  32. package/corpus/templates/design/actions/update-design-system.ts +10 -7
  33. package/corpus/templates/design/app/components/design/EditPanel.tsx +2 -45
  34. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +0 -12
  35. package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +6 -10
  36. package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +3 -19
  37. package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +10 -36
  38. package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +0 -15
  39. package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +1 -19
  40. package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +0 -6
  41. package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +23 -53
  42. package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +4 -142
  43. package/corpus/templates/design/app/components/design/types.ts +0 -6
  44. package/corpus/templates/design/app/i18n-data.ts +0 -20
  45. package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +1 -1
  46. package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -78
  47. package/corpus/templates/design/app/pages/design-editor/html-layer-positioning.ts +1 -6
  48. package/corpus/templates/design/e2e/helpers.ts +0 -17
  49. package/corpus/templates/design/shared/resolve-tweaks.ts +18 -10
  50. package/corpus/templates/forms/server/lib/response-email.ts +0 -2
  51. package/corpus/templates/plan/actions/request-plan-access.ts +12 -33
  52. package/corpus/templates/plan/server/lib/comment-notifications.ts +27 -53
  53. package/corpus/templates/slides/server/lib/comment-notifications.ts +21 -44
  54. package/dist/a2a/handlers.js +9 -10
  55. package/dist/brand-kit/index.d.ts +0 -1
  56. package/dist/brand-kit/index.js +0 -1
  57. package/dist/brand-kit/types.d.ts +0 -25
  58. package/dist/collab/awareness.d.ts +2 -2
  59. package/dist/collab/struct-routes.d.ts +1 -1
  60. package/dist/db/client.js +8 -8
  61. package/dist/db/migrations.js +39 -19
  62. package/dist/localization/default-messages.d.ts +0 -45
  63. package/dist/localization/default-messages.js +0 -45
  64. package/dist/notifications/routes.d.ts +6 -6
  65. package/dist/observability/routes.d.ts +3 -3
  66. package/dist/org/handlers.js +1 -8
  67. package/dist/progress/routes.d.ts +1 -1
  68. package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
  69. package/dist/provider-api/actions/provider-api.d.ts +7 -7
  70. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  71. package/dist/resources/handlers.d.ts +1 -1
  72. package/dist/secrets/routes.d.ts +9 -9
  73. package/dist/server/action-discovery.js +0 -22
  74. package/dist/server/auth.js +3 -1
  75. package/dist/server/better-auth-instance.js +2 -17
  76. package/dist/server/email.d.ts +0 -9
  77. package/dist/server/email.js +3 -44
  78. package/dist/server/index.d.ts +0 -1
  79. package/dist/server/index.js +0 -1
  80. package/dist/server/transcribe-voice.d.ts +1 -1
  81. package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +3 -2
  82. package/docs/content/authentication.mdx +25 -19
  83. package/package.json +1 -1
  84. package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +3 -2
  85. package/corpus/templates/analytics/server/lib/emails.ts +0 -51
  86. package/corpus/templates/analytics/server/plugins/transactional-emails.ts +0 -5
  87. package/corpus/templates/calendar/server/lib/emails.ts +0 -167
  88. package/corpus/templates/calendar/server/plugins/transactional-emails.ts +0 -5
  89. package/corpus/templates/clips/server/lib/emails.ts +0 -256
  90. package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +0 -5
  91. package/corpus/templates/content/server/lib/emails.ts +0 -70
  92. package/corpus/templates/content/server/plugins/transactional-emails.ts +0 -5
  93. package/corpus/templates/design/app/components/design/edit-panel/design-system-swatches.ts +0 -215
  94. package/corpus/templates/design/changelog/2026-08-04-design-system-tokens-show-by-name-in-the-editor.md +0 -6
  95. package/corpus/templates/design/server/lib/design-system-tokens.ts +0 -35
  96. package/corpus/templates/design/shared/authored-color-styles.ts +0 -401
  97. package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +0 -4
  98. package/corpus/templates/dispatch/app/routes/transactional-email.tsx +0 -4
  99. package/corpus/templates/forms/server/lib/emails.ts +0 -52
  100. package/corpus/templates/forms/server/plugins/transactional-emails.ts +0 -5
  101. package/corpus/templates/plan/server/lib/emails.ts +0 -67
  102. package/corpus/templates/plan/server/plugins/transactional-emails.ts +0 -5
  103. package/corpus/templates/slides/server/lib/emails.ts +0 -40
  104. package/corpus/templates/slides/server/plugins/transactional-emails.ts +0 -5
  105. package/dist/brand-kit/tokens.d.ts +0 -92
  106. package/dist/brand-kit/tokens.js +0 -280
  107. package/dist/email-catalog/actions/list-email-activity.d.ts +0 -6
  108. package/dist/email-catalog/actions/list-email-activity.js +0 -15
  109. package/dist/email-catalog/actions/list-email-engagement.d.ts +0 -6
  110. package/dist/email-catalog/actions/list-email-engagement.js +0 -15
  111. package/dist/email-catalog/actions/list-email-log.d.ts +0 -8
  112. package/dist/email-catalog/actions/list-email-log.js +0 -22
  113. package/dist/email-catalog/actions/list-transactional-emails.d.ts +0 -23
  114. package/dist/email-catalog/actions/list-transactional-emails.js +0 -61
  115. package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +0 -10
  116. package/dist/email-catalog/actions/render-transactional-email-preview.js +0 -24
  117. package/dist/email-catalog/authorize.d.ts +0 -2
  118. package/dist/email-catalog/authorize.js +0 -10
  119. package/dist/email-catalog/log.d.ts +0 -57
  120. package/dist/email-catalog/log.js +0 -117
  121. package/dist/email-catalog/provider-metrics.d.ts +0 -34
  122. package/dist/email-catalog/provider-metrics.js +0 -165
  123. package/dist/email-catalog/registry.d.ts +0 -79
  124. package/dist/email-catalog/registry.js +0 -71
  125. package/dist/email-catalog/schema.d.ts +0 -212
  126. package/dist/email-catalog/schema.js +0 -49
  127. package/dist/email-catalog/system-emails.d.ts +0 -14
  128. package/dist/email-catalog/system-emails.js +0 -68
package/corpus/README.md CHANGED
@@ -31,4 +31,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
31
31
 
32
32
  ## Generated Counts
33
33
 
34
- - template files: 7658
34
+ - template files: 7643
@@ -15,7 +15,7 @@ function resolveScope() {
15
15
 
16
16
  export default defineAction({
17
17
  description:
18
- "Query the built-in first-party Analytics source: events recorded through this app's analytics collector endpoint (/track), compact daily rollups, identifiable user-day rollups, and session replay summaries recorded through /api/analytics/replay. This source does not require an external provider connection. Use it for questions about app/site traffic, product events, template/app usage, conversions, session recordings, LLM/agent observability, model cost, token volume, latency, and other first-party data collected by this analytics app. Use source-specific actions such as BigQuery, GA4, Mixpanel, PostHog, or Amplitude when the user asks for those sources or the relevant data lives there. SQL may read analytics_events, analytics_event_daily_rollups, analytics_user_days, and session_recordings; session_replay_chunks is intentionally unavailable, and reads are automatically scoped to the current user/org. Prefer analytics_event_daily_rollups for event counts and analytics_user_days for active-user or retention questions. Use analytics_events only for bounded recent drill-downs with an explicit event_date or timestamp range. Aggregate, project only needed columns, and add a LIMIT for raw or high-cardinality reads; do not issue an unbounded raw-event scan or paginate a large cohort. An explicit all-time or lifetime request remains all-time, so do not invent a default lower time bound. Safe scoped results are cached for up to five minutes for this agent action. analytics_events columns include event_name, timestamp, event_date, user_id, anonymous_id, user_key, session_id, app, template, signed_in, url, path, hostname, referrer, properties, and context. analytics_event_daily_rollups columns include tenant_key, owner_email, org_id, event_date, event_name, app, template, and event_count. analytics_user_days columns include tenant_key, owner_email, org_id, event_date, and user_key. LLM observability events use event_name = '$ai_generation'; useful properties include $ai_trace_id/run_id, $ai_session_id/thread_id, $ai_model/model, $ai_provider/provider, $ai_input_tokens/input_tokens, $ai_output_tokens/output_tokens, cache_read_tokens, cache_write_tokens, cost_cents_x100, $ai_total_cost_usd/cost_usd, duration_ms/$ai_latency, status, tool_calls, successful_tools, failed_tools, tools, tools_truncated, delegated, delegation_protocol, caller_app, delegation_task_id, a2a_task_id, parent_run_id, parent_turn_id, and error_message/$ai_error. Agent Teams child runs use delegation_protocol = 'agent-team' and retain their own run_id while linking to the launching run through parent_run_id. The bounded tools array contains names, relative start times, durations, statuses, and coarse error classes only, never args or results; failed runs and interrupted tools remain queryable. session_recordings columns include id, session_id, user_id, anonymous_id, user_key, started_at, ended_at, duration_ms, chunk_count, event_count, page_count, error_count, rage_click_count, app, template, status, first_url, last_url, path, hostname, referrer, and metadata.",
18
+ "Query the built-in first-party Analytics source: events recorded through this app's analytics collector endpoint (/track), compact daily rollups, identifiable user-day rollups, and session replay summaries recorded through /api/analytics/replay. This source does not require an external provider connection. Use it for questions about app/site traffic, product events, template/app usage, conversions, session recordings, LLM/agent observability, model cost, token volume, latency, and other first-party data collected by this analytics app. Use source-specific actions such as BigQuery, GA4, Mixpanel, PostHog, or Amplitude when the user asks for those sources or the relevant data lives there. SQL may read analytics_events, analytics_event_daily_rollups, analytics_user_days, and session_recordings; session_replay_chunks is intentionally unavailable, and reads are automatically scoped to the current user/org. Prefer analytics_event_daily_rollups for event counts and analytics_user_days for active-user or retention questions. The rollups are backfilled from existing analytics_events when this schema is upgraded and are updated transactionally with new ingest. Use analytics_events only for bounded recent drill-downs with an explicit event_date or timestamp range. Aggregate, project only needed columns, and add a LIMIT for raw or high-cardinality reads; do not issue an unbounded raw-event scan or paginate a large cohort. An explicit all-time or lifetime request remains all-time, so do not invent a default lower time bound. Safe scoped results are cached for up to five minutes for this agent action. analytics_events columns include event_name, timestamp, event_date, user_id, anonymous_id, user_key, session_id, app, template, signed_in, url, path, hostname, referrer, properties, and context. analytics_event_daily_rollups columns include tenant_key, owner_email, org_id, event_date, event_name, app, template, and event_count. analytics_user_days columns include tenant_key, owner_email, org_id, event_date, and user_key. LLM observability events use event_name = '$ai_generation'; useful properties include $ai_trace_id/run_id, $ai_session_id/thread_id, $ai_model/model, $ai_provider/provider, $ai_input_tokens/input_tokens, $ai_output_tokens/output_tokens, cache_read_tokens, cache_write_tokens, cost_cents_x100, $ai_total_cost_usd/cost_usd, duration_ms/$ai_latency, status, tool_calls, successful_tools, failed_tools, tools, tools_truncated, delegated, delegation_protocol, caller_app, delegation_task_id, a2a_task_id, parent_run_id, parent_turn_id, and error_message/$ai_error. Agent Teams child runs use delegation_protocol = 'agent-team' and retain their own run_id while linking to the launching run through parent_run_id. The bounded tools array contains names, relative start times, durations, statuses, and coarse error classes only, never args or results; failed runs and interrupted tools remain queryable. session_recordings columns include id, session_id, user_id, anonymous_id, user_key, started_at, ended_at, duration_ms, chunk_count, event_count, page_count, error_count, rage_click_count, app, template, status, first_url, last_url, path, hostname, referrer, and metadata.",
19
19
  schema: z.object({
20
20
  sql: z
21
21
  .string()
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-05
4
+ ---
5
+
6
+ Historical analytics events now populate rollups, and new events update raw data and rollups atomically.
@@ -18,7 +18,6 @@ import {
18
18
  type DashboardReportCaptureOutcome,
19
19
  type DashboardReportSubscription,
20
20
  } from "./dashboard-report-subscriptions";
21
- import { ANALYTICS_DASHBOARD_REPORT_EMAIL_ID } from "./emails";
22
21
 
23
22
  const DATE_FILTER_TYPES: ReadonlySet<FilterType> = new Set([
24
23
  "date",
@@ -327,7 +326,6 @@ export async function sendDashboardReportSubscription(
327
326
  ? { attachments: rendered.attachments }
328
327
  : {}),
329
328
  timeoutMs: emailTimeoutMs,
330
- templateId: ANALYTICS_DASHBOARD_REPORT_EMAIL_ID,
331
329
  });
332
330
  }
333
331
 
@@ -73,12 +73,12 @@ function stableId(prefix: string, parts: readonly string[]): string {
73
73
  }
74
74
 
75
75
  /**
76
- * Upsert compact rollups after the corresponding raw analytics rows commit.
77
- * The caller owns the raw insert; this function deliberately does not read or
78
- * write analytics_events, so ingestion can choose its own failure boundary.
76
+ * Upsert compact rollups for a normalized batch. When ingestion passes its
77
+ * transaction through, raw events and rollups share one commit boundary.
79
78
  */
80
79
  export async function upsertFirstPartyAnalyticsRollups(
81
80
  rows: readonly NormalizedFirstPartyAnalyticsEventRow[],
81
+ transaction?: any,
82
82
  ): Promise<FirstPartyAnalyticsRollupResult> {
83
83
  const dailyRollups = new Map<string, DailyRollupRow>();
84
84
  const userDays = new Map<string, UserDayRow>();
@@ -140,8 +140,7 @@ export async function upsertFirstPartyAnalyticsRollups(
140
140
  };
141
141
  }
142
142
 
143
- const db = getDb() as any;
144
- await db.transaction(async (tx: any) => {
143
+ const writeRollups = async (tx: any) => {
145
144
  const dailyRows = [...dailyRollups.values()];
146
145
  await tx
147
146
  .insert(schema.analyticsEventDailyRollups)
@@ -172,7 +171,13 @@ export async function upsertFirstPartyAnalyticsRollups(
172
171
  ],
173
172
  });
174
173
  }
175
- });
174
+ };
175
+
176
+ if (transaction) {
177
+ await writeRollups(transaction);
178
+ } else {
179
+ await (getDb() as any).transaction(writeRollups);
180
+ }
176
181
 
177
182
  return {
178
183
  eventCount: rows.length,
@@ -505,24 +505,14 @@ export async function recordAnalyticsEvents(
505
505
  });
506
506
 
507
507
  if (rows.length) {
508
- await db.insert(schema.analyticsEvents).values(rows);
509
- await db
510
- .update(schema.analyticsPublicKeys)
511
- .set({ lastUsedAt: receivedAt })
512
- .where(eq(schema.analyticsPublicKeys.id, key.id));
513
-
514
- try {
515
- await upsertFirstPartyAnalyticsRollups(rows);
516
- } catch (error) {
517
- // Raw events are the durable ingest record. Keep /track available when
518
- // a rollup write is temporarily unavailable; the warning preserves the
519
- // failure signal for operators without turning a successful raw ingest
520
- // into a client retry and duplicate event batch.
521
- console.warn(
522
- "[first-party-analytics] Rollup update failed after raw ingest:",
523
- error,
524
- );
525
- }
508
+ await db.transaction(async (tx: any) => {
509
+ await tx.insert(schema.analyticsEvents).values(rows);
510
+ await tx
511
+ .update(schema.analyticsPublicKeys)
512
+ .set({ lastUsedAt: receivedAt })
513
+ .where(eq(schema.analyticsPublicKeys.id, key.id));
514
+ await upsertFirstPartyAnalyticsRollups(rows, tx);
515
+ });
526
516
  }
527
517
 
528
518
  // Fork captured exceptions into the dedicated error-capture tables. This is
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  ensureAdditiveColumns,
3
+ getDialect,
3
4
  getDbExec,
5
+ isPostgres,
4
6
  runMigrations,
5
7
  } from "@agent-native/core/db";
6
8
 
@@ -28,6 +30,169 @@ function isDrizzleTable(value: unknown): value is object {
28
30
 
29
31
  const schemaTables = Object.values(schema).filter(isDrizzleTable);
30
32
 
33
+ const ANALYTICS_ROLLUP_BACKFILL_STATEMENTS = {
34
+ postgres: [
35
+ `
36
+ INSERT INTO analytics_event_daily_rollups (
37
+ id, tenant_key, owner_email, org_id, event_date, event_name,
38
+ app, template, event_count
39
+ )
40
+ SELECT
41
+ md5(random()::text || clock_timestamp()::text), tenant_key,
42
+ MIN(owner_email), MIN(org_id), event_date, event_name, app, template,
43
+ COUNT(*)::INTEGER
44
+ FROM (
45
+ SELECT
46
+ CASE
47
+ WHEN org_id IS NOT NULL AND org_id <> '' THEN 'org:' || org_id
48
+ ELSE 'user:' || owner_email
49
+ END AS tenant_key,
50
+ owner_email,
51
+ org_id,
52
+ COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)) AS event_date,
53
+ event_name,
54
+ COALESCE(app, '') AS app,
55
+ COALESCE(template, '') AS template
56
+ FROM analytics_events
57
+ ) AS historical_events
58
+ WHERE event_date <> ''
59
+ GROUP BY tenant_key, event_date, event_name, app, template
60
+ ON CONFLICT (tenant_key, event_date, event_name, app, template)
61
+ DO UPDATE SET event_count = GREATEST(
62
+ analytics_event_daily_rollups.event_count,
63
+ EXCLUDED.event_count
64
+ )
65
+ `,
66
+ `
67
+ INSERT INTO analytics_user_days (
68
+ id, tenant_key, owner_email, org_id, event_date, user_key
69
+ )
70
+ SELECT
71
+ md5(random()::text || clock_timestamp()::text), tenant_key,
72
+ owner_email, org_id, event_date, user_key
73
+ FROM (
74
+ SELECT DISTINCT
75
+ CASE
76
+ WHEN org_id IS NOT NULL AND org_id <> '' THEN 'org:' || org_id
77
+ ELSE 'user:' || owner_email
78
+ END AS tenant_key,
79
+ owner_email,
80
+ org_id,
81
+ COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)) AS event_date,
82
+ user_key
83
+ FROM analytics_events
84
+ WHERE user_key IS NOT NULL AND TRIM(user_key) <> ''
85
+ ) AS historical_user_days
86
+ WHERE event_date <> ''
87
+ ON CONFLICT (tenant_key, event_date, user_key) DO NOTHING
88
+ `,
89
+ ],
90
+ sqlite: [
91
+ `
92
+ INSERT INTO analytics_event_daily_rollups (
93
+ id, tenant_key, owner_email, org_id, event_date, event_name,
94
+ app, template, event_count
95
+ )
96
+ SELECT
97
+ lower(hex(randomblob(16))), tenant_key, MIN(owner_email), MIN(org_id),
98
+ event_date, event_name, app, template, COUNT(*)
99
+ FROM (
100
+ SELECT
101
+ CASE
102
+ WHEN org_id IS NOT NULL AND org_id <> '' THEN 'org:' || org_id
103
+ ELSE 'user:' || owner_email
104
+ END AS tenant_key,
105
+ owner_email,
106
+ org_id,
107
+ COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)) AS event_date,
108
+ event_name,
109
+ COALESCE(app, '') AS app,
110
+ COALESCE(template, '') AS template
111
+ FROM analytics_events
112
+ ) AS historical_events
113
+ WHERE event_date <> ''
114
+ GROUP BY tenant_key, event_date, event_name, app, template
115
+ ON CONFLICT (tenant_key, event_date, event_name, app, template)
116
+ DO UPDATE SET event_count = excluded.event_count
117
+ `,
118
+ `
119
+ INSERT INTO analytics_user_days (
120
+ id, tenant_key, owner_email, org_id, event_date, user_key
121
+ )
122
+ SELECT
123
+ lower(hex(randomblob(16))), tenant_key, owner_email, org_id,
124
+ event_date, user_key
125
+ FROM (
126
+ SELECT DISTINCT
127
+ CASE
128
+ WHEN org_id IS NOT NULL AND org_id <> '' THEN 'org:' || org_id
129
+ ELSE 'user:' || owner_email
130
+ END AS tenant_key,
131
+ owner_email,
132
+ org_id,
133
+ COALESCE(NULLIF(event_date, ''), substr(timestamp, 1, 10)) AS event_date,
134
+ user_key
135
+ FROM analytics_events
136
+ WHERE user_key IS NOT NULL AND TRIM(user_key) <> ''
137
+ ) AS historical_user_days
138
+ WHERE event_date <> ''
139
+ ON CONFLICT (tenant_key, event_date, user_key) DO NOTHING
140
+ `,
141
+ ],
142
+ } as const;
143
+
144
+ async function runHistoricalAnalyticsRollupBackfill(): Promise<void> {
145
+ const db = getDbExec();
146
+
147
+ if (isPostgres()) {
148
+ if (!db.transaction) {
149
+ throw new Error(
150
+ "Analytics rollup backfill requires a Postgres transaction",
151
+ );
152
+ }
153
+
154
+ await db.transaction(async (tx) => {
155
+ // Only one serverless instance should perform the expensive scan. A
156
+ // try-lock is intentional: competing cold starts fail fast instead of
157
+ // holding pooled Neon connections while waiting for the winner. The
158
+ // monotonic rollup upsert below keeps a live increment from being
159
+ // overwritten if ingest commits during the historical snapshot.
160
+ const lockResult = await tx.execute({
161
+ sql: "SELECT pg_try_advisory_xact_lock(hashtextextended(?, 0::bigint)) AS acquired",
162
+ args: ["agent-native:analytics-rollup-backfill"],
163
+ });
164
+ const acquired = lockResult.rows[0]?.acquired;
165
+ if (acquired !== true && acquired !== "t") {
166
+ throw new Error(
167
+ "Analytics rollup backfill is already running on another instance",
168
+ );
169
+ }
170
+ for (const statement of ANALYTICS_ROLLUP_BACKFILL_STATEMENTS.postgres) {
171
+ await tx.execute(statement);
172
+ }
173
+ });
174
+ return;
175
+ }
176
+
177
+ const statements = ANALYTICS_ROLLUP_BACKFILL_STATEMENTS.sqlite;
178
+ if (getDialect() === "d1") {
179
+ if (!db.atomicBatch) {
180
+ throw new Error("D1 analytics rollup backfill requires an atomic batch");
181
+ }
182
+ await db.atomicBatch(statements);
183
+ return;
184
+ }
185
+
186
+ if (!db.transaction) {
187
+ throw new Error(
188
+ "Analytics rollup backfill requires a database transaction",
189
+ );
190
+ }
191
+ await db.transaction(async (tx) => {
192
+ for (const statement of statements) await tx.execute(statement);
193
+ });
194
+ }
195
+
31
196
  // Convention: every new migration below MUST set a unique `name:` slug (see
32
197
  // packages/core/src/db/migrations.ts for the full rationale). Version numbers
33
198
  // alone are not a safe identity across parallel branches that each extend
@@ -1367,6 +1532,15 @@ const runAnalyticsMigrations = runMigrations(
1367
1532
  name: "analytics-query-pressure-daily-key-idx",
1368
1533
  sql: `CREATE UNIQUE INDEX IF NOT EXISTS analytics_query_pressure_daily_key_idx ON analytics_query_pressure_daily (tenant_key, event_date, query_class)`,
1369
1534
  },
1535
+ // Rebuild the compact analytics rollups from raw history once when the
1536
+ // tables first ship. The run-only migration takes the same write lock as
1537
+ // ingest so a live increment cannot be overwritten by its snapshot.
1538
+ {
1539
+ version: 132,
1540
+ name: "analytics-rollups-historical-backfill",
1541
+ sql: {},
1542
+ run: runHistoricalAnalyticsRollupBackfill,
1543
+ },
1370
1544
  ],
1371
1545
  { table: "analytics_migrations" },
1372
1546
  );
@@ -112,48 +112,48 @@ export function OutOfOfficeEvent({
112
112
  }}
113
113
  />
114
114
  </div>
115
- <div
116
- data-out-of-office-trigger={event.id}
117
- className={`pointer-events-auto absolute ${
118
- isBeingDragged && isDragging ? "z-[100]" : "z-40"
119
- }`}
120
- style={{
121
- top: `${top + 4}px`,
122
- right: `${4 + (compactMarker ? markerIndex * 24 : 0)}px`,
123
- left: compactMarker ? undefined : `${4 + markerIndex * 12}px`,
124
- }}
115
+ <EventDetailPopover
116
+ event={event}
117
+ onDelete={onDelete}
118
+ isDraft={isDraft}
119
+ defaultOpen={defaultOpen}
120
+ onTitleSave={onTitleSave}
121
+ onDismissNew={onDismissNew}
122
+ onDraftUpdate={onDraftUpdate}
123
+ onDraftCreate={onDraftCreate}
124
+ onDraftDiscard={onDraftDiscard}
125
+ onOpenChange={onOpenChange}
125
126
  >
126
- <EventDetailPopover
127
- event={event}
128
- onDelete={onDelete}
129
- isDraft={isDraft}
130
- defaultOpen={defaultOpen}
131
- onTitleSave={onTitleSave}
132
- onDismissNew={onDismissNew}
133
- onDraftUpdate={onDraftUpdate}
134
- onDraftCreate={onDraftCreate}
135
- onDraftDiscard={onDraftDiscard}
136
- onOpenChange={onOpenChange}
137
- >
138
- <button
139
- onPointerDown={(pointerEvent) =>
140
- onMovePointerDown?.(pointerEvent, startsOnDay)
127
+ <button
128
+ type="button"
129
+ data-out-of-office-trigger={event.id}
130
+ onPointerDown={(pointerEvent) =>
131
+ onMovePointerDown?.(pointerEvent, startsOnDay)
132
+ }
133
+ onClick={(clickEvent) => {
134
+ if (shouldSuppressClick?.()) {
135
+ clickEvent.preventDefault();
136
+ clickEvent.stopPropagation();
141
137
  }
142
- onClick={(clickEvent) => {
143
- if (shouldSuppressClick?.()) {
144
- clickEvent.preventDefault();
145
- clickEvent.stopPropagation();
146
- }
147
- }}
148
- className={`flex h-5 max-w-full items-center truncate rounded-sm text-[10px] font-medium text-foreground outline-none transition-[filter,box-shadow] hover:brightness-110 focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 ${
138
+ }}
139
+ className={`pointer-events-auto absolute inset-x-0 block border-0 bg-transparent p-0 text-left outline-none ${
140
+ canManipulate ? "cursor-grab" : ""
141
+ } ${isBeingDragged && isDragging ? "cursor-grabbing" : ""}`}
142
+ aria-label={`${label}: ${title}`}
143
+ style={{ top: `${top}px`, height: `${height}px` }}
144
+ >
145
+ <span
146
+ className={`pointer-events-auto absolute top-1 z-40 flex h-5 max-w-full items-center truncate rounded-sm text-[10px] font-medium text-foreground outline-none transition-[filter,box-shadow] hover:brightness-110 focus-within:ring-1 focus-within:ring-ring focus-within:ring-offset-1 ${
147
+ isBeingDragged && isDragging ? "z-[100] shadow-lg" : ""
148
+ } ${
149
149
  compactMarker
150
150
  ? "w-5 justify-center px-0"
151
151
  : "gap-1 px-1.5 text-left"
152
- } ${canManipulate ? "cursor-grab" : ""} ${
153
- isBeingDragged && isDragging ? "cursor-grabbing shadow-lg" : ""
154
152
  }`}
155
- aria-label={`${label}: ${title}`}
153
+ aria-hidden="true"
156
154
  style={{
155
+ right: compactMarker ? `${4 + markerIndex * 24}px` : undefined,
156
+ left: compactMarker ? undefined : `${4 + markerIndex * 12}px`,
157
157
  backgroundColor: `color-mix(in srgb, ${color} 20%, hsl(var(--background)))`,
158
158
  boxShadow: `0 0 0 1px color-mix(in srgb, ${color} 34%, transparent)`,
159
159
  }}
@@ -164,9 +164,9 @@ export function OutOfOfficeEvent({
164
164
  style={{ color }}
165
165
  />
166
166
  {!compactMarker && <span className="truncate">{title}</span>}
167
- </button>
168
- </EventDetailPopover>
169
- </div>
167
+ </span>
168
+ </button>
169
+ </EventDetailPopover>
170
170
  {canManipulate && (
171
171
  <div
172
172
  data-resize-handle="true"
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-05
4
+ ---
5
+
6
+ Out-of-office blocks now open from their scheduled time range instead of creating a new event.