@agent-native/core 0.144.0 → 0.144.2

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 (88) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/actions/migrate-first-party-analytics-to-bigquery.ts +1 -1
  3. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +36 -14
  4. package/corpus/templates/clips/app/components/player/player-controls.tsx +0 -3
  5. package/corpus/templates/clips/app/components/player/scrubber.tsx +2 -33
  6. package/corpus/templates/clips/app/components/player/video-player.tsx +58 -11
  7. package/corpus/templates/clips/changelog/2026-08-06-trimmed-clips-play-on-a-clean-timeline-without-cut-markers.md +6 -0
  8. package/corpus/templates/dispatch/AGENTS.md +8 -1
  9. package/corpus/templates/dispatch/app/dispatch-extensions.tsx +1 -0
  10. package/corpus/templates/dispatch/app/hooks/use-navigation-state.ts +30 -15
  11. package/corpus/templates/dispatch/app/i18n-data.ts +131 -0
  12. package/corpus/templates/dispatch/app/routes/admin._index.tsx +4 -0
  13. package/corpus/templates/dispatch/app/routes/admin.agents.tsx +1 -0
  14. package/corpus/templates/dispatch/app/routes/admin.approvals.tsx +1 -0
  15. package/corpus/templates/dispatch/app/routes/admin.apps.$appId.tsx +1 -0
  16. package/corpus/templates/dispatch/app/routes/admin.apps.tsx +1 -0
  17. package/corpus/templates/dispatch/app/routes/admin.audit.tsx +1 -0
  18. package/corpus/templates/dispatch/app/routes/admin.automations.tsx +1 -0
  19. package/corpus/templates/dispatch/app/routes/admin.destinations.tsx +4 -0
  20. package/corpus/templates/dispatch/app/routes/admin.dreams.tsx +1 -0
  21. package/corpus/templates/dispatch/app/routes/admin.identities.tsx +1 -0
  22. package/corpus/templates/dispatch/app/routes/admin.integrations.tsx +4 -0
  23. package/corpus/templates/dispatch/app/routes/admin.messaging.tsx +1 -0
  24. package/corpus/templates/dispatch/app/routes/admin.metrics.tsx +1 -0
  25. package/corpus/templates/dispatch/app/routes/admin.new-app.tsx +1 -0
  26. package/corpus/templates/dispatch/app/routes/admin.operations.tsx +1 -0
  27. package/corpus/templates/dispatch/app/routes/admin.thread-debug.tsx +4 -0
  28. package/corpus/templates/dispatch/app/routes/admin.transactional-email.$appId.$id.tsx +4 -0
  29. package/corpus/templates/dispatch/app/routes/admin.transactional-email.tsx +4 -0
  30. package/corpus/templates/dispatch/app/routes/admin.tsx +1 -0
  31. package/corpus/templates/dispatch/app/routes/admin.vault.tsx +1 -0
  32. package/corpus/templates/dispatch/app/routes/admin.workspace.tsx +1 -0
  33. package/corpus/templates/dispatch/changelog/2026-08-06-dispatch-keeps-everyday-navigation-focused-while-management-.md +6 -0
  34. package/corpus/templates/slides/AGENTS.md +10 -12
  35. package/corpus/templates/slides/actions/export-pptx.ts +68 -0
  36. package/corpus/templates/slides/actions/get-deck.ts +12 -0
  37. package/corpus/templates/slides/actions/import-file.ts +130 -55
  38. package/corpus/templates/slides/actions/import-pptx.ts +108 -8
  39. package/corpus/templates/slides/actions/patch-deck.ts +51 -0
  40. package/corpus/templates/slides/actions/update-slide.ts +29 -3
  41. package/corpus/templates/slides/app/components/onboarding/FirstDeckOnboardingFlow.tsx +11 -0
  42. package/corpus/templates/slides/app/lib/create-deck-generation.ts +72 -11
  43. package/corpus/templates/slides/app/lib/import-uploaded-deck.ts +89 -0
  44. package/corpus/templates/slides/app/pages/Index.tsx +85 -18
  45. package/corpus/templates/slides/changelog/2026-08-06-uploaded-decks-keep-their-slides-and-visuals-when-you-improv.md +6 -0
  46. package/corpus/templates/slides/server/handlers/import/html-converter.ts +13 -2
  47. package/corpus/templates/slides/server/lib/chat-attachments.ts +2 -2
  48. package/corpus/templates/slides/server/lib/source-import.ts +167 -0
  49. package/corpus/templates/slides/server/plugins/agent-chat.ts +8 -0
  50. package/dist/client/analytics/index.d.ts +1 -1
  51. package/dist/client/analytics/index.js +1 -1
  52. package/dist/client/analytics-session.d.ts +13 -0
  53. package/dist/client/analytics-session.js +57 -0
  54. package/dist/client/analytics.d.ts +1 -0
  55. package/dist/client/analytics.js +1 -0
  56. package/dist/client/index.d.ts +1 -1
  57. package/dist/client/index.js +1 -1
  58. package/dist/client/track.js +7 -0
  59. package/dist/client/use-action.js +10 -1
  60. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  61. package/dist/jobs/scheduler.js +79 -23
  62. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  63. package/dist/notifications/routes.d.ts +3 -3
  64. package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
  65. package/dist/resources/handlers.d.ts +1 -1
  66. package/dist/secrets/routes.d.ts +9 -9
  67. package/dist/server/action-routes.js +4 -2
  68. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  69. package/dist/server/agent-run-context.d.ts +4 -2
  70. package/dist/server/agent-run-context.js +5 -3
  71. package/dist/server/core-routes-plugin.js +2 -0
  72. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +5 -1
  73. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +20 -3
  74. package/dist/tracking/index.d.ts +1 -1
  75. package/dist/tracking/providers.js +12 -1
  76. package/dist/tracking/registry.d.ts +12 -2
  77. package/dist/tracking/registry.js +27 -3
  78. package/dist/tracking/types.d.ts +8 -0
  79. package/docs/content/creating-templates.mdx +1 -1
  80. package/docs/content/getting-started-actions.mdx +38 -189
  81. package/docs/content/getting-started-database.mdx +80 -164
  82. package/docs/content/getting-started-pages.mdx +44 -155
  83. package/docs/content/getting-started.mdx +26 -106
  84. package/docs/content/observability.mdx +16 -0
  85. package/docs/content/tracking.mdx +17 -2
  86. package/package.json +1 -1
  87. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +5 -1
  88. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +20 -3
