@agent-native/core-corpus 0.212.0 → 0.213.1

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 (76) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/onboarding-journeys/SKILL.md +29 -14
  3. package/corpus/templates/analytics/CHANGELOG.md +11 -0
  4. package/corpus/templates/analytics/actions/estimate-onboarding-journey-cost.ts +23 -6
  5. package/corpus/templates/analytics/actions/get-onboarding-journey.ts +20 -5
  6. package/corpus/templates/analytics/actions/provider-api-register.ts +2 -7
  7. package/corpus/templates/analytics/changelog/2026-10-21-analytics-recognizes-questions-about-connected-data-product-.md +6 -0
  8. package/corpus/templates/analytics/changelog/2026-10-21-onboarding-journeys-now-honor-larger-event-row-budgets.md +6 -0
  9. package/corpus/templates/analytics/changelog/2026-10-21-preserve-unknown-replay-availability-in-partial-journeys.md +6 -0
  10. package/corpus/templates/analytics/package.json +6 -0
  11. package/corpus/templates/analytics/scripts/journey-capture-plan.ts +18 -7
  12. package/corpus/templates/analytics/scripts/journey-capture.ts +3 -1
  13. package/corpus/templates/analytics/server/lib/db-admin-connections.ts +19 -12
  14. package/corpus/templates/analytics/server/lib/journey-tree.ts +24 -9
  15. package/corpus/templates/analytics/server/lib/onboarding-journey-cost.ts +163 -94
  16. package/corpus/templates/analytics/server/lib/onboarding-journey.ts +162 -74
  17. package/corpus/templates/assets/server/lib/org-admin.ts +20 -16
  18. package/corpus/templates/brain/server/lib/zoom-governance.ts +11 -6
  19. package/corpus/templates/clips/CHANGELOG.md +1 -0
  20. package/corpus/templates/clips/changelog/2026-10-16-rewind-status-says-when-it-remembers-audio.md +6 -0
  21. package/corpus/templates/clips/desktop/src/app.tsx +7 -4
  22. package/corpus/templates/clips/desktop/src/components/RewindCaptureStatus.tsx +25 -0
  23. package/corpus/templates/clips/desktop/src/lib/rewind-status.ts +6 -1
  24. package/corpus/templates/clips/desktop/src/shared/config.ts +3 -0
  25. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2 -0
  26. package/corpus/templates/clips/server/lib/recordings.ts +4 -19
  27. package/corpus/templates/community/qr/package.json +1 -1
  28. package/corpus/templates/content/CHANGELOG.md +8 -0
  29. package/corpus/templates/content/actions/_content-space-access.ts +95 -6
  30. package/corpus/templates/content/actions/_database-utils.ts +20 -16
  31. package/corpus/templates/content/actions/_github-source-freshness.ts +72 -17
  32. package/corpus/templates/content/actions/_github-source-metadata.ts +63 -17
  33. package/corpus/templates/content/actions/_github-source.ts +141 -77
  34. package/corpus/templates/content/actions/check-github-connection.ts +15 -0
  35. package/corpus/templates/content/actions/create-github-collection.ts +180 -0
  36. package/corpus/templates/content/actions/disconnect-content-database-source.ts +5 -0
  37. package/corpus/templates/content/actions/link-github-folder.ts +104 -36
  38. package/corpus/templates/content/actions/refresh-content-database-source.ts +37 -4
  39. package/corpus/templates/content/actions/sync-github-folder.ts +23 -5
  40. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +172 -19
  41. package/corpus/templates/content/app/components/editor/database/GitHubLinkDialog.tsx +938 -0
  42. package/corpus/templates/content/app/components/editor/database/GitHubSourcePanel.tsx +228 -0
  43. package/corpus/templates/content/app/components/editor/database/GitHubSourceStatusLine.tsx +561 -0
  44. package/corpus/templates/content/app/components/editor/database/GitHubUnlinkDialog.tsx +96 -0
  45. package/corpus/templates/content/app/components/editor/database/github-availability.ts +147 -0
  46. package/corpus/templates/content/app/components/editor/database/github-source-text.ts +280 -0
  47. package/corpus/templates/content/app/components/layout/Layout.tsx +2 -0
  48. package/corpus/templates/content/app/hooks/content-action-refresh.ts +6 -0
  49. package/corpus/templates/content/app/hooks/use-content-database.ts +3 -1
  50. package/corpus/templates/content/app/hooks/use-github-folder.ts +199 -0
  51. package/corpus/templates/content/app/i18n-data.ts +154 -1
  52. package/corpus/templates/content/app/lib/github-link-dialog.ts +86 -0
  53. package/corpus/templates/content/changelog/2026-10-18-connected-ai-assistants-can-link-a-new-collection-to-a-github.md +1 -1
  54. package/corpus/templates/content/changelog/2026-10-18-link-a-new-collection-to-a-github-folder-from-its-sources.md +6 -0
  55. package/corpus/templates/content/changelog/2026-10-21-organization-favorites-include-documents-shared-with-the-cur.md +6 -0
  56. package/corpus/templates/content/docs/product/capabilities/content.source.sync-policy.md +32 -7
  57. package/corpus/templates/content/docs/product/encyclopedia.md +3 -3
  58. package/corpus/templates/content/docs/product/features/content.feature.trust-your-connected-sources.md +2 -2
  59. package/corpus/templates/content/docs/product/roadmap.md +1 -1
  60. package/corpus/templates/content/parity/matrix.md +42 -42
  61. package/corpus/templates/content/parity/matrix.ts +14 -0
  62. package/corpus/templates/content/server/lib/github-source-client.ts +50 -1
  63. package/corpus/templates/content/shared/api.ts +3 -0
  64. package/corpus/templates/content/shared/github-source.ts +68 -0
  65. package/corpus/templates/design/CHANGELOG.md +1 -0
  66. package/corpus/templates/design/actions/connect-assets-mcp.ts +5 -9
  67. package/corpus/templates/design/changelog/2026-10-21-journey-canvases-accept-partial-analytics-samples-when-repla.md +6 -0
  68. package/corpus/templates/design/shared/journey-canvas.ts +6 -4
  69. package/corpus/templates/dispatch/.agents/skills/a2a-protocol/SKILL.md +16 -13
  70. package/corpus/templates/dispatch/CHANGELOG.md +10 -0
  71. package/corpus/templates/dispatch/changelog/2026-10-21-customer-ai-credit-questions-route-to-account-health-instead.md +6 -0
  72. package/corpus/templates/dispatch/changelog/2026-10-21-workspace-apps-load-reliably-with-organization-sign-in-and-f.md +6 -0
  73. package/corpus/templates/factory/server/lib/require-workspace-member.ts +5 -17
  74. package/corpus/templates/mail/server/lib/queued-drafts.ts +9 -4
  75. package/corpus/templates/slides/server/workspace-defaults.ts +12 -15
  76. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -32,4 +32,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core-corpus/corpu
