@agent-native/core 0.154.4 → 0.155.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 (207) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/account-health/SKILL.md +76 -0
  3. package/corpus/templates/analytics/.agents/skills/bigquery/SKILL.md +3 -0
  4. package/corpus/templates/analytics/.agents/skills/cross-source-analysis/SKILL.md +2 -1
  5. package/corpus/templates/analytics/.agents/skills/data-querying/SKILL.md +6 -5
  6. package/corpus/templates/analytics/AGENTS.md +3 -0
  7. package/corpus/templates/analytics/actions/account-deep-dive.ts +3 -2
  8. package/corpus/templates/analytics/actions/bigquery.ts +1 -1
  9. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +4 -1
  10. package/corpus/templates/analytics/app/components/layout/NewDashboardDialog.tsx +1 -0
  11. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyCombobox.tsx +12 -4
  12. package/corpus/templates/analytics/app/pages/adhoc/explorer/components/PropertyValueCombobox.tsx +5 -2
  13. package/corpus/templates/analytics/app/pages/adhoc/explorer/property-label.ts +32 -0
  14. package/corpus/templates/analytics/changelog/2026-08-13-account-health-readouts-verify-customer-scope-and-contract-metrics.md +7 -0
  15. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +43 -17
  16. package/corpus/templates/analytics/server/lib/analytics-query-catalog.ts +17 -0
  17. package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
  18. package/corpus/templates/calendar/AGENTS.md +2 -0
  19. package/corpus/templates/calendar/DEVELOPING.md +4 -4
  20. package/corpus/templates/calendar/actions/get-settings.ts +4 -11
  21. package/corpus/templates/calendar/actions/update-availability.ts +15 -4
  22. package/corpus/templates/calendar/actions/update-settings.ts +15 -4
  23. package/corpus/templates/calendar/app/components/booking/DatePicker.tsx +9 -3
  24. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +21 -19
  25. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +9 -6
  26. package/corpus/templates/calendar/app/components/calendar/FindTimePanel.tsx +25 -3
  27. package/corpus/templates/calendar/app/components/calendar/InlineEventPickers.tsx +10 -3
  28. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +9 -9
  29. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +11 -3
  30. package/corpus/templates/calendar/app/components/layout/Sidebar.tsx +10 -4
  31. package/corpus/templates/calendar/app/i18n-data.ts +30 -0
  32. package/corpus/templates/calendar/app/lib/availability-schedule.ts +53 -0
  33. package/corpus/templates/calendar/app/lib/event-form-utils.ts +5 -0
  34. package/corpus/templates/calendar/app/pages/AvailabilitySettings.tsx +90 -28
  35. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +135 -61
  36. package/corpus/templates/calendar/app/pages/BookingPage.tsx +2 -0
  37. package/corpus/templates/calendar/app/pages/CalendarView.tsx +30 -15
  38. package/corpus/templates/calendar/app/pages/Settings.tsx +60 -1
  39. package/corpus/templates/calendar/changelog/2026-08-13-find-a-time-now-closes-from-a-clear-header-action.md +6 -0
  40. package/corpus/templates/calendar/server/handlers/bookings.ts +2 -1
  41. package/corpus/templates/calendar/server/handlers/settings.ts +12 -16
  42. package/corpus/templates/calendar/shared/api.ts +1 -0
  43. package/corpus/templates/calendar/shared/availability-schedule.ts +64 -0
  44. package/corpus/templates/calendar/shared/calendar-week.ts +21 -0
  45. package/corpus/templates/calendar/shared/settings.ts +44 -0
  46. package/corpus/templates/clips/AGENTS.md +2 -0
  47. package/corpus/templates/clips/actions/add-comment.ts +5 -2
  48. package/corpus/templates/clips/actions/finalize-recording.ts +31 -1
  49. package/corpus/templates/clips/actions/get-recording-player-data.ts +9 -0
  50. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +21 -1
  51. package/corpus/templates/clips/actions/reply-to-comment.ts +5 -2
  52. package/corpus/templates/clips/actions/update-comment.ts +9 -2
  53. package/corpus/templates/clips/app/components/library/search-bar.tsx +26 -13
  54. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +135 -3
  55. package/corpus/templates/clips/app/components/player/comments-panel.tsx +5 -99
  56. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +7 -3
  57. package/corpus/templates/clips/app/components/player/video-player.tsx +84 -24
  58. package/corpus/templates/clips/app/components/workspace/notifications-list.tsx +11 -3
  59. package/corpus/templates/clips/app/i18n/en-US.ts +12 -0
  60. package/corpus/templates/clips/app/lib/public-meeting.ts +11 -3
  61. package/corpus/templates/clips/app/lib/search-focus.ts +15 -0
  62. package/corpus/templates/clips/app/root.tsx +2 -1
  63. package/corpus/templates/clips/app/routes/_app.meetings._index.tsx +94 -26
  64. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +24 -7
  65. package/corpus/templates/clips/app/routes/embed.$shareId.tsx +8 -4
  66. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +14 -7
  67. package/corpus/templates/clips/app/routes/share.$shareId.tsx +8 -4
  68. package/corpus/templates/clips/app/routes/share.meeting.$meetingId.tsx +72 -26
  69. package/corpus/templates/clips/changelog/2026-08-13-inline-markdown-comments.md +6 -0
  70. package/corpus/templates/clips/changelog/2026-08-13-private-meeting-agent-links.md +6 -0
  71. package/corpus/templates/clips/desktop/src/app.tsx +116 -19
  72. package/corpus/templates/clips/desktop/src/lib/recorder.ts +13 -29
  73. package/corpus/templates/clips/desktop/src/lib/recording-request.ts +39 -0
  74. package/corpus/templates/clips/desktop/src/lib/settings-navigation.ts +11 -0
  75. package/corpus/templates/clips/desktop/src/lib/whisper-model-picker.ts +7 -0
  76. package/corpus/templates/clips/desktop/src/styles.css +117 -0
  77. package/corpus/templates/clips/server/db/index.ts +10 -0
  78. package/corpus/templates/clips/server/db/schema.ts +1 -0
  79. package/corpus/templates/clips/server/lib/recordings.ts +3 -1
  80. package/corpus/templates/clips/server/lib/seekable-media-state.ts +90 -0
  81. package/corpus/templates/clips/server/plugins/db.ts +5 -0
  82. package/corpus/templates/clips/server/routes/api/public-meeting.get.ts +37 -4
  83. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +16 -5
  84. package/corpus/templates/clips/shared/clips-ai-prefs.ts +3 -0
  85. package/corpus/templates/clips/shared/meeting-agent-access.ts +5 -0
  86. package/corpus/templates/content/.agents/skills/content/SKILL.md +2 -2
  87. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +20 -4
  88. package/corpus/templates/content/actions/_database-row-mutation.ts +1490 -0
  89. package/corpus/templates/content/actions/_database-utils.ts +44 -0
  90. package/corpus/templates/content/actions/_property-utils.ts +4 -1
  91. package/corpus/templates/content/actions/add-comment.ts +2 -1
  92. package/corpus/templates/content/actions/add-database-item.ts +62 -240
  93. package/corpus/templates/content/actions/configure-document-property.ts +165 -1
  94. package/corpus/templates/content/actions/delete-document-property.ts +18 -0
  95. package/corpus/templates/content/actions/set-document-property.ts +78 -1
  96. package/corpus/templates/content/actions/update-comment.ts +1 -1
  97. package/corpus/templates/content/actions/update-database-item.ts +62 -0
  98. package/corpus/templates/content/actions/upsert-database-item-by-key.ts +43 -796
  99. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +24 -15
  100. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +10 -8
  101. package/corpus/templates/content/app/hooks/use-content-database.ts +33 -32
  102. package/corpus/templates/content/changelog/2026-08-10-database-row-actions-now-validate-exact-schemas-and-safe-ret.md +6 -0
  103. package/corpus/templates/content/parity/matrix.md +1 -1
  104. package/corpus/templates/content/parity/matrix.ts +2 -0
  105. package/corpus/templates/content/server/db/schema.ts +39 -3
  106. package/corpus/templates/content/server/plugins/db.ts +30 -0
  107. package/corpus/templates/content/shared/api.ts +75 -2
  108. package/corpus/templates/design/actions/generate-design.ts +10 -7
  109. package/corpus/templates/design/actions/present-design-variants.ts +35 -6
  110. package/corpus/templates/design/actions/run-design-audit.ts +33 -0
  111. package/corpus/templates/design/changelog/2026-08-13-the-design-audit-now-catches-a-screen-that-renders-behind-an.md +6 -0
  112. package/corpus/templates/design/shared/design-review.ts +1 -0
  113. package/corpus/templates/design/shared/html-integrity.ts +66 -0
  114. package/corpus/templates/dispatch/.env.example +5 -0
  115. package/corpus/templates/dispatch/AGENTS.md +24 -19
  116. package/corpus/templates/dispatch/app/i18n-data.ts +20 -0
  117. package/corpus/templates/dispatch/server/lib/identity-sso.ts +21 -90
  118. package/corpus/templates/dispatch/server/plugins/identity-sso.ts +3 -12
  119. package/corpus/templates/dispatch/shared/feature-flags.ts +4 -0
  120. package/corpus/templates/factory/AGENTS.md +5 -1
  121. package/corpus/templates/factory/README.md +23 -0
  122. package/corpus/templates/factory/actions/view-screen.ts +17 -0
  123. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +239 -0
  124. package/corpus/templates/factory/app/components/factory/FactoryCanvas.tsx +2 -0
  125. package/corpus/templates/factory/app/components/factory/FactoryInspector.tsx +130 -7
  126. package/corpus/templates/factory/app/i18n/en-US.ts +20 -0
  127. package/corpus/templates/factory/app/routes/factory.tsx +41 -0
  128. package/corpus/templates/factory/package.json +1 -0
  129. package/corpus/templates/factory/server/factory-graph/contracts.ts +2 -0
  130. package/corpus/templates/factory/server/plugins/agent-chat.ts +9 -0
  131. package/corpus/templates/factory/server/plugins/dispatch-db.ts +1 -0
  132. package/corpus/templates/factory/server/plugins/setup-dispatch.ts +3 -0
  133. package/corpus/templates/mail/app/lib/native-mcp-exclusions.ts +3 -0
  134. package/corpus/templates/mail/vite.config.ts +7 -0
  135. package/corpus/templates/plan/actions/reply-to-plan-comment.ts +7 -2
  136. package/corpus/templates/plan/actions/resolve-plan-comment.ts +2 -2
  137. package/corpus/templates/plan/actions/update-visual-plan.ts +1 -1
  138. package/corpus/templates/plan/app/pages/PlansPage.tsx +29 -20
  139. package/corpus/templates/slides/actions/add-slide-comment.ts +1 -1
  140. package/corpus/templates/slides/actions/update-slide-comment.ts +1 -1
  141. package/corpus/templates/slides/app/components/comments/SlideCommentsPanel.tsx +9 -6
  142. package/dist/action.d.ts +22 -0
  143. package/dist/action.js +27 -0
  144. package/dist/agent/production-agent.js +5 -1
  145. package/dist/agent/thread-data-builder.js +17 -1
  146. package/dist/cli/skills-content/help.js +6 -4
  147. package/dist/cli/skills.js +17 -2
  148. package/dist/cli/workspacify.d.ts +5 -0
  149. package/dist/cli/workspacify.js +65 -8
  150. package/dist/client/AgentPanel.d.ts +5 -0
  151. package/dist/client/AgentPanel.js +20 -4
  152. package/dist/client/AssistantChat.d.ts +21 -0
  153. package/dist/client/AssistantChat.js +66 -68
  154. package/dist/client/active-run-state.d.ts +14 -1
  155. package/dist/client/active-run-state.js +0 -0
  156. package/dist/client/agent-chat-adapter.js +107 -5
  157. package/dist/client/analytics.js +9 -2
  158. package/dist/client/chat/tool-call-display.js +4 -4
  159. package/dist/client/chat-first/apps-rail.js +9 -4
  160. package/dist/client/chat-first/primary-nav.d.ts +2 -1
  161. package/dist/client/chat-first/primary-nav.js +3 -3
  162. package/dist/client/chat-first/types.d.ts +5 -1
  163. package/dist/client/markdown/InlineMarkdown.d.ts +23 -0
  164. package/dist/client/markdown/InlineMarkdown.js +65 -0
  165. package/dist/client/markdown/index.d.ts +1 -0
  166. package/dist/client/markdown/index.js +1 -0
  167. package/dist/client/review/ReviewThreadPanel.js +2 -1
  168. package/dist/client/run-stream-ownership.d.ts +39 -0
  169. package/dist/client/run-stream-ownership.js +65 -0
  170. package/dist/collab/routes.d.ts +1 -1
  171. package/dist/collab/struct-routes.d.ts +1 -1
  172. package/dist/feature-flags/index.d.ts +1 -1
  173. package/dist/feature-flags/index.js +1 -1
  174. package/dist/feature-flags/store.d.ts +10 -0
  175. package/dist/feature-flags/store.js +112 -1
  176. package/dist/guards/no-env-credentials.js +4 -0
  177. package/dist/index.browser.d.ts +1 -1
  178. package/dist/index.browser.js +1 -1
  179. package/dist/index.d.ts +2 -2
  180. package/dist/index.js +2 -2
  181. package/dist/integrations/adapters/slack.js +5 -1
  182. package/dist/localization/default-messages.d.ts +1 -0
  183. package/dist/localization/default-messages.js +1 -0
  184. package/dist/notifications/routes.d.ts +1 -1
  185. package/dist/observability/routes.d.ts +1 -1
  186. package/dist/progress/routes.d.ts +1 -1
  187. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  188. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  189. package/dist/resources/agents.js +40 -5
  190. package/dist/resources/handlers.d.ts +1 -1
  191. package/dist/resources/metadata.d.ts +10 -0
  192. package/dist/resources/metadata.js +7 -3
  193. package/dist/review/actions/create-review-comment.js +1 -1
  194. package/dist/review/actions/reply-review-comment.js +1 -1
  195. package/dist/review/actions/resolve-review-thread.js +1 -1
  196. package/dist/server/action-routes.js +42 -8
  197. package/dist/server/agent-discovery.js +14 -3
  198. package/dist/server/core-routes-plugin.js +1 -1
  199. package/dist/server/identity-sso-migrations.d.ts +7 -0
  200. package/dist/server/identity-sso-migrations.js +36 -0
  201. package/dist/server/identity-sso-store.d.ts +3 -2
  202. package/dist/server/identity-sso-store.js +8 -3
  203. package/dist/server/realtime-token.d.ts +1 -1
  204. package/dist/server/release-migrations.js +4 -0
  205. package/dist/tracking/providers.js +13 -3
  206. package/dist/workspace-files/tool.js +1 -1
  207. package/package.json +3 -2
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: 8212
34
+ - template files: 8236
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: account-health
3
+ description: >-
4
+ How to produce a trustworthy named-customer account health, QBR, renewal, or
5
+ contract-usage readout. Use when a prompt names an organization or account.
6
+ ---
7
+
8
+ # Account Health
9
+
10
+ Use these checks for named customer, organization, account-health, QBR,
11
+ renewal, risk, expansion, contract-utilization, or adoption questions. This is
12
+ not an ordinary metric lookup: a wrong identity or metric definition can put
13
+ another customer's data into a customer-facing decision.
14
+
15
+ ## Identity lock
16
+
17
+ Treat an organization ID supplied in a prompt as a lookup key, not as proof that
18
+ the rows returned by a warehouse query belong to that customer.
19
+
20
+ 1. Resolve the ID through the canonical CRM or contract source first.
21
+ 2. Record the resolved customer name, organization ID, root organization ID,
22
+ and any canonical account key.
23
+ 3. Carry the same resolved identity through every usage query and check the
24
+ returned rows against it.
25
+ 4. Stop and report an identity mismatch if a result contains another customer,
26
+ mixed organization IDs, or an unresolved ID. Do not summarize the rows or
27
+ silently choose the most plausible customer.
28
+
29
+ ## Source order and completeness
30
+
31
+ 1. Use the catalog and organization data dictionary before writing warehouse
32
+ SQL. Approved definitions and saved query shapes outrank a table name that
33
+ merely sounds right.
34
+ 2. Gather available CRM, contract, support, and conversation evidence. Treat
35
+ that context as complementary, not as a complete usage result.
36
+ 3. For account health, product usage is mandatory: query approved definitions
37
+ for each requested product or feature dimension separately, using an explicit
38
+ window (default 90 days for adoption unless the user asks for another one).
39
+ 4. Report source, window, filters, row counts, identity join, freshness, and
40
+ gaps. A missing source is a gap, not a reason to fill in a plausible value.
41
+
42
+ ## Metric contracts
43
+
44
+ - **Contract metric** - distinguish the contracted measure from similarly named
45
+ platform, web, content, or event metrics. Do not use a familiar synonym when
46
+ comparing against a contract.
47
+ - **Completed-month usage** - “last month” excludes the current partial month.
48
+ Verify the month/as-of field and freshness before using a reporting table;
49
+ a single current-month snapshot is not last month's completed value.
50
+ - **Seats** - use the total distinct contracted/eligible users for the stated
51
+ period when comparing with a seat contract. Do not substitute DAU, WAU, or a
52
+ point-in-time daily active count, and label active users separately.
53
+ - **Capacity usage** - report actual usage, contracted capacity, and utilization.
54
+ Explicitly flag utilization at or above 100% as an overage risk and potential
55
+ renewal/expansion signal.
56
+ - **Adoption** - keep separate products or feature dimensions separate. Include
57
+ feature adoption, active-user/session counts, and last activity only when the
58
+ source query returns them; absence claims require complete coverage for the
59
+ stated window.
60
+
61
+ ## Known source traps
62
+
63
+ - If a saved query or dictionary entry is marked deprecated or retired, do not
64
+ run it; use the current approved replacement and note the stale reference.
65
+ - A source that returns only one current partial-period snapshot is not proof of
66
+ a completed-period value. Verify the period/as-of field and freshness first.
67
+ - If the dictionary does not define the requested contract metric or the
68
+ replacement source cannot be verified, ask or report the gap instead of
69
+ guessing from a similarly named table.
70
+
71
+ ## Answer shape
72
+
73
+ Lead with the identity check, then provide a compact table of contract versus
74
+ actual usage, followed by product/feature adoption signals, account context,
75
+ risks, and next steps. Make every number traceable to one source and explicitly
76
+ say when the readout is partial.
@@ -69,6 +69,9 @@ answers the question.
69
69
  -- Or for TIMESTAMP columns: WHERE event_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