@@ -0,0 +1,167 @@
1
+ export type SourceImportFormat = "pdf" | "pptx";
2
+
3
+ export interface SourceImportSlideSnapshot {
4
+ id: string;
5
+ text: string;
6
+ notes: string;
7
+ imageUrls: string[];
8
+ editableText: boolean;
9
+ }
10
+
11
+ export interface SourceImportMetadata {
12
+ mode: "source-preserving";
13
+ format: SourceImportFormat;
14
+ fidelity: "source-faithful" | "partial";
15
+ importedAt: string;
16
+ slideCount: number;
17
+ slideIds: string[];
18
+ slides: SourceImportSlideSnapshot[];
19
+ imagesSkipped?: number;
20
+ }
21
+
22
+ export function buildSourceImportMetadata(args: {
23
+ format: SourceImportFormat;
24
+ importedAt?: string;
25
+ slides: SourceImportSlideSnapshot[];
26
+ imagesSkipped?: number;
27
+ }): SourceImportMetadata {
28
+ const importedAt = args.importedAt ?? new Date().toISOString();
29
+ const imagesSkipped = args.imagesSkipped ?? 0;
30
+ return {
31
+ mode: "source-preserving",
32
+ format: args.format,
33
+ fidelity: imagesSkipped > 0 ? "partial" : "source-faithful",
34
+ importedAt,
35
+ slideCount: args.slides.length,
36
+ slideIds: args.slides.map((slide) => slide.id),
37
+ slides: args.slides,
38
+ ...(imagesSkipped > 0 ? { imagesSkipped } : {}),
39
+ };
40
+ }
41
+
42
+ export function mergeSourceImportMetadata(
43
+ existing: SourceImportMetadata | null,
44
+ incoming: SourceImportMetadata,
45
+ ): SourceImportMetadata {
46
+ if (!existing) return incoming;
47
+ if (existing.format !== incoming.format) {
48
+ throw new Error(
49
+ `Cannot append a ${incoming.format.toUpperCase()} source import to a ${existing.format.toUpperCase()} source-imported deck. Import matching source formats separately so every slide keeps its provenance.`,
50
+ );
51
+ }
52
+
53
+ const slidesById = new Map<string, SourceImportSlideSnapshot>();
54
+ for (const slide of existing.slides) slidesById.set(slide.id, slide);
55
+ for (const slide of incoming.slides) slidesById.set(slide.id, slide);
56
+ const slides = [...slidesById.values()];
57
+ const imagesSkipped =
58
+ (existing.imagesSkipped ?? 0) + (incoming.imagesSkipped ?? 0);
59
+
60
+ return {
61
+ ...incoming,
62
+ fidelity:
63
+ existing.fidelity === "partial" || incoming.fidelity === "partial"
64
+ ? "partial"
65
+ : "source-faithful",
66
+ slideCount: slides.length,
67
+ slideIds: slides.map((slide) => slide.id),
68
+ slides,
69
+ ...(imagesSkipped > 0 ? { imagesSkipped } : {}),
70
+ };
71
+ }
72
+
73
+ export function sourceImportForDeck(
74
+ value: unknown,
75
+ ): SourceImportMetadata | null {
76
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
77
+ const record = value as Partial<SourceImportMetadata>;
78
+ if (
79
+ record.mode !== "source-preserving" ||
80
+ (record.format !== "pdf" && record.format !== "pptx") ||
81
+ !Array.isArray(record.slides)
82
+ ) {
83
+ return null;
84
+ }
85
+ return record as SourceImportMetadata;
86
+ }
87
+
88
+ function extractImageUrls(content: string): string[] {
89
+ const urls: string[] = [];
90
+ const seen = new Set<string>();
91
+ const imagePattern = /<img\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi;
92
+ for (const match of content.matchAll(imagePattern)) {
93
+ const url = match[1]
94
+ ?.replace(/&amp;/g, "&")
95
+ .replace(/&quot;/g, '"')
96
+ .trim();
97
+ if (url && !seen.has(url)) {
98
+ seen.add(url);
99
+ urls.push(url);
100
+ }
101
+ }
102
+ return urls;
103
+ }
104
+
105
+ function sourceWords(value: string): Set<string> {
106
+ return new Set(
107
+ value
108
+ .toLowerCase()
109
+ .match(/[a-z0-9][a-z0-9'-]{3,}/g)
110
+ ?.map((word) => word.replace(/^['-]+|['-]+$/g, "")) ?? [],
111
+ );
112
+ }
113
+
114
+ /**
115
+ * Protects an imported deck from the common "make it prettier" failure mode:
116
+ * the agent replaces a source slide with a generic card and silently drops the
117
+ * original artwork or most of its factual copy. Explicit rewrite requests can
118
+ * opt out with preserveSource=false.
119
+ */
120
+ export function assertSourceSlidePreserved(args: {
121
+ metadata: SourceImportMetadata | null;
122
+ slideId: string;
123
+ nextContent?: string;
124
+ nextNotes?: string;
125
+ preserveSource?: boolean;
126
+ }): void {
127
+ if (args.preserveSource === false || !args.metadata) return;
128
+ const snapshot = args.metadata.slides.find(
129
+ (slide) => slide.id === args.slideId,
130
+ );
131
+ if (!snapshot) return;
132
+
133
+ if (
134
+ args.nextNotes !== undefined &&
135
+ snapshot.notes.length > 0 &&
136
+ args.nextNotes !== snapshot.notes
137
+ ) {
138
+ throw new Error(
139
+ `Source-preserving edit would remove or change imported speaker notes on slide ${args.slideId}. Preserve the source notes, or pass preserveSource=false only when the user explicitly asks for a rewrite.`,
140
+ );
141
+ }
142
+
143
+ if (args.nextContent === undefined) return;
144
+
145
+ const nextImageUrls = new Set(extractImageUrls(args.nextContent));
146
+ const missingImages = snapshot.imageUrls.filter(
147
+ (url) => !nextImageUrls.has(url),
148
+ );
149
+ if (missingImages.length > 0) {
150
+ throw new Error(
151
+ `Source-preserving edit would remove ${missingImages.length} original image(s) from slide ${args.slideId}. Keep every existing source image, or pass preserveSource=false only when the user explicitly asks for a rewrite.`,
152
+ );
153
+ }
154
+
155
+ if (!snapshot.editableText) return;
156
+ const originalWords = sourceWords(snapshot.text);
157
+ if (originalWords.size < 12) return;
158
+ const nextWords = sourceWords(args.nextContent.replace(/<[^>]+>/g, " "));
159
+ const retainedWords = [...originalWords].filter((word) =>
160
+ nextWords.has(word),
161
+ ).length;
162
+ if (retainedWords / originalWords.size < 0.35) {
163
+ throw new Error(
164
+ `Source-preserving edit would drop most of the original factual copy from slide ${args.slideId}. Preserve the source text, or pass preserveSource=false only when the user explicitly asks for a rewrite.`,
165
+ );
166
+ }
167
+ }
@@ -15,6 +15,8 @@ const INITIAL_TOOL_NAMES = [
15
15
  "get-layout-overflows",
16
16
  "list-decks",
17
17
  "get-deck",
18
+ "get-design-system",
19
+ "get-workspace-defaults",
18
20
  "get-deck-reference-context",
19
21
  "create-deck",
20
22
  "add-slide",
@@ -50,6 +52,12 @@ export default createAgentChatPlugin({
50
52
  prepareRequest: prepareSlidesChatAttachments,
51
53
  systemPrompt: `You are an AI deck assistant. You create, edit, import, export, style, share, and navigate decks through actions and shared application state. For a newly created presentation, use create-deck with slides: [] only when you are creating the deck yourself, then add-slide sequentially with full rendered HTML. The legacy generate-slides-ai action returns Markdown drafts and is not part of the persisted presentation workflow.
52
54
 
55
+ When the user asks to improve, beautify, restyle, or make an uploaded/existing deck on-brand, treat it as an in-place source-preserving edit unless the user explicitly asks to rewrite the story or change slide count. First call view-screen when the active deck is unclear, then get-deck. If get-deck.sourceImport exists, preserve its slide count, order, IDs, factual copy, notes, images, charts, tables, diagrams, freeform objects, and source aspect ratio. Use update-slide on existing slide IDs. Do not use add-slide, delete, reorder, or replace source imagery with generic cards for this workflow. Verify the same slide IDs and count with get-deck after editing. If sourceImport.fidelity is partial or imagesSkipped is nonzero, stop and report the exact fidelity warning instead of claiming a reliable improvement.
56
+
57
+ For source-faithful PDF slides, keep the original full-page image and style around it with restrained design-system chrome such as a frame, edge treatment, caption, or safe overlay that does not obscure source content; never OCR-reconstruct the page from extracted text. For PPTX slides, preserve the imported positioned HTML and every uploaded source image. The update-slide action enforces these preservation rules by default; pass preserveSource=false only when the user explicitly requests a rewrite of that slide.
58
+
59
+ If the deck has designSystemId, call get-design-system before writing and follow its exact agentContext tokens, assets, and custom instructions. If the user asks for on-brand styling and no design system is linked, call get-workspace-defaults, link its usable design system with patch-deck, then call get-design-system. Do not improvise a generic Builder-like palette when configured Builder.io design-system context is available.
60
+
53
61
  Layout-fit workflow is strict. When the user asks to fix overflow, first call view-screen and inspect the deck-wide layout-fit section. If it says measurements are unknown, do not claim the deck fits. Call get-layout-overflows when you need the structured per-slide results. Read the affected slides with get-deck, then make one bounded structural repair pass with one patch-slide operation per affected slide in a single patch-deck call. Wait for the action result and verify the persisted HTML with get-deck before saying it is fixed. If a fresh measurement still reports overflow, make at most one focused follow-up repair based on that measurement; never loop, repeatedly re-measure, or claim success after a chat response alone.
54
62
 
55
63
  Fit means the main content fits the native content area. A small outer-wrapper spill is tolerated by the measurement, but cards, text, columns, and other visible content must fit. Never use zoom, transform: scale(), overflow: hidden/scroll, clipping, or a smaller-than-16px body font to hide overflow. Preserve manually positioned freeform objects and their data-slide-object-id values; repair normal-flow structure, copy, gaps, or slide padding instead. A successful action result must include the affected slide IDs; if it does not, report that no verified write occurred.
@@ -1,2 +1,2 @@
1
- export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureClientException, captureError, captureException, captureMessage, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackEvent, trackSessionStatus, type AgentChatLifecycleEvent, type CaptureExceptionContext, type CapturedExceptionEvent, type ClientCaptureContext, type ConfigureTrackingOptions, type ErrorCaptureConfigOptions, type ExceptionBreadcrumb, type ExceptionLevel, type FirstTouchAttribution, type SessionReplayContext, type SessionReplayLinkOptions, type SessionReplayOptions, type SessionReplayStartResult, type SessionReplayUrlMatcher, type TrackingIdentityUser, } from "../analytics.js";
1
+ export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureClientException, captureError, captureException, captureMessage, clearAnalyticsSessionId, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setAnalyticsSessionId, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackEvent, trackSessionStatus, type AgentChatLifecycleEvent, type CaptureExceptionContext, type CapturedExceptionEvent, type ClientCaptureContext, type ConfigureTrackingOptions, type ErrorCaptureConfigOptions, type ExceptionBreadcrumb, type ExceptionLevel, type FirstTouchAttribution, type SessionReplayContext, type SessionReplayLinkOptions, type SessionReplayOptions, type SessionReplayStartResult, type SessionReplayUrlMatcher, type TrackingIdentityUser, } from "../analytics.js";
2
2
  export { track } from "../track.js";
@@ -1,2 +1,2 @@
1
- export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureClientException, captureError, captureException, captureMessage, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackEvent, trackSessionStatus, } from "../analytics.js";
1
+ export { AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureClientException, captureError, captureException, captureMessage, clearAnalyticsSessionId, configureTracking, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, getSessionReplayContext, getSessionReplayUrl, isErrorCaptureInstalled, maybeStartSessionReplay, setAnalyticsSessionId, setSentryUser, setTrackingContentCaptureEnabled, setTrackingIdentity, startSessionReplay, stopSessionReplay, trackAgentChatLifecycle, trackEvent, trackSessionStatus, } from "../analytics.js";
2
2
  export { track } from "../track.js";
@@ -1,2 +1,15 @@
1
1
  export declare function getOrCreateAnalyticsAnonymousId(): string | undefined;
2
+ /**
3
+ * Pin the browser session id an app wants every analytics event, agent run, and
4
+ * action request in this tab to carry. Returns the stored id, or `undefined`
5
+ * when there is no browser storage to pin it in (SSR) — the caller can tell
6
+ * those apart.
7
+ *
8
+ * Throws on an id the transport cannot carry rather than storing it.
9
+ */
10
+ export declare function setAnalyticsSessionId(sessionId: string): string | undefined;
11
+ /**
12
+ * Drop a pinned id and let the next read start a fresh rotating session.
13
+ */
14
+ export declare function clearAnalyticsSessionId(): void;
2
15
  export declare function getOrCreateAnalyticsSessionId(): string | undefined;
@@ -1,10 +1,17 @@
1
1
  import { serializeAnalyticsAnonymousIdCookie } from "../shared/analytics-anonymous-id.js";
2
2
  const ANONYMOUS_ID_STORAGE_KEY = "agent-native.anonymous_id";
3
3
  const SESSION_ID_STORAGE_KEY = "agent-native.session_id";
4
+ const SESSION_ID_PIN_STORAGE_KEY = "agent-native.session_id_pin";
4
5
  const SESSION_LAST_ACTIVITY_STORAGE_KEY = "agent-native.session_last_activity";
5
6
  // 30-minute idle timeout matches GA4 / Mixpanel defaults: a tab left open
6
7
  // overnight starts a new session in the morning instead of stretching one visit.
7
8
  const SESSION_IDLE_TIMEOUT_MS = 30 * 60 * 1000;
9
+ // The server drops the session header at 128 characters
10
+ // (`readBrowserSessionIdHeader`), and `fetch` throws on a header value with
11
+ // non-ASCII or newline bytes. Both are rejected at the point the id is set so a
12
+ // bad id fails once, loudly, instead of silently unlinking every later request.
13
+ const MAX_SESSION_ID_LENGTH = 127;
14
+ const SAFE_SESSION_ID = /^[!-~]+$/;
8
15
  function generateVisitorId() {
9
16
  try {
10
17
  if (typeof crypto !== "undefined" &&
@@ -35,6 +42,15 @@ function safeStorageSet(key, value) {
35
42
  // private browsing / storage disabled -- best-effort
36
43
  }
37
44
  }
45
+ function safeStorageRemove(key) {
46
+ try {
47
+ window.localStorage.removeItem(key);
48
+ // coercion-ok: storage that refuses writes holds no key to clear, so the caller's end state already holds
49
+ }
50
+ catch {
51
+ // private browsing / storage disabled -- best-effort
52
+ }
53
+ }
38
54
  function syncAnalyticsAnonymousIdCookie(id) {
39
55
  try {
40
56
  const cookie = serializeAnalyticsAnonymousIdCookie(id);
@@ -57,10 +73,51 @@ export function getOrCreateAnalyticsAnonymousId() {
57
73
  syncAnalyticsAnonymousIdCookie(id);
58
74
  return id;
59
75
  }
76
+ /**
77
+ * Pin the browser session id an app wants every analytics event, agent run, and
78
+ * action request in this tab to carry. Returns the stored id, or `undefined`
79
+ * when there is no browser storage to pin it in (SSR) — the caller can tell
80
+ * those apart.
81
+ *
82
+ * Throws on an id the transport cannot carry rather than storing it.
83
+ */
84
+ export function setAnalyticsSessionId(sessionId) {
85
+ const trimmed = typeof sessionId === "string" ? sessionId.trim() : "";
86
+ if (!trimmed ||
87
+ trimmed.length > MAX_SESSION_ID_LENGTH ||
88
+ !SAFE_SESSION_ID.test(trimmed)) {
89
+ throw new Error(`Invalid analytics session id: expected 1-${MAX_SESSION_ID_LENGTH} printable ASCII characters with no whitespace`);
90
+ }
91
+ if (typeof window === "undefined")
92
+ return undefined;
93
+ safeStorageSet(SESSION_ID_PIN_STORAGE_KEY, trimmed);
94
+ safeStorageSet(SESSION_ID_STORAGE_KEY, trimmed);
95
+ safeStorageSet(SESSION_LAST_ACTIVITY_STORAGE_KEY, String(Date.now()));
96
+ return trimmed;
97
+ }
98
+ /**
99
+ * Drop a pinned id and let the next read start a fresh rotating session.
100
+ */
101
+ export function clearAnalyticsSessionId() {
102
+ if (typeof window === "undefined")
103
+ return;
104
+ safeStorageRemove(SESSION_ID_PIN_STORAGE_KEY);
105
+ safeStorageRemove(SESSION_ID_STORAGE_KEY);
106
+ safeStorageRemove(SESSION_LAST_ACTIVITY_STORAGE_KEY);
107
+ }
60
108
  export function getOrCreateAnalyticsSessionId() {
61
109
  if (typeof window === "undefined")
62
110
  return undefined;
63
111
  const now = Date.now();
112
+ // A pinned id opts out of idle rotation: the app chose it to correlate its
113
+ // own runs, so expiring it mid-workflow would split one correlated session
114
+ // into two that nothing can join back together.
115
+ const pinned = safeStorageGet(SESSION_ID_PIN_STORAGE_KEY);
116
+ if (pinned) {
117
+ safeStorageSet(SESSION_ID_STORAGE_KEY, pinned);
118
+ safeStorageSet(SESSION_LAST_ACTIVITY_STORAGE_KEY, String(now));
119
+ return pinned;
120
+ }
64
121
  const lastActivityRaw = safeStorageGet(SESSION_LAST_ACTIVITY_STORAGE_KEY);
65
122
  const lastActivity = lastActivityRaw
66
123
  ? Number.parseInt(lastActivityRaw, 10)
@@ -1,3 +1,4 @@
1
+ export { clearAnalyticsSessionId, setAnalyticsSessionId, } from "./analytics-session.js";
1
2
  import type { SessionReplayOptions, SessionReplayStartResult } from "./session-replay.js";
2
3
  export { scrubUrl } from "./url-scrub.js";
3
4
  export { addErrorBreadcrumb, captureException, captureMessage, isErrorCaptureInstalled, type CaptureExceptionContext, type CapturedExceptionEvent, type ExceptionBreadcrumb, type ExceptionLevel, } from "./error-capture.js";
@@ -1,6 +1,7 @@
1
1
  import { llmConnectionTrackingProperties, } from "../shared/llm-connection.js";
2
2
  import { toPostHogExceptionProperties } from "../tracking/posthog-exception.js";
3
3
  import { getOrCreateAnalyticsAnonymousId, getOrCreateAnalyticsSessionId, } from "./analytics-session.js";
4
+ export { clearAnalyticsSessionId, setAnalyticsSessionId, } from "./analytics-session.js";
4
5
  import { fetchAgentEngineStatus, fetchAuthSessionStatus, } from "./client-status-requests.js";
5
6
  import { installErrorCapture, } from "./error-capture.js";
6
7
  import { isDynamicImportFailureMessage } from "./route-chunk-recovery.js";
@@ -18,7 +18,7 @@ export { DevDatabaseLink, type DevDatabaseLinkProps, } from "./db-admin/DevDatab
18
18
  export { DbAdminPage } from "./db-admin/DbAdminPage.js";
19
19
  export { installRouteChunkRecovery, reloadForStaleChunk, recoverFromStaleChunkError, } from "./route-chunk-recovery.js";
20
20
  export { AgentTerminal, type AgentTerminalProps } from "./terminal/index.js";
21
- export { trackEvent, trackAgentChatLifecycle, trackSessionStatus, configureTracking, setTrackingContentCaptureEnabled, maybeStartSessionReplay, startSessionReplay, stopSessionReplay, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, setSentryUser, setTrackingIdentity, getSessionReplayContext, getSessionReplayUrl, captureError, captureClientException, AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureException, captureMessage, isErrorCaptureInstalled, type CaptureExceptionContext, type CapturedExceptionEvent, type ClientCaptureContext, type AgentChatLifecycleEvent, type ConfigureTrackingOptions, type ErrorCaptureConfigOptions, type ExceptionBreadcrumb, type ExceptionLevel, type FirstTouchAttribution, type SessionReplayOptions, type SessionReplayContext, type SessionReplayLinkOptions, type SessionReplayStartResult, type SessionReplayUrlMatcher, type TrackingIdentityUser, } from "./analytics.js";
21
+ export { trackEvent, trackAgentChatLifecycle, trackSessionStatus, configureTracking, setTrackingContentCaptureEnabled, maybeStartSessionReplay, startSessionReplay, stopSessionReplay, getAnalyticsAnonymousId, getAnalyticsSessionId, setAnalyticsSessionId, clearAnalyticsSessionId, getFirstTouchAttribution, setSentryUser, setTrackingIdentity, getSessionReplayContext, getSessionReplayUrl, captureError, captureClientException, AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureException, captureMessage, isErrorCaptureInstalled, type CaptureExceptionContext, type CapturedExceptionEvent, type ClientCaptureContext, type AgentChatLifecycleEvent, type ConfigureTrackingOptions, type ErrorCaptureConfigOptions, type ExceptionBreadcrumb, type ExceptionLevel, type FirstTouchAttribution, type SessionReplayOptions, type SessionReplayContext, type SessionReplayLinkOptions, type SessionReplayStartResult, type SessionReplayUrlMatcher, type TrackingIdentityUser, } from "./analytics.js";
22
22
  export { track } from "./track.js";
23
23
  export { useCollaborativeDoc, isReconcileLeadClient, emailToColor, emailToName, dedupeCollabUsersByEmail, type UseCollaborativeDocOptions, type UseCollaborativeDocResult, type CollabUser, } from "../collab/client.js";
24
24
  export { AGENT_CLIENT_ID } from "../collab/agent-identity.js";
@@ -18,7 +18,7 @@ export { DevDatabaseLink, } from "./db-admin/DevDatabaseLink.js";
18
18
  export { DbAdminPage } from "./db-admin/DbAdminPage.js";
19
19
  export { installRouteChunkRecovery, reloadForStaleChunk, recoverFromStaleChunkError, } from "./route-chunk-recovery.js";
20
20
  export { AgentTerminal } from "./terminal/index.js";
21
- export { trackEvent, trackAgentChatLifecycle, trackSessionStatus, configureTracking, setTrackingContentCaptureEnabled, maybeStartSessionReplay, startSessionReplay, stopSessionReplay, getAnalyticsAnonymousId, getAnalyticsSessionId, getFirstTouchAttribution, setSentryUser, setTrackingIdentity, getSessionReplayContext, getSessionReplayUrl, captureError, captureClientException,
21
+ export { trackEvent, trackAgentChatLifecycle, trackSessionStatus, configureTracking, setTrackingContentCaptureEnabled, maybeStartSessionReplay, startSessionReplay, stopSessionReplay, getAnalyticsAnonymousId, getAnalyticsSessionId, setAnalyticsSessionId, clearAnalyticsSessionId, getFirstTouchAttribution, setSentryUser, setTrackingIdentity, getSessionReplayContext, getSessionReplayUrl, captureError, captureClientException,
22
22
  // First-party, Sentry-style error capture (auto + manual API).
23
23
  AGENT_NATIVE_EXCEPTION_EVENT_NAME, addErrorBreadcrumb, captureException, captureMessage, isErrorCaptureInstalled, } from "./analytics.js";
24
24
  export { track } from "./track.js";
@@ -1,3 +1,4 @@
1
+ import { getOrCreateAnalyticsSessionId } from "./analytics-session.js";
1
2
  import { agentNativePath } from "./api-path.js";
2
3
  /**
3
4
  * Fire an analytics event from browser/app code. This is the client-side twin
@@ -35,6 +36,7 @@ export function track(name, properties) {
35
36
  // Non-serializable properties — drop rather than throw into the caller.
36
37
  return Promise.resolve();
37
38
  }
39
+ const browserSessionId = getOrCreateAnalyticsSessionId();
38
40
  return fetch(agentNativePath("/_agent-native/track"), {
39
41
  method: "POST",
40
42
  headers: {
@@ -43,6 +45,11 @@ export function track(name, properties) {
43
45
  // middleware trusts it as a first-party marker. Matches the convention
44
46
  // used by other client writes (application-state, guided-questions).
45
47
  "X-Agent-Native-CSRF": "1",
48
+ // Same session the action client and agent chat send, so a client event
49
+ // and the server events from the same visit share one session.
50
+ ...(browserSessionId
51
+ ? { "X-Agent-Native-Session-Id": browserSessionId }
52
+ : {}),
46
53
  },
47
54
  body,
48
55
  keepalive: true,
@@ -22,6 +22,7 @@
22
22
  * compatibility.
23
23
  */
24
24
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
25
+ import { getOrCreateAnalyticsSessionId } from "./analytics-session.js";
25
26
  import { trackEvent } from "./analytics.js";
26
27
  import { agentNativePath } from "./api-path.js";
27
28
  import { getBrowserTabId } from "./browser-tab-id.js";
@@ -192,8 +193,16 @@ async function performActionFetch(name, method, params, options) {
192
193
  if (buildId)
193
194
  headers["X-Agent-Native-Build-Id"] = buildId;
194
195
  const tz = resolveUserTimezone();
195
- if (tz)
196
+ if (tz) {
196
197
  headers["x-user-timezone"] = tz;
198
+ }
199
+ // Same browser session the agent chat sends on an agent run, so an action
200
+ // called from the UI and an action the agent calls during that visit land on
201
+ // one `$session_id` in traces and session replay.
202
+ const browserSessionId = getOrCreateAnalyticsSessionId();
203
+ if (browserSessionId) {
204
+ headers["X-Agent-Native-Session-Id"] = browserSessionId;
205
+ }
197
206
  const init = {
198
207
  method,
199
208
  headers,
@@ -17,11 +17,11 @@ declare const _default: import("../../action.js").ActionDefinition<{
17
17
  id?: undefined;
18
18
  provider?: undefined;
19
19
  } | {
20
- error?: undefined;
21
20
  configured?: undefined;
22
21
  connectPath?: undefined;
23
22
  url: string;
24
23
  id: string;
25
24
  provider: string;
25
+ error?: undefined;
26
26
  }>;
27
27
  export default _default;
@@ -14,7 +14,10 @@ export function buildJobContent(meta, body) {
14
14
  return buildJobResourceContent(meta, body);
15
15
  }
16
16
  const MAX_CONCURRENT_SCHEDULED_JOBS = 8;
17
+ const MAX_IDENTITY_PREFLIGHTS_PER_TICK = MAX_CONCURRENT_SCHEDULED_JOBS * 4;
18
+ const IDENTITY_FAILURE_RETRY_MS = 5 * 60_000;
17
19
  const _activeScheduledJobs = new Set();
20
+ const _preflightingScheduledJobs = new Set();
18
21
  // Skip the DB query on every tick if we recently confirmed no jobs exist.
19
22
  // `_hasJobsCache` is invalidated whenever a `jobs/*` resource is written or
20
23
  // deleted (subscribed below), and refreshed at most every 5 minutes.
@@ -88,7 +91,7 @@ export async function processRecurringJobs(deps) {
88
91
  if (!_hasJobsCache)
89
92
  return;
90
93
  const now = new Date();
91
- const dueJobs = [];
94
+ const dueJobCandidates = [];
92
95
  for (const resource of jobResources) {
93
96
  // Skip non-markdown or .keep files
94
97
  if (!resource.path.endsWith(".md"))
@@ -136,14 +139,56 @@ export async function processRecurringJobs(deps) {
136
139
  // Skip if body is empty
137
140
  if (!body.trim())
138
141
  continue;
142
+ if (hasRecentIdentityFailure(meta, now))
143
+ continue;
139
144
  const key = `${resource.owner}:${resource.path}`;
140
- if (_activeScheduledJobs.has(key))
145
+ if (_activeScheduledJobs.has(key) ||
146
+ _preflightingScheduledJobs.has(key)) {
141
147
  continue;
142
- if (_activeScheduledJobs.size >= MAX_CONCURRENT_SCHEDULED_JOBS)
148
+ }
149
+ dueJobCandidates.push({ key, resource, meta, body });
150
+ }
151
+ const preflightCandidates = [];
152
+ for (const candidate of dueJobCandidates) {
153
+ if (_activeScheduledJobs.size >= MAX_CONCURRENT_SCHEDULED_JOBS ||
154
+ preflightCandidates.length >= MAX_IDENTITY_PREFLIGHTS_PER_TICK) {
155
+ break;
156
+ }
157
+ if (_activeScheduledJobs.has(candidate.key) ||
158
+ _preflightingScheduledJobs.has(candidate.key)) {
143
159
  continue;
144
- _activeScheduledJobs.add(key);
145
- reservedJobKeys.add(key);
146
- dueJobs.push({ key, resource, meta, body });
160
+ }
161
+ preflightCandidates.push(candidate);
162
+ }
163
+ // Identity checks can reject stale jobs that remain due for admin review.
164
+ // Bound the checks so a large blocked backlog cannot consume the whole
165
+ // invocation or make valid jobs wait behind an unbounded stale queue.
166
+ const dueJobs = [];
167
+ for (const candidate of preflightCandidates) {
168
+ _preflightingScheduledJobs.add(candidate.key);
169
+ try {
170
+ const identity = await resolveBackgroundAutomationIdentity({
171
+ name: candidate.resource.path
172
+ .replace(/^jobs\//, "")
173
+ .replace(/\.md$/, ""),
174
+ meta: candidate.meta,
175
+ body: candidate.body,
176
+ resource: candidate.resource,
177
+ });
178
+ if (!identity.ok) {
179
+ await recordIdentityFailure(candidate.resource, candidate.meta, candidate.body, now, identity.reason);
180
+ continue;
181
+ }
182
+ if (_activeScheduledJobs.size >= MAX_CONCURRENT_SCHEDULED_JOBS) {
183
+ break;
184
+ }
185
+ _activeScheduledJobs.add(candidate.key);
186
+ reservedJobKeys.add(candidate.key);
187
+ dueJobs.push(candidate);
188
+ }
189
+ finally {
190
+ _preflightingScheduledJobs.delete(candidate.key);
191
+ }
147
192
  }
148
193
  if (dueJobs.length > 0)
149
194
  dispatchedAt = Date.now();
@@ -219,6 +264,33 @@ async function recoverStaleAutomationHistory(owner, path) {
219
264
  }
220
265
  }
221
266
  export const jobRunCutOffReason = backgroundRunCutOffReason;
267
+ async function recordIdentityFailure(resource, meta, body, now, reason, historyId) {
268
+ const jobName = resource.path.replace(/^jobs\//, "").replace(/\.md$/, "");
269
+ console.warn(`[recurring-jobs] Skipping job "${jobName}": ${reason}. ` +
270
+ `User/membership no longer valid — leaving cron entry for admin review.`);
271
+ // Keep blocked jobs due so an admin can find them, but do not let their
272
+ // persistent failure consume an execution slot on every scheduler sweep.
273
+ const alreadyRecorded = meta.lastStatus === "skipped" && meta.lastError === reason;
274
+ meta.lastCheck = now.toISOString();
275
+ meta.lastStatus = "skipped";
276
+ meta.lastError = reason;
277
+ if (!alreadyRecorded)
278
+ await updateResource(resource, meta, body);
279
+ if (historyId) {
280
+ await finishAutomationRun(historyId, "error", `Automation did not run: ${reason}. No delivery was confirmed.`);
281
+ }
282
+ return { status: "skipped", error: reason };
283
+ }
284
+ function hasRecentIdentityFailure(meta, now) {
285
+ if (meta.lastStatus !== "skipped" || !meta.lastCheck || !meta.lastError) {
286
+ return false;
287
+ }
288
+ const lastCheckMs = Date.parse(meta.lastCheck);
289
+ const elapsedMs = now.getTime() - lastCheckMs;
290
+ return (Number.isFinite(lastCheckMs) &&
291
+ elapsedMs >= 0 &&
292
+ elapsedMs < IDENTITY_FAILURE_RETRY_MS);
293
+ }
222
294
  async function executeJob(resource, meta, body, deps, now, options = {}) {
223
295
  const jobName = resource.path.replace(/^jobs\//, "").replace(/\.md$/, "");
224
296
  const jobContext = {
@@ -234,23 +306,7 @@ async function executeJob(resource, meta, body, deps, now, options = {}) {
234
306
  // failure; leave the cron entry alone so an admin can purge after
235
307
  // investigation.
236
308
  if (!identity.ok) {
237
- console.warn(`[recurring-jobs] Skipping job "${jobName}": ${identity.reason}. ` +
238
- `User/membership no longer valid — leaving cron entry for admin review.`);
239
- // Mark as skipped without resetting nextRun so an admin can find it.
240
- // `lastRun` is deliberately untouched: the job did not run, and stamping
241
- // it here made a permanently blocked job look like it ran every minute.
242
- // Re-writing an unchanged resource on every tick also churns the poll
243
- // stream, so only persist when the failure state actually changed.
244
- const alreadyRecorded = meta.lastStatus === "skipped" && meta.lastError === identity.reason;
245
- meta.lastCheck = now.toISOString();
246
- meta.lastStatus = "skipped";
247
- meta.lastError = identity.reason;
248
- if (!alreadyRecorded)
249
- await updateResource(resource, meta, body);
250
- if (options.historyId) {
251
- await finishAutomationRun(options.historyId, "error", `Automation did not run: ${identity.reason}. No delivery was confirmed.`);
252
- }
253
- return { status: "skipped", error: identity.reason };
309
+ return recordIdentityFailure(resource, meta, body, now, identity.reason, options.historyId);
254
310
  }
255
311
  const jobUserEmail = identity.identity.userEmail;
256
312
  const jobOrgId = identity.identity.orgId;
@@ -137,13 +137,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
137
137
  inputSchema: {
138
138
  type: string;
139
139
  properties: {
140
- count?: undefined;
141
140
  query?: undefined;
142
141
  minutes?: undefined;
143
142
  limit?: undefined;
144
143
  clientHint?: undefined;
145
144
  timestamp?: undefined;
146
145
  chapterId?: undefined;
146
+ count?: undefined;
147
147
  startAt?: undefined;
148
148
  endAt?: undefined;
149
149
  reason?: undefined;
@@ -159,7 +159,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
159
159
  inputSchema: {
160
160
  type: string;
161
161
  properties: {
162
- count?: undefined;
163
162
  query: {
164
163
  type: string;
165
164
  description: string;
@@ -175,6 +174,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
175
174
  clientHint?: undefined;
176
175
  timestamp?: undefined;
177
176
  chapterId?: undefined;
177
+ count?: undefined;
178
178
  startAt?: undefined;
179
179
  endAt?: undefined;
180
180
  reason?: undefined;
@@ -190,7 +190,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
190
190
  inputSchema: {
191
191
  type: string;
192
192
  properties: {
193
- count?: undefined;
194
193
  minutes: {
195
194
  type: string;
196
195
  description: string;
@@ -200,6 +199,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
200
199
  clientHint?: undefined;
201
200
  timestamp?: undefined;
202
201
  chapterId?: undefined;
202
+ count?: undefined;
203
203
  startAt?: undefined;
204
204
  endAt?: undefined;
205
205
  reason?: undefined;
@@ -216,7 +216,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
216
216
  type: string;
217
217
  required: string[];
218
218
  properties: {
219
- count?: undefined;
220
219
  query: {
221
220
  type: string;
222
221
  description: string;
@@ -235,6 +234,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
235
234
  };
236
235
  timestamp?: undefined;
237
236
  chapterId?: undefined;
237
+ count?: undefined;
238
238
  startAt?: undefined;
239
239
  endAt?: undefined;
240
240
  reason?: undefined;
@@ -250,7 +250,6 @@ export declare function screenMemoryMcpToolDefinitions(): ({
250
250
  type: string;
251
251
  required: string[];
252
252
  properties: {
253
- count?: undefined;
254
253
  query?: undefined;
255
254
  minutes?: undefined;
256
255
  limit?: undefined;
@@ -264,6 +263,7 @@ export declare function screenMemoryMcpToolDefinitions(): ({
264
263
  description: string;
265
264
  };
266
265
  chapterId?: undefined;
266
+ count?: undefined;
267
267
  startAt?: undefined;
268
268
  endAt?: undefined;
269
269
  includeMicrophone?: undefined;
@@ -314,13 +314,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
314
314
  type: string;
315
315
  required: string[];
316
316
  properties: {
317
- count?: undefined;
318
317
  query?: undefined;
319
318
  minutes?: undefined;
320
319
  limit?: undefined;
321
320
  clientHint?: undefined;
322
321
  timestamp?: undefined;
323
322
  chapterId?: undefined;
323
+ count?: undefined;
324
324
  startAt: {
325
325
  type: string;
326
326
  description: string;
@@ -349,13 +349,13 @@ export declare function screenMemoryMcpToolDefinitions(): ({
349
349
  type: string;
350
350
  required: string[];
351
351
  properties: {
352
- count?: undefined;
353
352
  query?: undefined;
354
353
  minutes?: undefined;
355
354
  limit?: undefined;
356
355
  clientHint?: undefined;
357
356
  timestamp?: undefined;
358
357
  chapterId?: undefined;
358
+ count?: undefined;
359
359
  startAt?: undefined;
360
360
  endAt?: undefined;
361
361
  reason?: undefined;