32
32
 
33
33
  ## Generated Counts
34
34
 
35
- - template files: 13642
35
+ - template files: 13662
@@ -31,12 +31,18 @@ The pipeline runs without a UI. Codex does all four steps from the terminal:
31
31
 
32
32
  `estimate-onboarding-journey-cost` dry-runs the same access-scoped BigQuery
33
33
  event-page SQL as `get-onboarding-journey`. Pass matching dates, app,
34
- email filter, follow-up mode, and event row budget. It returns estimated bytes
35
- per possible page, their sum in `possiblePagesEstimatedBytes`, the unchanged
36
- per-query cap, and `within_cap` or `over_cap`. Page two is estimated when the
37
- row budget permits it; production reads it only when page one fills. The sum
38
- is a possible-pages estimate, not executed or billed bytes. Data-dependent
39
- follow-up queries are excluded. An unavailable estimate fails explicitly.
34
+ email filter, follow-up mode, event row budget, and `maxTotalBytesBilled`. It
35
+ reports requested and allowed page counts, then dry-runs each allowed 4,000-row
36
+ page, including the one-row overflow check when within budget. `maxTotalBytesBilled`
37
+ defaults to 275 GB, reserving the full 25 GB per-query cap for each of 11 pages;
38
+ raise it deliberately to allow more pages, up to 1.275 TB and 51 pages. Match
39
+ this value when calling `get-onboarding-journey`. A 45-second deadline or a
40
+ per-page dry-run timeout can return `status: "incomplete"`, the count of
41
+ unestimated allowed pages, and `possiblePagesEstimatedBytes: null`. Read
42
+ `pageBudgetLimited` and the requested/allowed counts before interpreting an
43
+ estimate. The sum is a possible-pages estimate, not executed or billed bytes.
44
+ Data-dependent follow-up queries are excluded. An unavailable estimate fails
45
+ explicitly.
40
46
 
41
47
  `get-onboarding-journey` returns `JourneyTree`; `format: "summary"` returns the
42
48
  same event-derived journey and follow-up counts as an indented `outline` with no
@@ -48,12 +54,12 @@ To include the bounded cross-session estimate from the repository root, run:
48
54
  `pnpm --filter analytics action get-onboarding-journey --dateFrom=2026-08-01 --dateTo=2026-08-31 --app=all --emailFilter=exclude_builder --followUpMode=person --format=summary`.
49
55
 