70
70
  ```
71
71
 
72
+ For account health, verify the reporting period, as-of field, and freshness.
73
+ Never label a current partial-month snapshot as last month's completed usage.
74
+
72
75
  ## Actions
73
76
 
74
77
  | Action | Use |
@@ -29,7 +29,8 @@ For any question about a specific customer account, follow this source order:
29
29
  Not every source is needed for every question — start with the primary source
30
30
  and add secondary sources only when the question requires them. But do not
31
31
  stop at one source for a "deep dive" — at minimum pull CRM + Gong + product
32
- usage for any account health question.
32
+ usage for any account health question. Read `account-health` for the identity
33
+ lock and contract-metric rules before querying the warehouse.
33
34
 
34
35
  ## 1. Plan Before You Query (Catalog-First)
35
36
 
@@ -12,11 +12,12 @@ The analytics app connects to multiple data sources. This skill covers general p
12
12
  ## Approach
13
13
 
14
14
  0. **Orient catalog-first** — before querying, consult what already exists: the injected `<data-dictionary>` and data-source status tell you which sources are configured and which table/columns/join paths to use. Use them to pick the one source that owns the fact instead of fanning out blind queries.
15
- 1. **Read the relevant provider skill first** — check `.agents/skills/<provider>/SKILL.md` for table names, column mappings, auth, and gotchas. For BigQuery, read `.agents/skills/bigquery/SKILL.md` and use `search-bigquery-schema` before guessing table or column names.
16
- 2. **Clarify if ambiguous** — if the metric definition, date range, or grain is unclear and a wrong guess would change the numbers, use the `ask-question` clarifying tool (multiple-choice) before querying. Ask at most once per turn; skip it when the dictionary or the user already answered.
17
- 3. **Use existing actions or connected provider MCP tools** — call the provider action/tool with structured arguments, then filter or aggregate the returned records in your answer
18
- 4. **Write ad-hoc scripts** — if no existing script covers the question, create one in `actions/`
19
- 5. **Present data in chat** — don't just say "check the dashboard" — actually query, get the data, and present it. Only present numbers you actually retrieved; never report a value you did not query.
15
+ 1. **Route named account health deliberately** — for a customer/org health, QBR, renewal, contract-utilization, risk, or adoption request, read `account-health` before writing SQL. It adds identity-lock and metric-definition checks that an ordinary lookup does not need.
16
+ 2. **Read the relevant provider skill first** — check `.agents/skills/<provider>/SKILL.md` for table names, column mappings, auth, and gotchas. For BigQuery, read `.agents/skills/bigquery/SKILL.md` and use `search-bigquery-schema` before guessing table or column names.
17
+ 3. **Clarify if ambiguous** — if the metric definition, date range, or grain is unclear and a wrong guess would change the numbers, use the `ask-question` clarifying tool (multiple-choice) before querying. Ask at most once per turn; skip it when the dictionary or the user already answered.
18
+ 4. **Use existing actions or connected provider MCP tools** — call the provider action/tool with structured arguments, then filter or aggregate the returned records in your answer
19
+ 5. **Write ad-hoc scripts** — if no existing script covers the question, create one in `actions/`
20
+ 6. **Present data in chat** — don't just say "check the dashboard" — actually query, get the data, and present it. Only present numbers you actually retrieved; never report a value you did not query.
20
21
 
21
22
  For events recorded by the analytics template itself via its `/track` endpoint, use `pnpm action query-agent-native-analytics --sql "SELECT ... FROM analytics_events ..."`. This includes pageviews, site/app traffic, template usage, app usage, and event counts collected by this analytics app. Pageviews and traffic can also live in GA4, BigQuery/warehouse tables, Mixpanel, PostHog, Amplitude, or another configured provider, so choose the source from the user's wording, connected-source status, existing dashboards, data dictionary, and user/org resources. Ask one concise clarification if multiple configured sources are plausible. Do not use `db-query` for data-source analysis; `db-query` is only for internal app tables and will confuse analytics questions. The shipped `agent-native-templates-first-party` SQL dashboard is the template engagement dashboard for the first-party collector source.
22
23
 
@@ -11,6 +11,8 @@ Read the relevant skill before deeper work:
11
11
  - `data-querying` for source inspection, SQL generation, result handling, and
12
12
  `/chart` embeds; `bigquery`, `hubspot`, `gong`, `prometheus` for provider
13
13
  specifics.
14
+ - `account-health` for named customer health, QBR, renewal, contract usage,
15
+ identity, and product adoption.
14
16
  - `cross-source-analysis` for questions spanning sources (identity stitching,
15
17
  de-duplication).
16
18
  - `dashboard-management` for dashboard/panel storage, layout, extensions,
@@ -64,6 +66,7 @@ Read the relevant skill before deeper work:
64
66
  suggest and wait otherwise. Scope them to the question, avoid decorative
65
67
  metrics, and never modify existing dashboards without a directive.
66
68
  - For named account/deal deep dives, call `account-deep-dive` first.
69
+ - For named account health, read `account-health` before querying.
67
70
  - When the user challenges coverage or asks why records are missing, rerun from
68
71
  the source cohort and include the updated answer directly — never claim a
69
72
  revision you didn't produce.
@@ -362,7 +362,7 @@ async function loadGongEvidence(options: {
362
362
 
363
363
  export default defineAction({
364
364
  description:
365
- "Build a Fusion-quality account/deal deep-dive evidence bundle from HubSpot and Gong in one bounded read-only call. Use this first for named account, customer, deal, opportunity, renewal, risk, or 'deep dive' prompts before synthesizing the answer.",
365
+ "Build a bounded CRM/Gong account or deal evidence bundle from HubSpot and Gong. Use this first for named account, customer, deal, opportunity, renewal, risk, or deep-dive prompts. This is not a complete account-health result: for account health, follow it with verified product-usage queries for the resolved customer using the account-health skill.",
366
366
  schema: z.object({
367
367
  query: z
368
368
  .string()
@@ -602,10 +602,11 @@ export default defineAction({
602
602
  emailCount: emails.length,
603
603
  gongCallCount: gong.calls.length,
604
604
  transcriptCount: gong.transcripts.length,
605
+ productUsageIncluded: false,
605
606
  gaps,
606
607
  },
607
608
  guidance:
608
- "Synthesize a Fusion-style deal deep dive from this evidence. Include: executive summary, company/deal overview, key contacts and roles, dated timeline, Gong conversation evidence with call dates/titles, current state and risk assessment, likely blockers, recommended next steps, and methodology/gaps. Attribute every claim to HubSpot or Gong evidence and distinguish customer statements from internal notes.",
609
+ "Synthesize a CRM/Gong deal deep dive from this evidence. Include: executive summary, company/deal overview, key contacts and roles, dated timeline, Gong conversation evidence with call dates/titles, current state and risk assessment, likely blockers, recommended next steps, and methodology/gaps. Attribute every claim to HubSpot or Gong evidence and distinguish customer statements from internal notes. This bundle does not include product usage, contract utilization, adoption, or cross-source identity validation; for account health, run the account-health checks and report those gaps until verified usage evidence is present.",
609
610
  };
610
611
  },
611
612
  });
@@ -69,7 +69,7 @@ function stopForBigQueryCancellation(): never {
69
69
 
70
70
  export default defineAction({
71
71
  description:
72
- "Query the user-configured BigQuery data warehouse. Use this when the user asks for warehouse SQL, BigQuery, or a data-dictionary metric/table that lives in BigQuery. If the user names a provider action such as Jira or Pylon, use that provider action first and do not use BigQuery unless the user explicitly asks for a warehouse copy. Pass standard SQL via the `sql` arg. Do NOT use `db-query` for warehouse data (it only reaches the app's own SQL database). If a query fails with a schema or SQL error (unknown dataset/table/column, syntax), treat it as a normal debugging signal: inspect the real schema with `search-bigquery-schema` (or query INFORMATION_SCHEMA), correct the query based on the error, and run it again — a few corrective attempts are expected. Surface the error to the user only if it still fails after a few attempts or is non-recoverable (missing credentials, permission, quota). Never rerun identical failing SQL, and never substitute made-up numbers for data you could not query.",
72
+ "Query the user-configured BigQuery data warehouse. Use this when the user asks for warehouse SQL, BigQuery, or a data-dictionary metric/table that lives in BigQuery. If the user names a provider action such as Jira or Pylon, use that provider action first and do not use BigQuery unless the user explicitly asks for a warehouse copy. For a named customer or organization ID, resolve the canonical CRM/contract identity first and verify the returned rows carry the same customer and org/root-org identifiers. For account health, distinguish completed-month usage from current partial snapshots, contract metrics from similarly named platform metrics, total distinct contracted users from DAU/WAU, and actual usage from contracted capacity. Pass standard SQL via the `sql` arg. Do NOT use `db-query` for warehouse data (it only reaches the app's own SQL database). If a query fails with a schema or SQL error (unknown dataset/table/column, syntax), treat it as a normal debugging signal: inspect the real schema with `search-bigquery-schema` (or query INFORMATION_SCHEMA), correct the query based on the error, and run it again — a few corrective attempts are expected. Surface the error to the user only if it still fails after a few attempts or is non-recoverable (missing credentials, permission, quota). Never rerun identical failing SQL, and never substitute made-up numbers for data you could not query.",
73
73
  schema: z.object({
74
74
  sql: z.string().describe("SQL query to execute"),
75
75
  }),
@@ -231,6 +231,9 @@ export function formatSqlChartError(error: unknown): string {
231
231
  if (/inactivity timeout|too much time has passed/i.test(readableMessage)) {
232
232
  return "This chart took too long to load. Try again.";
233
233
  }
234
+ if (/abort(?:ed|ing)?|signal is aborted/i.test(readableMessage)) {
235
+ return "This chart load was interrupted. Try again.";
236
+ }
234
237
  if (/internal server error/i.test(readableMessage)) {
235
238
  return "This chart could not be loaded. Try again.";
236
239
  }
@@ -1381,7 +1384,7 @@ export function SqlChart({
1381
1384
  className={`flex flex-1 flex-col items-center justify-center gap-3 px-4 ${placeholderPadY} ${placeholderMinH}`}
1382
1385
  role="alert"
1383
1386
  >
1384
- <p className="text-center text-sm text-red-400 break-all">
1387
+ <p className="text-center text-sm text-destructive break-words">
1385
1388
  {formatSqlChartError(error)}
1386
1389
  </p>
1387
1390
  <Button
@@ -79,6 +79,7 @@ export function NewDashboardDialog({
79
79
  <PromptComposer
80
80
  autoFocus
81
81
  disabled={isGenerating}
82
+ layoutVariant="compact"
82
83
  placeholder={t("dialogs.newDashboardPlaceholder")}
83
84
  draftScope="analytics:new-dashboard"
84
85
  onSubmit={handleSubmit}
@@ -19,6 +19,7 @@ import {
19
19
  import { Skeleton } from "@/components/ui/skeleton";
20
20
  import { cn } from "@/lib/utils";
21
21
 
22
+ import { formatExplorerPropertyLabel } from "../property-label";
22
23
  import { KNOWN_PROPERTIES, ENRICHED_PROPERTY_MAP } from "../types";
23
24
  import { useDynamicProperties } from "../use-dynamic-schema";
24
25
 
@@ -66,6 +67,11 @@ export function PropertyCombobox({
66
67
  setOpen(false);
67
68
  };
68
69
 
70
+ const displayLabel = useMemo(() => {
71
+ if (!value) return null;
72
+ return formatExplorerPropertyLabel(value);
73
+ }, [value]);
74
+
69
75
  return (
70
76
  <Popover open={open} onOpenChange={setOpen}>
71
77
  <PopoverTrigger asChild>
@@ -77,12 +83,12 @@ export function PropertyCombobox({
77
83
  size="sm"
78
84
  >
79
85
  <span className="truncate">
80
- {value || triggerLabel || t("explorer.selectProperty")}
86
+ {displayLabel || triggerLabel || t("explorer.selectProperty")}
81
87
  </span>
82
88
  <IconSelector className="ml-1 h-3 w-3 shrink-0 opacity-50" />
83
89
  </Button>
84
90
  </PopoverTrigger>
85
- <PopoverContent className="w-[280px] p-0" align="start">
91
+ <PopoverContent className="w-[320px] p-0" align="start">
86
92
  <Command shouldFilter={true}>
87
93
  <CommandInput
88
94
  placeholder={t("explorer.searchOrTypeProperty")}
@@ -137,7 +143,7 @@ export function PropertyCombobox({
137
143
  value === prop ? "opacity-100" : "opacity-0",
138
144
  )}
139
145
  />
140
- {enriched?.label ?? prop}
146
+ {enriched?.label ?? formatExplorerPropertyLabel(prop)}
141
147
  {enriched && (
142
148
  <span className="ml-auto text-[9px] bg-primary/10 text-primary px-1.5 py-0.5 rounded">
143
149
  {t("explorer.joined")}
@@ -166,7 +172,9 @@ export function PropertyCombobox({
166
172
  value === p.name ? "opacity-100" : "opacity-0",
167
173
  )}
168
174
  />
169
- <span className="truncate">{p.name}</span>
175
+ <span className="truncate">
176
+ {formatExplorerPropertyLabel(p.name)}
177
+ </span>
170
178
  <span className="ml-auto text-[10px] text-muted-foreground">
171
179
  {p.count.toLocaleString()}
172
180
  </span>
@@ -19,6 +19,7 @@ import {
19
19
  import { Skeleton } from "@/components/ui/skeleton";
20
20
  import { cn } from "@/lib/utils";
21
21
 
22
+ import { formatExplorerPropertyLabel } from "../property-label";
22
23
  import { usePropertyValues } from "../use-dynamic-schema";
23
24
 
24
25
  interface PropertyValueComboboxProps {
@@ -61,7 +62,7 @@ export function PropertyValueCombobox({
61
62
  )}
62
63
  </Button>
63
64
  </PopoverTrigger>
64
- <PopoverContent className="w-[240px] p-0" align="start">
65
+ <PopoverContent className="w-[280px] p-0" align="start">
65
66
  <Command shouldFilter={true}>
66
67
  <CommandInput
67
68
  placeholder={t("explorer.searchOrTypeValue")}
@@ -96,7 +97,9 @@ export function PropertyValueCombobox({
96
97
  </CommandEmpty>
97
98
  {values.length > 0 && (
98
99
  <CommandGroup
99
- heading={t("explorer.topValuesForProperty", { property })}
100
+ heading={t("explorer.topValuesForProperty", {
101
+ property: formatExplorerPropertyLabel(property),
102
+ })}
100
103
  >
101
104
  {values.map((v) => (
102
105
  <CommandItem
@@ -0,0 +1,32 @@
1
+ const PROPERTY_ACRONYMS: Record<string, string> = {
2
+ api: "API",
3
+ fbclid: "FBCLID",
4
+ gclid: "GCLID",
5
+ id: "ID",
6
+ url: "URL",
7
+ utm: "UTM",
8
+ };
9
+
10
+ export function formatExplorerPropertyLabel(property: string): string {
11
+ const trimmed = property.trim();
12
+ if (!trimmed) return property;
13
+
14
+ const spaced = trimmed
15
+ .replace(/[_-]+/g, " ")
16
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
17
+ .replace(/\s+/g, " ")
18
+ .trim();
19
+
20
+ if (!spaced) return property;
21
+
22
+ return spaced
23
+ .split(" ")
24
+ .map((word, index) => {
25
+ const lower = word.toLowerCase();
26
+ return (
27
+ PROPERTY_ACRONYMS[lower] ??
28
+ (index === 0 ? lower.charAt(0).toUpperCase() + lower.slice(1) : lower)
29
+ );
30
+ })
31
+ .join(" ");
32
+ }
@@ -0,0 +1,7 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-08-13
4
+ ---
5
+
6
+ Account health readouts now verify customer identity, completed usage periods,
7
+ contract metrics, and product adoption before summarizing.
@@ -20,8 +20,8 @@ const DEFAULT_MAX_BATCHES_PER_SWEEP = 4;
20
20
  const MAX_BATCHES_PER_SWEEP = 4;
21
21
  const DEFAULT_PARALLELISM = 8;
22
22
  const MAX_PARALLELISM = 8;
23
- const DEFAULT_MAX_ACTIVE_SESSIONS = 80;
24
23
  const DEFAULT_MAX_TOTAL_SESSIONS = 250;
24
+ const PRESSURE_RETRY_MS = 60 * 1000;
25
25
 
26
26
  type Query =
27
27
  | string
@@ -813,7 +813,7 @@ export async function queueFirstPartyAnalyticsBigQueryBackfill(
813
813
 
814
814
  async function pressureSnapshot(
815
815
  db: Executor,
816
- ): Promise<{ paused: boolean; reason?: string }> {
816
+ ): Promise<{ paused: boolean; degraded?: boolean; reason?: string }> {
817
817
  if (!isPostgres()) return { paused: false };
818
818
  try {
819
819
  const result = await db.execute({
@@ -833,15 +833,11 @@ async function pressureSnapshot(
833
833
  const active = numberValue(row, "active_sessions");
834
834
  const waiting = numberValue(row, "waiting_sessions");
835
835
  const lockWaiters = numberValue(row, "lock_waiters");
836
- const maxActive = positiveEnvNumber(
837
- "ANALYTICS_BIGQUERY_BACKFILL_MAX_ACTIVE_SESSIONS",
838
- DEFAULT_MAX_ACTIVE_SESSIONS,
839
- );
840
836
  const maxTotal = positiveEnvNumber(
841
837
  "ANALYTICS_BIGQUERY_BACKFILL_MAX_TOTAL_SESSIONS",
842
838
  DEFAULT_MAX_TOTAL_SESSIONS,
843
839
  );
844
- if (lockWaiters > 0 || active >= maxActive || total >= maxTotal) {
840
+ if (lockWaiters > 0 || waiting >= 8 || total >= maxTotal) {
845
841
  return {
846
842
  paused: true,
847
843
  reason: `database pressure: total=${total}, active=${active}, waiting=${waiting}, lockWaiters=${lockWaiters}`,
@@ -850,20 +846,51 @@ async function pressureSnapshot(
850
846
  return { paused: false };
851
847
  } catch (error) {
852
848
  return {
853
- paused: true,
849
+ paused: false,
850
+ degraded: true,
854
851
  reason: `pressure probe failed: ${error instanceof Error ? error.message : String(error)}`,
855
852
  };
856
853
  }
857
854
  }
858
855
 
856
+ async function recordPressurePause(
857
+ db: Executor,
858
+ jobId: string | null,
859
+ reason: string,
860
+ ): Promise<void> {
861
+ if (!jobId) return;
862
+ const retryAt = new Date(Date.now() + PRESSURE_RETRY_MS).toISOString();
863
+ await db.execute({
864
+ sql: `UPDATE ${JOB_TABLE}
865
+ SET last_error = ?, next_run_at = ?, updated_at = ?
866
+ WHERE id = ? AND status <> 'completed'`,
867
+ args: [reason.slice(0, 1_000), retryAt, new Date().toISOString(), jobId],
868
+ timeoutMs: 5_000,
869
+ maxAttempts: 1,
870
+ });
871
+ }
872
+
859
873
  export async function runFirstPartyAnalyticsBigQueryBackfillOnce(): Promise<BigQueryBackfillSweepResult> {
860
874
  if (process.env.ANALYTICS_BIGQUERY_BACKFILL_JOBS?.trim() === "0") {
861
875
  return { status: "disabled", batches: 0, copied: 0, remaining: 0 };
862
876
  }
863
877
 
864
878
  const db = executor();
879
+ const coordinator = await getNextFirstPartyAnalyticsBigQueryBackfillJob(
880
+ db,
881
+ new Date().toISOString(),
882
+ );
883
+ if (!coordinator) {
884
+ return { status: "idle", batches: 0, copied: 0, remaining: 0 };
885
+ }
886
+
865
887
  const initialPressure = await pressureSnapshot(db);
866
888
  if (initialPressure.paused) {
889
+ await recordPressurePause(
890
+ db,
891
+ coordinator.id,
892
+ initialPressure.reason ?? "database pressure",
893
+ );
867
894
  return {
868
895
  status: "paused-pressure",
869
896
  batches: 0,
@@ -873,21 +900,19 @@ export async function runFirstPartyAnalyticsBigQueryBackfillOnce(): Promise<BigQ
873
900
  };
874
901
  }
875
902
 
876
- const coordinator = await getNextFirstPartyAnalyticsBigQueryBackfillJob(
877
- db,
878
- new Date().toISOString(),
879
- );
880
- if (!coordinator) {
881
- return { status: "idle", batches: 0, copied: 0, remaining: 0 };
882
- }
883
-
884
903
  await ensureBackfillShards(db, coordinator);
885
904
 
886
905
  let batches = 0;
887
906
  let copied = 0;
907
+ let degraded = initialPressure.degraded === true;
888
908
  for (let index = 0; index < maxBatchesPerSweep(); index += 1) {
889
909
  const pressure = await pressureSnapshot(db);
890
910
  if (pressure.paused) {
911
+ await recordPressurePause(
912
+ db,
913
+ coordinator.id,
914
+ pressure.reason ?? "database pressure",
915
+ );
891
916
  return {
892
917
  status: "paused-pressure",
893
918
  batches,
@@ -896,10 +921,11 @@ export async function runFirstPartyAnalyticsBigQueryBackfillOnce(): Promise<BigQ
896
921
  reason: pressure.reason,
897
922
  };
898
923
  }
924
+ degraded ||= pressure.degraded === true;
899
925
 
900
926
  const claimed = (
901
927
  await Promise.all(
902
- Array.from({ length: parallelism() }, () =>
928
+ Array.from({ length: degraded ? 1 : parallelism() }, () =>
903
929
  claimNextShard(db, coordinator.id, new Date().toISOString()),
904
930
  ),
905
931
  )
@@ -11,6 +11,12 @@ import {
11
11
  const DATA_DICTIONARY_KEY_PREFIX = "data-dict-";
12
12
  const MAX_QUERY_LENGTH = 12_000;
13
13
  const MAX_CATALOG_DASHBOARD_HYDRATION = 24;
14
+ const RETIRED_CATALOG_STATES = new Set([
15
+ "deprecated",
16
+ "obsolete",
17
+ "removed",
18
+ "retired",
19
+ ]);
14
20
  const STOP_WORDS = new Set([
15
21
  "a",
16
22
  "all",
@@ -61,6 +67,15 @@ const STOP_WORDS = new Set([
61
67
  type DictionaryEntry = Record<string, unknown>;
62
68
  type DashboardPanel = Record<string, unknown>;
63
69
 
70
+ function isRetiredCatalogReference(value: Record<string, unknown>): boolean {
71
+ if (value.deprecated === true) return true;
72
+ return [value.status, value.lifecycle, value.state].some(
73
+ (candidate) =>
74
+ typeof candidate === "string" &&
75
+ RETIRED_CATALOG_STATES.has(candidate.trim().toLowerCase()),
76
+ );
77
+ }
78
+
64
79
  export type AnalyticsQueryCatalogCandidate =
65
80
  | {
66
81
  kind: "dashboard-panel";
@@ -296,6 +311,7 @@ function dashboardPanelCandidates(args: {
296
311
  const wantsDemo = /\bdemo\b|node exporter/i.test(args.search);
297
312
 
298
313
  return panels.flatMap((panel) => {
314
+ if (isRetiredCatalogReference(panel)) return [];
299
315
  // 38k of the ~39k indexed panels are clones of the demo Node Exporter dashboard.
300
316
  // They drown real saved work and are never the answer to a real data question.
301
317
  if (!wantsDemo && text(panel.source) === "demo") return [];
@@ -391,6 +407,7 @@ function dictionaryCandidates(
391
407
  search: string,
392
408
  ): AnalyticsQueryCatalogCandidate[] {
393
409
  return entries.flatMap((entry) => {
410
+ if (isRetiredCatalogReference(entry)) return [];
394
411
  const { score, matchedTerms } = matchScore(search, [
395
412
  { value: entry.metric, weight: 28 },
396
413
  { value: entry.commonQuestions, weight: 16 },
@@ -105,6 +105,9 @@ function hasPartialDashboardBuild(
105
105
  export const BOUNDED_STRUCTURED_LOOKUP_GUIDANCE =
106
106
  "BOUNDED STRUCTURED LOOKUP FAST PATH — Treat existing analytics work like an engineer treats existing code: grep before writing. For an ordinary count, aggregate, grouped metric, trend, or record lookup, first call `search-analytics-query-catalog` once with focused metric/entity terms. It searches accessible dashboard names, chart titles/descriptions/saved queries, shipped dashboard patterns, and data-dictionary definitions together. Prefer the strongest approved dictionary or saved-chart match, preserve its source and business logic, adapt only the requested filters and explicit time window, then run one bounded query against that source. A user-named source wins, but still use a matching saved definition when it supplies the source's proven query shape. If there is no useful match, inspect only the most likely source schema or ask one clarification; do not fan out across providers. Do not separately list every dashboard, call data-source status, browse the whole dictionary, load provider catalogs/corpus tools, or query a second source after a strong match. Once the query succeeds, answer immediately with its source, time window, filters, row count, and only necessary caveats. Do not enrich, cross-check, retry, or add breakdowns unless the user requested them, the first query failed, or its result conflicts with the known definition. The words `all`, `total`, or `exact` in a structured aggregate do not by themselves make it a corpus investigation. Never repeat an identical invalid or failed tool call; correct its arguments once or surface the error. This does not waive the real-data requirement: never answer from a guess, stale value, or unverified result. ";
107
107
 
108
+ export const ANALYTICS_ACCOUNT_HEALTH_GUIDANCE =
109
+ "ACCOUNT HEALTH / CUSTOMER SCOPE GUARD — Use this for a named customer, organization ID, account-health, QBR, renewal, contract-usage, risk, or adoption request. Treat a prompt-supplied org ID as a lookup key, not proof of row ownership: resolve it against canonical account data first, carry the resolved customer name plus organization/root-organization identifiers through every usage query, and stop if results contain a different customer, mixed IDs, or an unresolved identity. Use the account-health skill. Account health is incomplete until verified usage queries cover each requested product or feature dimension separately. Before warehouse SQL, use the catalog/dictionary and schema metadata. Do not run definitions marked deprecated or retired, or use a source whose freshness cannot be verified; use the current approved definition instead. Do not call a current partial-period snapshot a completed period without proving the period/as-of row and freshness. Distinguish contract metrics from similarly named platform metrics, total distinct contracted users from DAU/WAU, and actual usage from contracted capacity. Always surface utilization at or above 100%, report adoption window and coverage, and state gaps instead of inferring them. ";
110
+
108
111
  export const DASHBOARD_REFERENCE_GUIDANCE =
109
112
  "DASHBOARD REFERENCE DISCOVERY — When the user asks to replicate, clone, or adapt an existing dashboard, this branch takes precedence over the ordinary metric fast path: call `search-dashboard-references` with focused terms before creating, editing, or querying anything. It searches accessible active saved dashboard ids, names, descriptions, and serialized config with bounded SQL wildcard matches, including legacy saved dashboards. Treat each result as a reference to inspect with `get-sql-dashboard` when `kind` is `sql` or `get-explorer-dashboard` when `kind` is `explorer`, not as proof that its source is authoritative for the new request. Do not automatically route a replication request to first-party Analytics or copy its source semantics without checking the user's requested provider and scope. ";
110
113
 
@@ -156,6 +159,7 @@ export function analyticsSourceGuidanceOpening(): string {
156
159
  NON_ANALYTICS_REQUEST_GUIDANCE +
157
160
  DASHBOARD_REFERENCE_GUIDANCE +
158
161
  BOUNDED_STRUCTURED_LOOKUP_GUIDANCE +
162
+ ANALYTICS_ACCOUNT_HEALTH_GUIDANCE +
159
163
  BUILT_IN_FIRST_PARTY_SOURCE_GUIDANCE +
160
164
  ANALYTICS_OBSERVABILITY_INCIDENT_GUIDANCE +
161
165
  `DATA-SOURCE SETUP UX — Chat remains available when no external data source is connected. For a live-data request that needs an unavailable external provider, explain what is missing in the context of the user's question and guide them naturally to [Connect data sources](${ANALYTICS_DATA_SOURCES_LINK}). Use that real link from the app; do not emit a generic canned no-data sentence. For general conversation, conceptual questions, and questions the built-in first-party source can answer, continue helping normally. ` +