50
56
  ```ts
51
- type JourneyExample = { sessionId: string; recordingId: string | null; ts: string; offsetMs: number | null; viewport: { width: number; height: number } | null; viewportReason?: string; replayUrl?: string };
57
+ type JourneyExample = { sessionId: string; recordingId?: string | null; ts: string; offsetMs: number | null; viewport: { width: number; height: number } | null; viewportReason?: string; replayUrl?: string };
52
58
  type JourneyNode = { key: string; keyTruncated?: boolean; label: string; labelTruncated?: boolean; parentKey: string | null; depth: number; kind: "step" | "other"; n: number; pctOfRoot: number; pctOfParent: number; dropoffN: number; dropoffPct: number; deeperN: number; examples: JourneyExample[]; otherBranchCount?: number; otherBranchSummariesPartial?: true; otherBranches?: { path: string[]; pathTruncated?: boolean; key: string; keyTruncated?: boolean; sourceStepKey?: string; sourceStepKeyTruncated?: boolean; n: number; pctOfParent: number }[] };
53
59
  type JourneyFollowup = { status: "complete" | "incomplete"; incompleteReason?: "journey_event_read_truncated" | "journey_event_read_invalid" | "journey_event_read_may_have_shifted" | "terminal_cohort_query_too_large" | "followup_aggregate_truncated" | "followup_aggregate_invalid" | "followup_aggregate_cost_limited" | "followup_aggregate_query_timeout" | "followup_aggregate_query_failed" | "terminal_cohort_mismatch"; observationCutoff: string; observationFollowupDurationMs: { min: number; max: number; mean: number } | null; rightCensoredAtWindowEnd: true; coverage: { journeyEventRead: { rows: number; pages: number; truncated: boolean; paginationConsistency: "stable" | "may_have_shifted" }; followupAggregateRead: { rows: number | null; queries: number; truncated: boolean; status?: "incomplete"; backendStatus?: number | null; backendReason?: string | null; backendOperation?: "submit" | "poll" | "job" | null }; cohortSessions: number | null }; laterRecordedActivityWithinWindow: { total: number | null; byTerminalStepKey: Record<string, number> | null }; noLaterRecordedActivityWithinWindow: { total: number | null; byTerminalStepKey: Record<string, number> | null } };
54
60
  type JourneyPersonFollowupCounts = { canonicalPeople: number; laterActivityInSelectedSession: number; laterActivityOutsideSelectedSessionOrApp: number; laterActivityInBothSelectedAndOutside: number; laterActivityObservedAnywhere: number; noActivityObservedWithinHorizon: number; rightCensoredHorizon: number; fullyObservedCanonicalPeople: number; noActivityObservedWithinHorizonPctOfFullyObservedCanonicalPeople: number | null; identityUnavailableSessions: number; identityUnavailableSessionEvidence: { laterActivityInSelectedSession: number; laterActivityOutsideSelectedSessionOrApp: number } };
55
61
  type JourneyPersonFollowup = { status: "complete" | "incomplete"; incompleteReason?: "journey_event_read_truncated" | "journey_event_read_invalid" | "journey_event_read_may_have_shifted" | "terminal_cohort_too_large" | "terminal_cohort_invalid" | "person_followup_aggregate_truncated" | "person_followup_query_cost_limited" | "person_followup_query_timeout" | "person_followup_query_failed" | "person_followup_aggregate_invalid" | "person_followup_terminal_cohort_mismatch"; horizonDays: 30; horizonMs: number; observationWatermark: string; observationFollowupDurationMs: { min: number; max: number; mean: number } | null; coverage: { journeyEventRead: { rows: number; pages: number; truncated: boolean; paginationConsistency: "stable" | "may_have_shifted" }; followupAggregateRead: { status: "complete" | "truncated" | "incomplete" | "not_run"; rows: number | null; queries: number; truncated: boolean; backendStatus?: number | null; backendReason?: string | null; backendOperation?: "submit" | "poll" | "job" | null }; terminalSessions: number | null; sessionsWithoutSelectedStep: number | null; identityJoin: { status: "complete" | "partial" | "unavailable" | "not_applicable" | "unknown"; terminalSessions: number | null; sessionsWithCanonicalIdentity: number | null; sessionsWithoutCanonicalIdentity: number | null; uniqueCanonicalPeople: number | null; coveragePct: number | null } }; total: JourneyPersonFollowupCounts | null; byTerminalStepKey: Record<string, JourneyPersonFollowupCounts> | null };
56
- type JourneyTree = { window: { from: string; to: string }; app: string; rootN: number; coverage: { sessionsWithEvents: number; sessionsWithReplay: number; truncated: boolean }; nodes: JourneyNode[]; followUp: JourneyFollowup; personFollowUp?: JourneyPersonFollowup; standaloneSetup?: { rootN: number; coverage: { sessionsWithEvents: number; sessionsWithReplay: number; truncated: boolean }; nodes: JourneyNode[] }; notes?: string[] };
62
+ type JourneyTree = { window: { from: string; to: string }; app: string; rootN: number; coverage: { sessionsWithEvents: number; sessionsWithReplay: number | null; truncated: boolean }; nodes: JourneyNode[]; followUp: JourneyFollowup; personFollowUp?: JourneyPersonFollowup; standaloneSetup?: { rootN: number; coverage: { sessionsWithEvents: number; sessionsWithReplay: number | null; truncated: boolean }; nodes: JourneyNode[] }; notes?: string[] };
57
63
  ```
58
64
 
59
65
  Analytics returns cohort nodes with counts. When extending a Design storyboard
@@ -132,8 +138,15 @@ top-level tree to `journey:capture`; the capture CLI reads top-level `nodes`.
132
138
  session IDs or member identity keys used internally. The consistency field
133
139
  reports `may_have_shifted` when the event read uses multiple `OFFSET` pages;
134
140
  a late-arriving event can change page membership inside a historical window
135
- too. The action caps event reads at two 4,000-row BigQuery pages to bound
136
- total query cost. If either read truncates, page boundaries may have shifted,
141
+ too. The action reads 4,000-row pages up to `maxEventRows` (at most 200,000),
142
+ plus one overflow row to detect truncation. Each BigQuery page keeps a 25 GB
143
+ billed-byte cap; by default, `maxTotalBytesBilled` reserves that cap for 11
144
+ pages (275 GB), and can be raised deliberately to 1.275 TB (51 pages). Event
145
+ page reads also stop at a 45-second deadline. If the aggregate budget or
146
+ deadline cuts a read, `coverage.truncated` and `notes` identify why; read them
147
+ before interpreting counts. A truncated event sample skips replay reads and
148
+ returns `sessionsWithReplay: null` because replay availability is unknown. If
149
+ a read truncates, page boundaries may have shifted,
137
150
  or the terminal cohort cannot be covered in one query under the