@@ -24,6 +24,8 @@ Detailed event, availability, booking, storage, and UI rules live in
24
24
  - Never hardcode API keys, tokens, webhook URLs, signing secrets, private Builder/internal data, customer data, or credential-looking literals. Use secrets/OAuth/runtime configuration and obvious placeholders in examples.
25
25
  - Use actions for events, availability, booking links, settings, navigation,
26
26
  Google Calendar connection, and sharing. Do not bypass app access checks.
27
+ - The `get-settings` and `update-settings` actions expose the General week-start
28
+ setting as `weekStart`: `sunday` or `monday`.
27
29
  - Use `connect-google-calendar` when the user asks to connect or reconnect
28
30
  Google Calendar. Return its link to the user; do not `fetch`
29
31
  `/_agent-native/google/auth-url` from the agent backend because that route
@@ -99,10 +99,10 @@ Structured data lives in SQL. Use `@agent-native/core/db/schema` helpers for sch
99
99
 
100
100
  Configuration lives in the SQL `settings` table, accessed via the settings API:
101
101
 
102
- | Key | Contents |
103
- | ----------------------- | -------------------------------------------- |
104
- | `calendar-settings` | App settings (timezone, booking page config) |
105
- | `calendar-availability` | Availability schedule configuration |
102
+ | Key | Contents |
103
+ | ----------------------- | -------------------------------------------------------- |
104
+ | `calendar-settings` | App settings (timezone, week start, booking page config) |
105
+ | `calendar-availability` | Availability schedule configuration |
106
106
 