138
151
  800,000-character SQL limit or 50,000-token parser limit, `status` is
139
152
  `incomplete` and all new cohort counts and follow-up duration are `null`;
@@ -191,15 +204,17 @@ top-level tree to `journey:capture`; the capture CLI reads top-level `nodes`.
191
204
  - A session is an analytics session id. One that began before `dateFrom`
192
205
  starts mid-journey, so start the window a day early.
193
206
  - Read `coverage` before using the numbers. `truncated: true` means the event
194
- read hit `maxEventRows`, a session went past `maxDepth`, or the node list hit
195
- `maxNodes`; `notes` says which.
207
+ read hit `maxEventRows`, `maxTotalBytesBilled`, or its 45-second deadline, a
208
+ session went past `maxDepth`, or the node list hit `maxNodes`; `notes` says
209
+ which.
196
210
  Never report a truncated tree as the whole window.
197
211
  - The event row cap is shared by onboarding and standalone setup. If it is hit,
198
212
  `standaloneSetup` may be an empty, truncated tree because standalone events
199
213
  were beyond the read boundary; do not interpret that as zero standalone use.
200
- - An example with `recordingId: null` has no replay the caller can open, and
214
+ - An example with `recordingId: null` has no replay the caller can open; an
215
+ omitted `recordingId` means replay availability was not checked. Its
201
216
  `viewportReason` says why the viewport is unknown (`no_recording`,
202
- `not_captured` for recordings before viewport capture, `unreadable`).
217
+ `recording_unknown`, `not_captured`, `unreadable`).
203
218
  - `offsetMs` is milliseconds from the recording's `startedAt`, not the first
204
219
  rrweb event.
205
220
  - `replayUrl` uses that same offset in `atMs`; the session page converts it to
@@ -3,6 +3,17 @@
3
3
  All notable user-facing changes to Agent-Native Analytics are documented here. Open it any
4
4
  time from the command menu (Cmd+K → "What's new") or from Settings.
5
5
 
6
+ ## 2026-10-21
7
+
8
+ ### Improved
9
+
10
+ - Analytics recognizes questions about connected data, product analytics, sessions, dashboards, and charts.
11
+ - Onboarding journeys honor larger event row budgets with aggregate cost limits, a read deadline, and clear partial-coverage reporting.
12
+
13
+ ### Fixed
14
+
15
+ - Preserve unknown replay availability in partial journeys
16
+
6
17
  ## 2026-10-20
7
18
 
8
19
  ### Fixed
@@ -14,10 +14,16 @@ import {
14
14
  estimateOnboardingJourneyEventQueryCost,
15
15
  OnboardingJourneyCostError,
16
16
  } from "../server/lib/onboarding-journey-cost.js";
17
- import { freezeOnboardingJourneyObservationWindow } from "../server/lib/onboarding-journey.js";
17
+ import {
18
+ DEFAULT_ONBOARDING_EVENT_ROW_BUDGET,
19
+ freezeOnboardingJourneyObservationWindow,
20
+ MAX_ONBOARDING_EVENT_ROW_BUDGET,
21
+ ONBOARDING_EVENTS_DEFAULT_MAX_TOTAL_BYTES_BILLED,
22
+ ONBOARDING_EVENTS_MAX_BYTES_BILLED,
23
+ ONBOARDING_EVENTS_MAX_TOTAL_BYTES_BILLED,
24
+ } from "../server/lib/onboarding-journey.js";
18
25
 
19
26
  const MAX_WINDOW_DAYS = 90;
20
- const MAX_JOURNEY_EVENT_ROWS = 200_000;
21
27
 
22
28
  const isoDate = z
23
29
  .string()
@@ -26,7 +32,7 @@ const isoDate = z
26
32
 
27
33
  export default defineAction({
28
34
  description:
29
- "Dry-run the caller-scoped BigQuery event-page queries for an onboarding journey and return each possible page's estimated bytes, their total if all pages run, the existing per-query bytes-billed cap, and cap status.",
35
+ "Dry-run the caller-scoped BigQuery event-page queries for an onboarding journey. Returns requested and budget-allowed page counts, estimated bytes per page, and total possible bytes when every allowed page is estimated. Each page reserves the 25 GB per-query cap against maxTotalBytesBilled. Estimates stop after 45 seconds and explicitly report unestimated pages.",
30
36
  schema: z.object({
31
37
  dateFrom: isoDate.describe(
32
38
  "Inclusive UTC start date, YYYY-MM-DD. Use the same dateFrom as get-onboarding-journey.",
@@ -59,11 +65,21 @@ export default defineAction({
59
65
  .number()
60
66
  .int()
61
67
  .min(1000)
62
- .max(MAX_JOURNEY_EVENT_ROWS)
68
+ .max(MAX_ONBOARDING_EVENT_ROW_BUDGET)
69
+ .optional()
70
+ .default(DEFAULT_ONBOARDING_EVENT_ROW_BUDGET)
71
+ .describe(
72
+ `Event row budget, matching get-onboarding-journey. Defaults to ${DEFAULT_ONBOARDING_EVENT_ROW_BUDGET}; at most ${MAX_ONBOARDING_EVENT_ROW_BUDGET}. Estimates include the one-row overflow check; the journey reads each page only when the previous page is full.`,
73
+ ),
74
+ maxTotalBytesBilled: z.coerce
75
+ .number()
76
+ .int()
77
+ .min(ONBOARDING_EVENTS_MAX_BYTES_BILLED)
78
+ .max(ONBOARDING_EVENTS_MAX_TOTAL_BYTES_BILLED)
63
79
  .optional()
64
- .default(40000)
80
+ .default(ONBOARDING_EVENTS_DEFAULT_MAX_TOTAL_BYTES_BILLED)
65
81
  .describe(
66
- `Event row budget, matching get-onboarding-journey. Defaults to 40000; at most ${MAX_JOURNEY_EVENT_ROWS}. The second page is estimated when this budget allows it, but the journey reads it only if the first page is full.`,
82
+ "Aggregate cap for event-page queries, in bytes billed. Each allowed BigQuery page reserves its full 25 GB per-query cap. Defaults to 275 GB (11 pages); set a higher value deliberately to allow more pages, up to 1.275 TB (51 pages). Match get-onboarding-journey.",
67
83
  ),
68
84
  }),
69
85
  http: { method: "GET" },
@@ -102,6 +118,7 @@ export default defineAction({
102
118
  freezeOnboardingJourneyObservationWindow(filters),
103
119
  args.followUpMode === "person",
104
120
  context?.signal,
121
+ args.maxTotalBytesBilled,
105
122
  );
106
123
  } catch (error) {
107
124
  if (
@@ -11,14 +11,18 @@ import {
11
11
  isCalendarDate,
12
12
  } from "../server/lib/first-party-metric-catalog.js";
13
13
  import {
14
+ DEFAULT_ONBOARDING_EVENT_ROW_BUDGET,
14
15
  getOnboardingJourney,
16
+ MAX_ONBOARDING_EVENT_ROW_BUDGET,
17
+ ONBOARDING_EVENTS_DEFAULT_MAX_TOTAL_BYTES_BILLED,
18
+ ONBOARDING_EVENTS_MAX_BYTES_BILLED,
19
+ ONBOARDING_EVENTS_MAX_TOTAL_BYTES_BILLED,
15
20
  OnboardingJourneyReadError,
16
21
  JourneyRecordingsError,
17
22
  } from "../server/lib/onboarding-journey.js";
18
23
 
19
24
  const MAX_WINDOW_DAYS = 90;
20
25
  const MAX_DEPTH = 40;
21
- const MAX_JOURNEY_EVENT_ROWS = 200_000;
22
26
 
23
27
  function resolveScope() {
24
28
  const userEmail = getRequestUserEmail();
@@ -37,7 +41,7 @@ const isoDate = z
37
41
 
38
42
  export default defineAction({
39
43
  description:
40
- 'Return an access-scoped per-session onboarding tree through explicit saved-output events, preserving session counts and direct-parent denominators. An aggregated `other` node includes up to 20 root-to-branch label paths, source step keys for identical paths, session counts, and direct-parent percentages; branch summaries are capped at 200 per tree and 64 KiB of serialized detail. `otherBranchCount` gives the full number of branches, and `otherBranchSummariesPartial: true` marks summaries omitted by those bounds. Labels/path segments longer than 300 characters and keys longer than 2,048 characters are visibly shortened with stable hash suffixes and truncation flags so the tree stays within the Design input contract. Journey counts stay bounded by the 200,000-event read cap. followUpMode "session" keeps existing bounded same-session counts. "person" adds aggregate-only activity joined by direct canonical auth_user_id across first-party sessions and apps over a fixed 30-day horizon. It separates selected-session activity, outside-session/app activity and their overlap, no activity after a fully observed horizon, right-censoring, unknown identity, identity coverage, and read completeness. Its minute-aligned observationWatermark freezes event and receive time. Unknown identity and sessions without a selected step never count as inactive; no-activity is window-bounded evidence, not permanent churn. Output and attempt IDs are never returned. Incomplete follow-up counts are null. If an event or follow-up query fails, the action returns a stage-specific error with safe BigQuery status/reason when available; an unreadable query never becomes zero counts or an empty-tree substitute.',
44
+ 'Return an access-scoped per-session onboarding tree through explicit saved-output events, preserving session counts and direct-parent denominators. An aggregated `other` node includes up to 20 root-to-branch label paths, source step keys for identical paths, session counts, and direct-parent percentages; branch summaries are capped at 200 per tree and 64 KiB of serialized detail. `otherBranchCount` gives the full number of branches, and `otherBranchSummariesPartial: true` marks summaries omitted by those bounds. Labels/path segments longer than 300 characters and keys longer than 2,048 characters are visibly shortened with stable hash suffixes and truncation flags so the tree stays within the Design input contract. Journey counts stay bounded by the 200,000-event read cap. BigQuery pages have a 25 GB per-query cap, and maxTotalBytesBilled bounds how many pages one event read may run; it defaults to 275 GB. Reads also stop at a 45-second event-read deadline, with coverage.truncated and notes identifying partial counts. Raise maxTotalBytesBilled deliberately to read more pages, up to 1.275 TB. When event coverage is truncated, replay reads are skipped, coverage.sessionsWithReplay is null, and examples omit recordingId with viewportReason "recording_unknown"; recordingId null means a completed lookup found no replay. followUpMode "session" keeps existing bounded same-session counts. "person" adds aggregate-only activity joined by direct canonical auth_user_id across first-party sessions and apps over a fixed 30-day horizon. It separates selected-session activity, outside-session/app activity and their overlap, no activity after a fully observed horizon, right-censoring, unknown identity, identity coverage, and read completeness. Its minute-aligned observationWatermark freezes event and receive time. Unknown identity and sessions without a selected step never count as inactive; no-activity is window-bounded evidence, not permanent churn. Output and attempt IDs are never returned. Incomplete follow-up counts are null. A query that fails for a backend, cost, or per-query timeout reason returns a stage-specific error with safe BigQuery status/reason when available; an internal event-read deadline instead returns completed pages with truncated coverage. An unreadable query never becomes zero counts or an empty-tree substitute.',
41
45
  schema: z.object({
42
46
  dateFrom: isoDate.describe(
43
47
  "Inclusive UTC start date, YYYY-MM-DD. Sessions that began earlier appear mid-journey, so start a day before the period you care about.",
@@ -114,11 +118,21 @@ export default defineAction({
114
118
  .number()
115
119
  .int()
116
120
  .min(1000)
117
- .max(MAX_JOURNEY_EVENT_ROWS)
121
+ .max(MAX_ONBOARDING_EVENT_ROW_BUDGET)
118
122
  .optional()
119
- .default(40000)
123
+ .default(DEFAULT_ONBOARDING_EVENT_ROW_BUDGET)
120
124
  .describe(
121
- "Event rows to read before stopping and reporting coverage.truncated. Each 4,000 rows is one query; raise it only for a wide window. Defaults to 40000.",
125
+ `Event rows to read, plus one extra row to detect truncation. Each query reads at most 4,000 rows and has a 25 GB BigQuery bytes-billed cap. The aggregate page budget and 45-second deadline can stop the read earlier; coverage.truncated and notes say when counts are partial. Defaults to ${DEFAULT_ONBOARDING_EVENT_ROW_BUDGET}.`,
126
+ ),
127
+ maxTotalBytesBilled: z.coerce
128
+ .number()
129
+ .int()
130
+ .min(ONBOARDING_EVENTS_MAX_BYTES_BILLED)
131
+ .max(ONBOARDING_EVENTS_MAX_TOTAL_BYTES_BILLED)
132
+ .optional()
133
+ .default(ONBOARDING_EVENTS_DEFAULT_MAX_TOTAL_BYTES_BILLED)
134
+ .describe(
135
+ "Aggregate cap for event-page queries, in bytes billed. Each allowed BigQuery page reserves its full 25 GB per-query cap. Defaults to 275 GB (11 pages); set a higher value deliberately to allow more pages, up to 1.275 TB (51 pages).",
122
136
  ),
123
137
  recency: z
124
138
  .enum(["newest", "none"])
@@ -203,6 +217,7 @@ export default defineAction({
203
217
  minNodeSessions: args.minNodeSessions,
204
218
  examplesPerNode: args.examplesPerNode,
205
219
  maxEventRows: args.maxEventRows,
220
+ maxTotalBytesBilled: args.maxTotalBytesBilled,
206
221
  settleMs: args.settleMs,
207
222
  recency: args.recency,
208
223
  viewport: Object.keys(viewport).length ? viewport : undefined,
@@ -1,19 +1,14 @@
1
- import { getDbExec } from "@agent-native/core/db";
2
1
  import {
3
2
  createCustomProviderRegistrationAction,
4
3
  CustomProviderRegistrationSchema,
5
4
  } from "@agent-native/core/provider-api/actions/custom-provider-registration";
5
+ import { readOrgMemberRole } from "@agent-native/core/server";
6
6
  import { getCredentialContext } from "@agent-native/core/server/request-context";
7
7
  import { z } from "zod";
8
8
 
9
9
  async function resolveCallerOrgRole(orgId: string, email: string) {
10
10
  try {
11
- const { rows } = await getDbExec().execute({
12
- sql: `SELECT role FROM org_members WHERE org_id = $1 AND LOWER(email) = $2 LIMIT 1`,
13
- args: [orgId, email.toLowerCase()],
14
- });
15
- const role = (rows[0] as { role?: unknown } | undefined)?.role;
16
- return typeof role === "string" && role ? role : null;
11
+ return await readOrgMemberRole(orgId, email);
17
12
  } catch {
18
13
  return null;
19
14
  }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-10-21
4
+ ---
5
+
6
+ Analytics recognizes questions about connected data, product analytics, sessions, dashboards, and charts.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-10-21
4
+ ---
5
+
6
+ Onboarding journeys honor larger event row budgets with aggregate cost limits, a read deadline, and clear partial-coverage reporting.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-10-21
4
+ ---
5
+
6
+ Preserve unknown replay availability in partial journeys
@@ -2,6 +2,7 @@
2
2
  "name": "analytics",
3
3
  "displayName": "Analytics",
4
4
  "private": true,
5
+ "description": "Connect data sources and warehouses, including BigQuery, to explore product and event analytics, session data, dashboards, and charts.",
5
6
  "type": "module",
6
7
  "scripts": {
7
8
  "dev": "DATABASE_URL=${ANALYTICS_DATABASE_URL:-pglite:./data/pglite} agent-native dev",
@@ -115,6 +116,11 @@
115
116
  "vitest": "catalog:"
116
117
  },
117
118
  "packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748",
119
+ "agent-native": {
120
+ "whenToUse": [
121
+ "Use Analytics for connected data sources and warehouses, including BigQuery, product and event analytics, session data, dashboards, and charts."
122
+ ]
123
+ },
118
124
  "pkg": {
119
125
  "assets": [
120
126
  "dist/spa/*"
@@ -15,7 +15,7 @@ export const DEFAULT_APP_URL = "https://analytics.agent-native.com";
15
15
 
16
16
  export interface TreeExample {
17
17
  sessionId: string;
18
- recordingId: string | null;
18
+ recordingId?: string | null;
19
19
  ts: string;
20
20
  offsetMs: number | null;
21
21
  viewport: { width: number; height: number } | null;
@@ -53,7 +53,11 @@ export function parseTree(raw: unknown): { nodes: TreeNode[] } {
53
53
  if (!e || typeof e.sessionId !== "string") {
54
54
  fail(`${at}.sessionId`, "must be a string");
55
55
  }
56
- if (e.recordingId !== null && typeof e.recordingId !== "string") {
56
+ if (
57
+ e.recordingId !== undefined &&
58
+ e.recordingId !== null &&
59
+ typeof e.recordingId !== "string"
60
+ ) {
57
61
  fail(`${at}.recordingId`, "must be a string or null");
58
62
  }
59
63
  if (
@@ -74,9 +78,10 @@ export function parseTree(raw: unknown): { nodes: TreeNode[] } {
74
78
  ) {
75
79
  fail(`${at}.viewport`, "must be { width, height } or null");
76
80
  }
81
+ const recordingId = e.recordingId as string | null | undefined;
77
82
  return {
78
83
  sessionId: e.sessionId as string,
79
- recordingId: e.recordingId as string | null,
84
+ ...(recordingId !== undefined ? { recordingId } : {}),
80
85
  ts: typeof e.ts === "string" ? e.ts : "",
81
86
  offsetMs: e.offsetMs as number | null,
82
87
  viewport: v
@@ -107,7 +112,11 @@ export interface PlanItem {
107
112
  export interface SkippedExample {
108
113
  nodeKey: string;
109
114
  exampleIndex: number;
110
- reason: "no_recording" | "no_offset" | "aspect_out_of_range";
115
+ reason:
116
+ | "no_recording"
117
+ | "replay_unknown"
118
+ | "no_offset"
119
+ | "aspect_out_of_range";
111
120
  }
112
121
 
113
122
  export function aspectInRange(
@@ -124,9 +133,10 @@ export function aspectInRange(
124
133
 
125
134
  /**
126
135
  * Up to `perNode` renderable examples per node, in the tree's order. An
127
- * example with no recording or offset, or a known viewport outside the aspect
128
- * range, is listed as skipped. An unknown viewport is rendered and checked on
129
- * the captured size instead.
136
+ * example with unknown replay availability, a completed lookup with no
137
+ * recording, no offset, or a known viewport outside the aspect range is listed
138
+ * as skipped. An unknown viewport is rendered and checked on the captured size
139
+ * instead.
130
140
  */
131
141
  export function planCapture(
132
142
  tree: { nodes: TreeNode[] },
@@ -139,6 +149,7 @@ export function planCapture(
139
149
  node.examples.forEach((example, exampleIndex) => {
140
150
  const skip = (reason: SkippedExample["reason"]) =>
141
151
  skipped.push({ nodeKey: node.key, exampleIndex, reason });
152
+ if (example.recordingId === undefined) return skip("replay_unknown");
142
153
  if (!example.recordingId) return skip("no_recording");
143
154
  if (example.offsetMs === null || example.offsetMs < 0) {
144
155
  return skip("no_offset");
@@ -2170,7 +2170,9 @@ async function main(argv: string[]): Promise<number> {
2170
2170
  );
2171
2171
  }
2172
2172
  console.error(
2173
- `Nothing to render: no example has a recording and an offset (see "skipped" in ${manifestPath}). Re-run get-onboarding-journey with a window that has replays.`,
2173
+ skipped.some((example) => example.reason === "replay_unknown")
2174
+ ? `Nothing to render: replay lookup was skipped for some examples because the journey read was truncated (see "skipped" in ${manifestPath}). Re-run get-onboarding-journey with a narrower window or a larger event budget.`
2175
+ : `Nothing to render: no example has a recording and an offset (see "skipped" in ${manifestPath}). Re-run get-onboarding-journey with a window that has replays.`,
2174
2176
  );
2175
2177
  return 1;
2176
2178
  }
@@ -12,7 +12,9 @@ import {
12
12
  import {
13
13
  getRequestOrgId,
14
14
  getRequestUserEmail,
15
+ getRequestContext,
15
16
  getSession,
17
+ readOrgMemberRole,
16
18
  runWithRequestContext,
17
19
  } from "@agent-native/core/server";
18
20
  import type { H3Event } from "h3";
@@ -68,18 +70,23 @@ export async function resolveOrgRole(
68
70
  userEmail: string,
69
71
  orgId: string,
70
72
  ): Promise<string | null> {
71
- const { rows } = await getDbExec()
72
- .execute({
73
- sql: `SELECT role FROM org_members WHERE org_id = $1 AND LOWER(email) = $2 AND federation_removal_pending_at IS NULL LIMIT 1`,
74
- args: [orgId, userEmail.toLowerCase()],
75
- })
76
- .catch((error: unknown) => {
77
- throw new DbAdminConnectionError(
78
- 503,
79
- `Couldn't verify your organization role: ${redactDbAdminError(error)}`,
80
- );
81
- });
82
- return readString((rows[0] ?? {}) as Record<string, unknown>, "role");
73
+ const request = getRequestContext();
74
+ if (
75
+ request?.a2aIdentityAssurance === "org-member" &&
76
+ (request.orgId?.trim() !== orgId.trim() ||
77
+ request.userEmail?.trim().toLowerCase() !==
78
+ userEmail.trim().toLowerCase())
79
+ ) {
80
+ return null;
81
+ }
82
+ try {
83
+ return await readOrgMemberRole(orgId, userEmail);
84
+ } catch (error) {
85
+ throw new DbAdminConnectionError(
86
+ 503,
87
+ `Couldn't verify your organization role: ${redactDbAdminError(error)}`,
88
+ );
89
+ }
83
90
  }
84
91
 
85
92
  export async function requireAnalyticsAdminContext(input?: {
@@ -48,20 +48,23 @@ export interface BuildJourneyTreeOptions {
48
48
 
49
49
  /**
50
50
  * Why an example has no viewport. `no_recording`: no accessible recording
51
- * covers the step. `not_captured`: the recording predates viewport capture.
52
- * `unreadable`: its metadata could not be parsed.
51
+ * covers the step. `recording_unknown`: replay lookup was skipped.
52
+ * `not_captured`: the recording predates viewport capture. `unreadable`: its
53
+ * metadata could not be parsed.
53
54
  */
54
55
  export type ViewportUnknownReason =
55
56
  | "no_recording"
57
+ | "recording_unknown"
56
58
  | "not_captured"
57
59
  | "unreadable";
58
60
 
59
61
  export interface JourneyExample {
60
62
  sessionId: string;
61
- recordingId: string | null;
63
+ /** Omitted when replay lookup was skipped; null means lookup found no replay. */
64
+ recordingId?: string | null;
62
65
  /** ISO time of the step in this session. */
63
66
  ts: string;
64
- /** Milliseconds from recording.startedAt to the step; null without a recording. */
67
+ /** Milliseconds from recording.startedAt to the step; null when no offset is known. */
65
68
  offsetMs: number | null;
66
69
  viewport: { width: number; height: number } | null;
67
70
  /** Present exactly when `viewport` is null. */
@@ -225,14 +228,17 @@ function hasViewportConstraints(constraints: ViewportConstraints | undefined) {
225
228
  interface Candidate {
226
229
  sessionId: string;
227
230
  tsMs: number;
228
- recording: JourneyRecording | null;
231
+ recording: JourneyRecording | null | undefined;
229
232
  }
230
233
 
231
234
  function chooseCandidate(
232
235
  ref: { sessionId: string; tsMs: number },
233
- recordings: readonly JourneyRecording[],
236
+ recordings: readonly JourneyRecording[] | null,
234
237
  constraints: ViewportConstraints | undefined,
235
238
  ): Candidate | null {
239
+ if (recordings === null) {
240
+ return constraints?.requireKnown ? null : { ...ref, recording: undefined };
241
+ }
236
242
  const constrained = hasViewportConstraints(constraints);
237
243
  const usable = recordings
238
244
  .filter((recording) => covers(recording, ref.tsMs))
@@ -268,6 +274,15 @@ function exampleFrom(
268
274
  ): JourneyExample {
269
275
  const { recording } = candidate;
270
276
  const ts = new Date(candidate.tsMs).toISOString();
277
+ if (recording === undefined) {
278
+ return {
279
+ sessionId: candidate.sessionId,
280
+ ts,
281
+ offsetMs: null,
282
+ viewport: null,
283
+ viewportReason: "recording_unknown",
284
+ };
285
+ }
271
286
  if (!recording) {
272
287
  return {
273
288
  sessionId: candidate.sessionId,
@@ -305,7 +320,7 @@ function exampleFrom(
305
320
 
306
321
  function pickExamples(
307
322
  refs: ReadonlyArray<{ sessionId: string; tsMs: number }>,
308
- recordings: ReadonlyMap<string, readonly JourneyRecording[]>,
323
+ recordings: ReadonlyMap<string, readonly JourneyRecording[]> | null,
309
324
  options: BuildJourneyTreeOptions,
310
325
  ): JourneyExample[] {
311
326
  if (options.examplesPerNode === 0) return [];
@@ -314,7 +329,7 @@ function pickExamples(
314
329
  for (const ref of refs) {
315
330
  const candidate = chooseCandidate(
316
331
  ref,
317
- recordings.get(ref.sessionId) ?? [],
332
+ recordings === null ? null : (recordings.get(ref.sessionId) ?? []),
318
333
  options.viewport,
319
334
  );
320
335
  if (candidate) candidates.push(candidate);
@@ -354,7 +369,7 @@ const compareKeys = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
354
369
  */
355
370
  export function buildJourneyTree(
356
371
  sessions: readonly JourneySession[],
357
- recordings: ReadonlyMap<string, readonly JourneyRecording[]>,
372
+ recordings: ReadonlyMap<string, readonly JourneyRecording[]> | null,
358
373
  options: BuildJourneyTreeOptions,
359
374
  ): { rootN: number; nodes: JourneyNode[] } {
360
375
  const root = trieNode("root", "All sessions", "root", 0);