107
107
  ### OAuth Tokens (via `@agent-native/core/oauth-tokens`)
108
108
 
@@ -3,14 +3,7 @@ import { getRequestUserEmail } from "@agent-native/core/server";
3
3
  import { getUserSetting } from "@agent-native/core/settings";
4
4
  import { z } from "zod";
5
5
 
6
- import type { Settings } from "../shared/api.js";
7
-
8
- const DEFAULT_SETTINGS: Settings = {
9
- timezone: "America/New_York",
10
- bookingPageTitle: "Book a Meeting",
11
- bookingPageDescription: "Select a time that works for you.",
12
- defaultEventDuration: 30,
13
- };
6
+ import { normalizeCalendarSettings } from "../shared/settings.js";
14
7
 
15
8
  export default defineAction({
16
9
  description: "Get calendar settings",
@@ -19,8 +12,8 @@ export default defineAction({
19
12
  run: async () => {
20
13
  const email = getRequestUserEmail();
21
14
  if (!email) throw new Error("no authenticated user");
22
- const settings =
23
- (await getUserSetting(email, "calendar-settings")) || DEFAULT_SETTINGS;
24
- return settings;
15
+ return normalizeCalendarSettings(
16
+ await getUserSetting(email, "calendar-settings"),
17
+ );
25
18
  },
26
19
  });
@@ -8,16 +8,27 @@ import {
8
8
  updateBookingUsername,
9
9
  } from "../server/handlers/booking-usernames.js";
10
10
  import type { AvailabilityConfig } from "../shared/api.js";
11
+ import { availabilitySlotsOverlap } from "../shared/availability-schedule.js";
11
12
 
12
13
  const timeSlotSchema = z.object({
13
14
  start: z.string(),
14
15
  end: z.string(),
15
16
  });
16
17
 
17
- const dayScheduleSchema = z.object({
18
- enabled: z.boolean(),
19
- slots: z.array(timeSlotSchema),
20
- });
18
+ const dayScheduleSchema = z
19
+ .object({
20
+ enabled: z.boolean(),
21
+ slots: z.array(timeSlotSchema),
22
+ })
23
+ .superRefine((day, ctx) => {
24
+ if (availabilitySlotsOverlap(day.slots)) {
25
+ ctx.addIssue({
26
+ code: z.ZodIssueCode.custom,
27
+ path: ["slots"],
28
+ message: "Availability windows must not overlap",
29
+ });
30
+ }
31
+ });
21
32
 
22
33
  const availabilitySchema = z.object({
23
34
  timezone: z.string(),