@agent-native/core 0.109.1 → 0.109.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +15 -0
  3. package/corpus/core/docs/content/integrations.mdx +87 -253
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/cli/create.ts +18 -12
  6. package/corpus/core/src/client/blocks/library/diagram.config.ts +10 -1
  7. package/corpus/core/src/client/blocks/library/diagram.tsx +9 -4
  8. package/corpus/core/src/client/require-session.tsx +19 -3
  9. package/corpus/core/src/client/use-action.ts +145 -2
  10. package/corpus/core/src/client/use-session.ts +79 -48
  11. package/corpus/core/src/db/client.ts +13 -7
  12. package/corpus/core/src/db/create-get-db.ts +20 -13
  13. package/corpus/core/src/db/request-telemetry.ts +154 -0
  14. package/corpus/core/src/deploy/build.ts +63 -3
  15. package/corpus/core/src/integrations/index.ts +1 -0
  16. package/corpus/core/src/integrations/plugin.ts +9 -3
  17. package/corpus/core/src/integrations/remote-push-delivery-job.ts +48 -0
  18. package/corpus/core/src/integrations/remote-push-delivery.ts +370 -0
  19. package/corpus/core/src/integrations/remote-push-store.ts +275 -26
  20. package/corpus/core/src/server/better-auth-instance.ts +11 -3
  21. package/corpus/core/src/server/core-routes-plugin.ts +36 -14
  22. package/corpus/core/src/server/framework-request-handler.ts +11 -1
  23. package/corpus/core/src/server/http-response-telemetry.ts +233 -31
  24. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
  25. package/corpus/templates/clips/.agents/skills/dictate/SKILL.md +28 -5
  26. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +7 -0
  27. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +23 -0
  28. package/corpus/templates/clips/AGENTS.md +10 -0
  29. package/corpus/templates/clips/actions/create-dictation.ts +29 -4
  30. package/corpus/templates/clips/actions/finalize-recording.ts +1 -1
  31. package/corpus/templates/clips/actions/get-recording-player-data.ts +16 -6
  32. package/corpus/templates/clips/actions/set-mobile-capture-state.ts +40 -0
  33. package/corpus/templates/clips/actions/update-dictation.ts +9 -1
  34. package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -6
  35. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +13 -2
  36. package/corpus/templates/clips/app/routes/share.$shareId.tsx +13 -2
  37. package/corpus/templates/clips/changelog/2026-07-17-capture-dictation-background-meeting-audio-and-camera-videos.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-17-delayed-upload-verification-keeps-local-backups-retryable.md +6 -0
  39. package/corpus/templates/clips/changelog/2026-07-18-new-recording-is-now-emphasized-as-the-primary-action-in-the.md +6 -0
  40. package/corpus/templates/clips/changelog/2026-07-18-recordings-now-remain-safely-recoverable-until-the-stored-me.md +6 -0
  41. package/corpus/templates/clips/desktop/src/app.tsx +17 -0
  42. package/corpus/templates/clips/desktop/src/lib/processing-backup-recovery.ts +19 -0
  43. package/corpus/templates/clips/desktop/src/lib/recorder.ts +60 -35
  44. package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +12 -0
  45. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  46. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +14 -0
  47. package/corpus/templates/clips/server/db/schema.ts +9 -1
  48. package/corpus/templates/clips/server/lib/builder-media-compression.ts +59 -7
  49. package/corpus/templates/clips/server/lib/media-verification-state.ts +17 -0
  50. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +7 -0
  51. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +22 -1
  52. package/corpus/templates/plan/changelog/2026-07-17-diagrams-can-opt-into-a-polished-clean-rendering-mode-withou.md +6 -0
  53. package/corpus/templates/plan/shared/plan-content.ts +3 -0
  54. package/corpus/templates/tasks/AGENTS.md +3 -0
  55. package/corpus/templates/tasks/actions/render-task-list-inline.ts +202 -0
  56. package/corpus/templates/tasks/changelog/2026-07-18-task-lists-can-now-appear-as-interactive-widgets-inline-in-c.md +6 -0
  57. package/corpus/templates/tasks/docs/features/f8-chat.mdx +2 -0
  58. package/corpus/templates/tasks/server/plugins/agent-chat.ts +2 -0
  59. package/dist/cli/create.d.ts +10 -1
  60. package/dist/cli/create.d.ts.map +1 -1
  61. package/dist/cli/create.js +17 -13
  62. package/dist/cli/create.js.map +1 -1
  63. package/dist/client/blocks/library/diagram.config.d.ts +2 -0
  64. package/dist/client/blocks/library/diagram.config.d.ts.map +1 -1
  65. package/dist/client/blocks/library/diagram.config.js +8 -1
  66. package/dist/client/blocks/library/diagram.config.js.map +1 -1
  67. package/dist/client/blocks/library/diagram.d.ts.map +1 -1
  68. package/dist/client/blocks/library/diagram.js +9 -4
  69. package/dist/client/blocks/library/diagram.js.map +1 -1
  70. package/dist/client/require-session.d.ts.map +1 -1
  71. package/dist/client/require-session.js +6 -3
  72. package/dist/client/require-session.js.map +1 -1
  73. package/dist/client/use-action.d.ts.map +1 -1
  74. package/dist/client/use-action.js +117 -1
  75. package/dist/client/use-action.js.map +1 -1
  76. package/dist/client/use-session.d.ts.map +1 -1
  77. package/dist/client/use-session.js +72 -50
  78. package/dist/client/use-session.js.map +1 -1
  79. package/dist/collab/routes.d.ts +1 -1
  80. package/dist/db/client.d.ts.map +1 -1
  81. package/dist/db/client.js +7 -1
  82. package/dist/db/client.js.map +1 -1
  83. package/dist/db/create-get-db.d.ts +3 -6
  84. package/dist/db/create-get-db.d.ts.map +1 -1
  85. package/dist/db/create-get-db.js +9 -2
  86. package/dist/db/create-get-db.js.map +1 -1
  87. package/dist/db/request-telemetry.d.ts +23 -0
  88. package/dist/db/request-telemetry.d.ts.map +1 -0
  89. package/dist/db/request-telemetry.js +95 -0
  90. package/dist/db/request-telemetry.js.map +1 -0
  91. package/dist/deploy/build.d.ts +3 -0
  92. package/dist/deploy/build.d.ts.map +1 -1
  93. package/dist/deploy/build.js +45 -1
  94. package/dist/deploy/build.js.map +1 -1
  95. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  96. package/dist/integrations/index.d.ts +1 -0
  97. package/dist/integrations/index.d.ts.map +1 -1
  98. package/dist/integrations/index.js +1 -0
  99. package/dist/integrations/index.js.map +1 -1
  100. package/dist/integrations/plugin.d.ts.map +1 -1
  101. package/dist/integrations/plugin.js +8 -3
  102. package/dist/integrations/plugin.js.map +1 -1
  103. package/dist/integrations/remote-push-delivery-job.d.ts +4 -0
  104. package/dist/integrations/remote-push-delivery-job.d.ts.map +1 -0
  105. package/dist/integrations/remote-push-delivery-job.js +44 -0
  106. package/dist/integrations/remote-push-delivery-job.js.map +1 -0
  107. package/dist/integrations/remote-push-delivery.d.ts +11 -0
  108. package/dist/integrations/remote-push-delivery.d.ts.map +1 -0
  109. package/dist/integrations/remote-push-delivery.js +265 -0
  110. package/dist/integrations/remote-push-delivery.js.map +1 -0
  111. package/dist/integrations/remote-push-store.d.ts +33 -0
  112. package/dist/integrations/remote-push-store.d.ts.map +1 -1
  113. package/dist/integrations/remote-push-store.js +195 -26
  114. package/dist/integrations/remote-push-store.js.map +1 -1
  115. package/dist/observability/routes.d.ts +5 -5
  116. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  117. package/dist/resources/handlers.d.ts +1 -1
  118. package/dist/server/better-auth-instance.js +7 -3
  119. package/dist/server/better-auth-instance.js.map +1 -1
  120. package/dist/server/core-routes-plugin.d.ts +8 -0
  121. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  122. package/dist/server/core-routes-plugin.js +23 -8
  123. package/dist/server/core-routes-plugin.js.map +1 -1
  124. package/dist/server/framework-request-handler.d.ts.map +1 -1
  125. package/dist/server/framework-request-handler.js +9 -1
  126. package/dist/server/framework-request-handler.js.map +1 -1
  127. package/dist/server/http-response-telemetry.d.ts +3 -2
  128. package/dist/server/http-response-telemetry.d.ts.map +1 -1
  129. package/dist/server/http-response-telemetry.js +160 -27
  130. package/dist/server/http-response-telemetry.js.map +1 -1
  131. package/dist/server/transcribe-voice.d.ts +1 -1
  132. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
  133. package/docs/content/integrations.mdx +87 -253
  134. package/package.json +1 -1
  135. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
@@ -1,18 +1,58 @@
1
- import { defineEventHandler, getHeader, getMethod } from "h3";
2
- import type { EventHandler, H3Event } from "h3";
1
+ import { randomUUID } from "node:crypto";
3
2
 
3
+ import {
4
+ getHeader,
5
+ getMethod,
6
+ getResponseStatus,
7
+ setResponseHeader,
8
+ setServerTiming,
9
+ } from "h3";
10
+ import type { H3Event } from "h3";
11
+
12
+ import {
13
+ claimStartupDatabaseTelemetry,
14
+ createDatabaseRequestTelemetry,
15
+ enterDatabaseRequestTelemetry,
16
+ type DatabaseRequestTelemetry,
17
+ } from "../db/request-telemetry.js";
4
18
  import { getDatabaseRuntimeFingerprint } from "../db/runtime-diagnostics.js";
5
19
  import { isMcpPublicPath } from "../mcp/route-paths.js";
6
20
  import { track } from "../tracking/index.js";
7
21
  import { getAppName } from "./app-name.js";
8
22
 
9
23
  const TELEMETRY_EVENT_NAME = "http.response";
24
+ const REQUEST_ID_HEADER = "x-agent-native-request-id";
10
25
  const TRACKING_INGEST_PATHS = new Set([
11
26
  "/track",
12
27
  "/api/analytics/track",
13
28
  "/api/events/track",
14
29
  "/_agent-native/track",
15
30
  ]);
31
+ const PROCESS_STATE_KEY = Symbol.for(
32
+ "@agent-native/core/http-response-telemetry.process-state",
33
+ );
34
+ type ProcessTelemetryState = { requestSequence: number };
35
+ type GlobalWithProcessTelemetry = typeof globalThis & {
36
+ [PROCESS_STATE_KEY]?: ProcessTelemetryState;
37
+ };
38
+ const globalRef = globalThis as GlobalWithProcessTelemetry;
39
+ const processState =
40
+ globalRef[PROCESS_STATE_KEY] ??
41
+ (globalRef[PROCESS_STATE_KEY] = { requestSequence: 0 });
42
+ const REQUEST_TELEMETRY_KEY = Symbol.for(
43
+ "@agent-native/core/http-response-telemetry.request",
44
+ );
45
+ const installedApps = new WeakSet<object>();
46
+
47
+ interface HttpRequestTelemetryState {
48
+ startedAt: number;
49
+ requestId: string;
50
+ processAgeAtStartMs: number;
51
+ requestSequence: number;
52
+ frameworkReadyWaitMs: number;
53
+ db: DatabaseRequestTelemetry;
54
+ startupDb?: DatabaseRequestTelemetry;
55
+ }
16
56
 
17
57
  function envValue(key: string): string | undefined {
18
58
  const value = process.env[key]?.trim();
@@ -27,9 +67,9 @@ function boolEnv(key: string): boolean {
27
67
 
28
68
  function sampleRate(): number {
29
69
  const raw = envValue("AGENT_NATIVE_HTTP_TELEMETRY_SAMPLE_RATE");
30
- if (!raw) return 1;
70
+ if (!raw) return 0.1;
31
71
  const parsed = Number.parseFloat(raw);
32
- if (!Number.isFinite(parsed)) return 1;
72
+ if (!Number.isFinite(parsed)) return 0.1;
33
73
  return Math.max(0, Math.min(1, parsed));
34
74
  }
35
75
 
@@ -107,31 +147,63 @@ function organizationForHost(host: string | undefined): string | undefined {
107
147
  : undefined;
108
148
  }
109
149
 
110
- function shouldTrack(pathname: string, statusCode: number): boolean {
150
+ function shouldTrack(
151
+ pathname: string,
152
+ statusCode: number,
153
+ state: HttpRequestTelemetryState,
154
+ ): boolean {
111
155
  if (shouldDisableTelemetry()) return false;
112
156
  if (TRACKING_INGEST_PATHS.has(pathname)) return false;
113
157
  if (pathname.startsWith("/api/analytics/replay")) return false;
114
158
  if (statusCode >= 500) return true;
159
+ if (
160
+ statusCode >= 400 &&
161
+ statusCode < 500 &&
162
+ /(?:^|\/)_agent-native\/actions(?:\/|$)/.test(pathname)
163
+ ) {
164
+ return true;
165
+ }
166
+ if (state.requestSequence === 1) return true;
167
+ if (state.startupDb) return true;
168
+ if (Date.now() - state.startedAt >= 1_000) return true;
169
+ if (state.db.errorCount > 0 || state.db.timeoutCount > 0) {
170
+ return true;
171
+ }
115
172
  const rate = sampleRate();
116
173
  if (rate <= 0) return false;
117
174
  if (rate >= 1) return true;
118
175
  return Math.random() < rate;
119
176
  }
120
177
 
121
- function responseStatusCode(event: H3Event): number {
178
+ function responseStatusCode(event: H3Event, response?: Response): number {
122
179
  const raw =
180
+ response?.status ??
123
181
  (event.node?.res as any)?.statusCode ??
124
182
  (event.node?.res as any)?.status ??
125
- (event as any).res?.status ??
183
+ getResponseStatus(event) ??
126
184
  200;
127
185
  const parsed = Number(raw);
128
186
  return Number.isFinite(parsed) ? parsed : 200;
129
187
  }
130
188
 
131
- function emitTelemetry(event: H3Event, startedAt: number): void {
132
- const statusCode = responseStatusCode(event);
189
+ function runtimeProvider(): string {
190
+ if (process.env.NETLIFY) return "netlify";
191
+ if (process.env.VERCEL) return "vercel";
192
+ if (process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.LAMBDA_TASK_ROOT) {
193
+ return "aws-lambda";
194
+ }
195
+ if (process.env.CF_PAGES) return "cloudflare-pages";
196
+ return "node";
197
+ }
198
+
199
+ function emitTelemetry(
200
+ event: H3Event,
201
+ state: HttpRequestTelemetryState,
202
+ response?: Response,
203
+ ): void {
204
+ const statusCode = responseStatusCode(event, response);
133
205
  const pathname = requestPath(event);
134
- if (!shouldTrack(pathname, statusCode)) return;
206
+ if (!shouldTrack(pathname, statusCode, state)) return;
135
207
 
136
208
  try {
137
209
  const host = hostForEvent(event);
@@ -146,7 +218,17 @@ function emitTelemetry(event: H3Event, startedAt: number): void {
146
218
  route_kind: routeKind(pathname),
147
219
  status_code: statusCode,
148
220
  status_class: statusClass(statusCode),
149
- duration_ms: Math.max(0, Date.now() - startedAt),
221
+ duration_ms: Math.max(0, Date.now() - state.startedAt),
222
+ request_id: state.requestId,
223
+ measurement: "nitro_request",
224
+ cold_start: state.requestSequence === 1,
225
+ request_sequence: state.requestSequence,
226
+ process_age_ms: state.processAgeAtStartMs,
227
+ framework_ready_wait_ms: state.frameworkReadyWaitMs,
228
+ runtime_provider: runtimeProvider(),
229
+ function_name: envValue("AWS_LAMBDA_FUNCTION_NAME"),
230
+ function_memory_mb: envValue("AWS_LAMBDA_FUNCTION_MEMORY_SIZE"),
231
+ region: envValue("AWS_REGION") ?? envValue("VERCEL_REGION"),
150
232
  host,
151
233
  environment: envValue("NODE_ENV"),
152
234
  deploy_context: envValue("CONTEXT") ?? envValue("VERCEL_ENV"),
@@ -161,34 +243,154 @@ function emitTelemetry(event: H3Event, startedAt: number): void {
161
243
  db_url_hash: db.urlHash,
162
244
  db_neon_endpoint: db.neon?.endpointId,
163
245
  db_neon_pooled: db.neon?.pooled,
246
+ db_operation_count: state.db.operationCount,
247
+ db_query_count: state.db.queryCount,
248
+ db_connect_count: state.db.connectCount,
249
+ db_retry_count: state.db.retryCount,
250
+ db_error_count: state.db.errorCount,
251
+ db_timeout_count: state.db.timeoutCount,
252
+ db_operation_total_ms: Math.round(state.db.operationTotalMs),
253
+ db_operation_wall_ms: Math.round(state.db.operationWallMs),
254
+ db_query_total_ms: Math.round(state.db.queryTotalMs),
255
+ db_connect_total_ms: Math.round(state.db.connectTotalMs),
256
+ db_slowest_operation_ms: Math.round(state.db.slowestOperationMs),
257
+ startup_db_operation_count: state.startupDb?.operationCount,
258
+ startup_db_query_count: state.startupDb?.queryCount,
259
+ startup_db_connect_count: state.startupDb?.connectCount,
260
+ startup_db_retry_count: state.startupDb?.retryCount,
261
+ startup_db_error_count: state.startupDb?.errorCount,
262
+ startup_db_timeout_count: state.startupDb?.timeoutCount,
263
+ startup_db_operation_total_ms: state.startupDb
264
+ ? Math.round(state.startupDb.operationTotalMs)
265
+ : undefined,
266
+ startup_db_operation_wall_ms: state.startupDb
267
+ ? Math.round(state.startupDb.operationWallMs)
268
+ : undefined,
269
+ startup_db_query_total_ms: state.startupDb
270
+ ? Math.round(state.startupDb.queryTotalMs)
271
+ : undefined,
272
+ startup_db_connect_total_ms: state.startupDb
273
+ ? Math.round(state.startupDb.connectTotalMs)
274
+ : undefined,
275
+ startup_db_slowest_operation_ms: state.startupDb
276
+ ? Math.round(state.startupDb.slowestOperationMs)
277
+ : undefined,
164
278
  });
165
279
  } catch {
166
280
  // Response telemetry is best-effort. Never perturb request handling.
167
281
  }
168
282
  }
169
283
 
170
- export function createHttpResponseTelemetryMiddleware(): EventHandler {
171
- return defineEventHandler((event) => {
172
- const startedAt = Date.now();
173
- const res = event.node?.res as
174
- | {
175
- once?: (event: "finish" | "close", cb: () => void) => void;
176
- on?: (event: "finish" | "close", cb: () => void) => void;
177
- }
178
- | undefined;
179
- let emitted = false;
180
- const emitOnce = () => {
181
- if (emitted) return;
182
- emitted = true;
183
- emitTelemetry(event, startedAt);
284
+ function requestTelemetryState(
285
+ event: H3Event,
286
+ ): HttpRequestTelemetryState | undefined {
287
+ return (event.context as Record<PropertyKey, unknown>)[
288
+ REQUEST_TELEMETRY_KEY
289
+ ] as HttpRequestTelemetryState | undefined;
290
+ }
291
+
292
+ function appendServerTiming(
293
+ response: Response,
294
+ event: H3Event,
295
+ name: string,
296
+ durationMs: number,
297
+ ): void {
298
+ const duration = Math.max(0, Math.round(durationMs));
299
+ try {
300
+ response.headers.append("server-timing", `${name};dur=${duration}`);
301
+ } catch {
302
+ try {
303
+ setServerTiming(event, name, { dur: duration });
304
+ } catch {
305
+ // Some adapters finalize headers eagerly. Tracking still runs.
306
+ }
307
+ }
308
+ }
309
+
310
+ export function recordFrameworkReadyWait(
311
+ event: H3Event,
312
+ durationMs: number,
313
+ ): void {
314
+ const state = requestTelemetryState(event);
315
+ if (state) {
316
+ state.frameworkReadyWaitMs += Math.max(0, durationMs);
317
+ state.startupDb ??= claimStartupDatabaseTelemetry();
318
+ }
319
+ }
320
+
321
+ export function installHttpResponseTelemetryHooks(nitroApp: any): void {
322
+ if (!nitroApp || installedApps.has(nitroApp)) return;
323
+ const hooks = nitroApp.hooks;
324
+ if (!hooks?.hook) return;
325
+ installedApps.add(nitroApp);
326
+
327
+ hooks.hook("request", (event: H3Event) => {
328
+ const state: HttpRequestTelemetryState = {
329
+ startedAt: Date.now(),
330
+ requestId: randomUUID(),
331
+ processAgeAtStartMs: Math.max(0, Math.round(process.uptime() * 1_000)),
332
+ requestSequence: ++processState.requestSequence,
333
+ frameworkReadyWaitMs: 0,
334
+ db: createDatabaseRequestTelemetry(),
184
335
  };
336
+ (event.context as Record<PropertyKey, unknown>)[REQUEST_TELEMETRY_KEY] =
337
+ state;
338
+ enterDatabaseRequestTelemetry(state.db);
339
+ });
185
340
 
186
- if (typeof res?.once === "function") {
187
- res.once("finish", emitOnce);
188
- res.once("close", emitOnce);
189
- } else if (typeof res?.on === "function") {
190
- res.on("finish", emitOnce);
191
- res.on("close", emitOnce);
341
+ hooks.hook("response", (response: Response, event: H3Event) => {
342
+ const state = requestTelemetryState(event);
343
+ if (!state) return;
344
+
345
+ const durationMs = Math.max(0, Date.now() - state.startedAt);
346
+ try {
347
+ response.headers.set(REQUEST_ID_HEADER, state.requestId);
348
+ } catch {
349
+ try {
350
+ setResponseHeader(event, REQUEST_ID_HEADER, state.requestId);
351
+ } catch {
352
+ // Some adapters finalize headers eagerly. Tracking still has the id.
353
+ }
354
+ }
355
+ appendServerTiming(response, event, "app", durationMs);
356
+ if (state.frameworkReadyWaitMs > 0) {
357
+ appendServerTiming(
358
+ response,
359
+ event,
360
+ "startup",
361
+ state.frameworkReadyWaitMs,
362
+ );
192
363
  }
364
+ if (state.db.operationCount > 0) {
365
+ appendServerTiming(response, event, "db", state.db.operationWallMs);
366
+ appendServerTiming(
367
+ response,
368
+ event,
369
+ "db-connect",
370
+ state.db.connectTotalMs,
371
+ );
372
+ appendServerTiming(
373
+ response,
374
+ event,
375
+ "db-slowest",
376
+ state.db.slowestOperationMs,
377
+ );
378
+ }
379
+ if (state.startupDb && state.startupDb.operationCount > 0) {
380
+ appendServerTiming(
381
+ response,
382
+ event,
383
+ "startup-db",
384
+ state.startupDb.operationWallMs,
385
+ );
386
+ appendServerTiming(
387
+ response,
388
+ event,
389
+ "startup-db-connect",
390
+ state.startupDb.connectTotalMs,
391
+ );
392
+ }
393
+
394
+ emitTelemetry(event, state, response);
193
395
  });
194
396
  }
@@ -138,6 +138,25 @@ Attribution parsing is fully defensive and never blocks signup — a missing/mal
138
138
  Other framework-level baseline events:
139
139
 
140
140
  - `session status` from `useSession()`, with `signed_in`
141
+ - `action.response` from the browser action transport, with action name,
142
+ browser-perceived duration and TTFB, response status/outcome, response size
143
+ when known, and parsed `Server-Timing` phases for framework readiness and
144
+ database work. Its `request_id` joins the exact browser and server events.
145
+ This separates server time from CDN/network/body overhead.
146
+ - `http.response` from Nitro request/response hooks, with normalized path,
147
+ status, request duration, first-request-in-isolate cold marker, process age,
148
+ framework readiness wait, deploy/runtime fingerprint, database
149
+ connection/query counts and timings, retries, timeouts, and failures. It also
150
+ emits `Server-Timing` for `app`, `startup`, `db`, `db-connect`, and
151
+ `db-slowest` plus an `X-Agent-Native-Request-Id` correlation header where
152
+ applicable. Query text and parameters are never captured.
153
+ Database activity that begins during the first two minutes of process/plugin
154
+ initialization is reported separately as `startup_db_*` on the first
155
+ framework request that passes the readiness gate.
156
+ Slow, cold-isolate, server failures, and 4xx action routes are always
157
+ retained; fast successful requests default to 10% sampling. Override with
158
+ `AGENT_NATIVE_HTTP_TELEMETRY_SAMPLE_RATE` on the server and
159
+ `VITE_AGENT_NATIVE_ACTION_TELEMETRY_SAMPLE_RATE` in the browser.
141
160
  - `signup` from Better Auth user creation, with `auth_provider`, `auth_user_id`, and first-touch referral attribution (`referral_source`, `referrer_user`, `referral_medium`, `referral_campaign`, `utm_*`, `first_touch_path`, `landing_referrer` — see "Referral / viral attribution" above)
142
161
  - `builder connect clicked` and `builder connect popup blocked` from browser Connect Builder CTAs
143
162
  - `builder connect started`, `builder connect succeeded`, `builder connect failed`, `builder disconnect succeeded`, and `builder disconnect failed` from the Builder connection routes, with LLM connection context when resolvable
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: dictate
3
3
  description: >-
4
- The Dictate tab in Clips — press-and-hold dictation history (Hold-Fn / Cmd+Shift+Space),
5
- AI cleanup, and the desktop-app integration that captures the audio. Use when listing
6
- past dictations, polishing dictation text, or wiring the desktop tray hand-off.
4
+ The Dictate tab in Clips — press-and-hold desktop dictation, mobile voice
5
+ dictation, history, AI cleanup, and native capture hand-offs. Use when
6
+ listing past dictations, polishing dictation text, or wiring native capture.
7
7
  ---
8
8
 
9
9
  # Dictate
@@ -25,7 +25,9 @@ The press-and-hold UX intentionally mirrors **Wispr Flow**. See `templates/clips
25
25
 
26
26
  ## Data model touched
27
27
 
28
- - **`dictations`** — `id`, `fullText`, `cleanedText`, `durationMs`, `audioUrl`, `source` (`hold-fn` | `hotkey` | `manual`), `createdAt`, `ownableColumns()`.
28
+ - **`dictations`** — `id`, `fullText`, `cleanedText`, `durationMs`, `audioUrl`,
29
+ `source` (desktop shortcuts, `manual`, or `mobile`), `createdAt`,
30
+ `ownableColumns()`.
29
31
 
30
32
  `fullText` is the raw native transcript captured on-device (macOS Speech / Web Speech). `cleanedText` is the optional AI-polished version produced by the shared `cleanup-transcript` pipeline. UI shows `cleanedText ?? fullText`.
31
33
 
@@ -81,7 +83,28 @@ The press-and-hold flow is **owned by the desktop app** (`src-tauri/`). On Hold-
81
83
  3. The dictation row is created via the framework HTTP layer — the agent does not start/stop dictations.
82
84
  4. AI cleanup is a background pass via `cleanup-dictation`, not in the hot path — never block paste-on-release on a network round-trip.
83
85
 
84
- Agents must **never** wire dictation start/stop server-side. The native key listener is the only entry point.
86
+ Agents must **never** wire dictation start/stop server-side. Desktop key
87
+ listeners and the mobile capture UI own those user gestures.
88
+
89
+ ## Mobile dictation
90
+
91
+ The Agent Native iOS/Android app exposes Dictate from native Home, deep links,
92
+ and OS quick actions. Mobile is click-to-toggle rather than hold-to-talk:
93
+
94
+ 1. `expo-audio` records mic-only M4A and persists it under the app documents
95
+ directory before any network request.
96
+ 2. A durable capture-queue row protects the audio across interruptions, app
97
+ restarts, and upload failures.
98
+ 3. The named mobile voice client posts multipart audio to the authenticated
99
+ `/_agent-native/transcribe-voice` route. Provider keys never enter the app.
100
+ 4. The cleaned transcript is editable, copied to the OS clipboard, and saved
101
+ with `create-dictation --source=mobile`; edits use `update-dictation`.
102
+ 5. The audio also syncs to Clips through the resumable recording upload path so
103
+ a failed transcription never destroys the captured speech.
104
+
105
+ Do not auto-send mobile dictation to an agent or another app. iOS/Android quick
106
+ actions open capture; clipboard is the cross-app fallback until platform
107
+ keyboard/accessibility insertion is enabled.
85
108
 
86
109
  ## UI conventions (don't break)
87
110
 
@@ -47,6 +47,13 @@ Meeting capture records **two streams** and tags transcript segments by source:
47
47
 
48
48
  Each transcript segment carries a `source: "mic" | "system"` tag, which we use to attribute action items to the right attendee. This is why **per-attendee action items only work reliably with mic + system capture** — mic-only recordings make remote attendees silent. Document this caveat whenever you surface action items.
49
49
 
50
+ The iOS/Android companion is intentionally different: mobile background meeting
51
+ capture is microphone-only because the operating systems do not expose another
52
+ phone app's Zoom/Meet/Teams audio. It is appropriate for in-person rooms and for
53
+ capturing audio played from a separate device. Mobile recordings still receive
54
+ Clips transcription and summaries, but do not promise desktop-quality remote
55
+ speaker attribution or per-attendee action items.
56
+
50
57
  ## Bidirectional recording ↔ meeting link
51
58
 
52
59
  A meeting can have an associated recording, and a recording can be linked back to a meeting:
@@ -38,6 +38,27 @@ Some recordings are linked to a meeting — when `meeting_id` is non-null on the
38
38
  `processing` to `ready`. Do not wait for the upload/finalize response before
39
39
  opening the page.
40
40
 
41
+ ## Mobile companion lifecycle
42
+
43
+ The Agent Native mobile app uses the same recording rows and binary upload
44
+ routes with native capture primitives:
45
+
46
+ 1. Camera video/import uses `expo-camera` / the system photo picker; meeting
47
+ audio uses `expo-audio` with background recording enabled.
48
+ 2. The native file is copied into the documents directory and a typed
49
+ AsyncStorage capture job is written before upload starts.
50
+ 3. `create-recording` receives a stable client-generated id,
51
+ `sourceAppName: "Agent Native Mobile"`, and the container MIME type.
52
+ 4. Upload reads at most 3 MiB through an Expo FileHandle and persists the next
53
+ chunk index after every acknowledged POST. The 4 MiB server cap still
54
+ applies.
55
+ 5. Foreground/resume reconciliation polls `/api/uploads/:id/status`; retryable
56
+ failures back off and never discard the local file. Completion can post a
57
+ local notification and the recording appears in Clips everywhere.
58
+
59
+ Mobile audio M4A is an MP4 container and uses the recording route's MP4 MIME
60
+ alias. The bytes are not converted or loaded whole into JavaScript memory.
61
+
41
62
  ## Seekable playback (don't ship raw MediaRecorder output)
42
63
 
43
64
  Raw `MediaRecorder` files are not friendly to progressive HTTP playback, which
@@ -191,6 +212,8 @@ export function useRecorder() {
191
212
  - **Never** start a `MediaRecorder` without a user gesture (or a user-initiated `record-intent`).
192
213
  - **Never** re-prompt for permissions on pause/resume — reuse the stream.
193
214
  - **Never** fire the upload from the main thread if the chunks are large — prefer a web worker for anything longer than ~60s.
215
+ - **Never** read a complete mobile recording into JavaScript memory. Use bounded
216
+ FileHandle reads and checkpoint every acknowledged chunk.
194
217
  - The `recordings` row must exist **before** the first chunk is sent.
195
218
  - On every lifecycle change, write `navigation` → `{ view: "record" }` → `{ view: "recording", recordingId }` so the agent can see what's happening.
196
219
  - All AI generated during/after recording goes through the agent chat — see `ai-video-tools`.
@@ -51,6 +51,16 @@ Detailed media, meeting, dictation, editing, and sharing rules live in
51
51
  `previewTruncated` is true, it may end mid-sentence and does not show where
52
52
  transcription ended. Call `get-recording-player-data` before judging
53
53
  completeness or quoting the full transcript.
54
+ - Mobile captures originate in the Agent Native iOS/Android app. The phone
55
+ persists each audio/video file before network work, creates the recording
56
+ with `sourceAppName: "Agent Native Mobile"`, and resumes bounded chunk
57
+ uploads from its durable queue. Do not ask users to keep a capture screen
58
+ open or re-record after a transient upload failure; reconnect Clips and retry
59
+ the saved job from mobile Home.
60
+ - Mobile meeting capture is microphone-only. It can record an in-person room or
61
+ audio playing from another device, but iOS and Android do not expose another
62
+ phone app's Zoom/Meet/Teams call audio to this flow. Do not claim mobile
63
+ attendee attribution has the desktop mic-plus-system-audio fidelity.
54
64
  - Dictation cleanup, Clip title/cleanup, and meeting summaries should pass
55
65
  bounded `voiceContext` to the shared cleanup/transcription path when active
56
66
  app context, learned vocabulary, user notes, or AGENTS.md preferences are
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Create a dictation row from browser or desktop voice capture.
2
+ * Create a dictation row from browser, desktop, or mobile voice capture.
3
3
  */
4
4
 
5
5
  import { defineAction } from "@agent-native/core";
6
6
  import { writeAppState } from "@agent-native/core/application-state";
7
+ import { and, eq } from "drizzle-orm";
7
8
  import { z } from "zod";
8
9
 
9
10
  import { getDb, schema } from "../server/db/index.js";
@@ -11,18 +12,28 @@ import {
11
12
  getActiveOrganizationId,
12
13
  getCurrentOwnerEmail,
13
14
  nanoid,
15
+ ownerEmailMatches,
14
16
  } from "../server/lib/recordings.js";
15
17
 
16
18
  export default defineAction({
17
19
  description:
18
- "Create a dictation history row from captured voice text. Used by browser Dictate and desktop voice capture.",
20
+ "Create a dictation history row from captured voice text. Used by browser, desktop, and mobile voice capture.",
19
21
  schema: z.object({
22
+ id: z.string().min(1).optional().describe("Stable client-generated id"),
20
23
  fullText: z.string().min(1).describe("Raw dictation text"),
21
24
  cleanedText: z.string().nullable().optional(),
22
25
  durationMs: z.coerce.number().int().min(0).optional(),
23
26
  audioUrl: z.string().url().nullable().optional(),
24
27
  source: z
25
- .enum(["fn-hold", "cmd-shift-space", "manual", "other", "fn", "custom"])
28
+ .enum([
29
+ "fn-hold",
30
+ "cmd-shift-space",
31
+ "manual",
32
+ "mobile",
33
+ "other",
34
+ "fn",
35
+ "custom",
36
+ ])
26
37
  .default("manual"),
27
38
  targetApp: z.string().nullable().optional(),
28
39
  startedAt: z.string().datetime().optional(),
@@ -32,10 +43,24 @@ export default defineAction({
32
43
  const ownerEmail = getCurrentOwnerEmail();
33
44
  const orgId = await getActiveOrganizationId().catch(() => null);
34
45
  const now = new Date().toISOString();
35
- const id = nanoid();
46
+ const id = args.id ?? nanoid();
36
47
  const fullText = args.fullText.trim();
37
48
  if (!fullText) throw new Error("Dictation text is required");
38
49
 
50
+ if (args.id) {
51
+ const [existing] = await db
52
+ .select()
53
+ .from(schema.dictations)
54
+ .where(
55
+ and(
56
+ eq(schema.dictations.id, id),
57
+ ownerEmailMatches(schema.dictations.ownerEmail, ownerEmail),
58
+ ),
59
+ )
60
+ .limit(1);
61
+ if (existing) return existing;
62
+ }
63
+
39
64
  await db.insert(schema.dictations).values({
40
65
  id,
41
66
  fullText,
@@ -1670,7 +1670,7 @@ export default defineAction({
1670
1670
  servedBytes = await verifyServedMediaUrl(upload.url);
1671
1671
  } catch (err) {
1672
1672
  const failureReason = err instanceof Error ? err.message : String(err);
1673
- return leaveRecordingProcessingForMediaVerification({
1673
+ return await leaveRecordingProcessingForMediaVerification({
1674
1674
  id,
1675
1675
  ownerEmail,
1676
1676
  failureReason,
@@ -29,6 +29,7 @@ import { and, asc, eq, or, sql } from "drizzle-orm";
29
29
  import { z } from "zod";
30
30
 
31
31
  import { getDb, schema } from "../server/db/index.js";
32
+ import { isMediaVerificationPending } from "../server/lib/media-verification-state.js";
32
33
  import { resolvePlayerVideoUrl } from "../server/lib/player-video-url.js";
33
34
  import {
34
35
  canOpenDirectRecordingPage,
@@ -178,12 +179,20 @@ export default defineAction({
178
179
  access.role === "owner" ||
179
180
  access.role === "admin" ||
180
181
  access.role === "editor";
181
- const [cleanupStateRaw, builderCreditsRaw] = await Promise.all([
182
- readAppState(`transcript-cleanup-${args.recordingId}`).catch(() => null),
183
- canEditRecording
184
- ? readAppState(CLIPS_BUILDER_CREDITS_STATE_KEY).catch(() => null)
185
- : Promise.resolve(null),
186
- ]);
182
+ const [cleanupStateRaw, builderCreditsRaw, verificationPending] =
183
+ await Promise.all([
184
+ readAppState(`transcript-cleanup-${args.recordingId}`).catch(
185
+ () => null,
186
+ ),
187
+ canEditRecording
188
+ ? readAppState(CLIPS_BUILDER_CREDITS_STATE_KEY).catch(() => null)
189
+ : Promise.resolve(null),
190
+ isMediaVerificationPending({
191
+ ownerEmail: rec.ownerEmail,
192
+ recordingId: args.recordingId,
193
+ recordingStatus: rec.status,
194
+ }),
195
+ ]);
187
196
  const cleanupState =
188
197
  cleanupStateRaw && typeof cleanupStateRaw === "object"
189
198
  ? (cleanupStateRaw as Record<string, unknown>)
@@ -326,6 +335,7 @@ export default defineAction({
326
335
  hasAudio: Boolean(rec.hasAudio),
327
336
  hasCamera: Boolean(rec.hasCamera),
328
337
  status: rec.status,
338
+ verificationPending,
329
339
  uploadProgress: rec.uploadProgress,
330
340
  failureReason: rec.failureReason,
331
341
  // Don't leak the password to clients (especially to MCP hosts that
@@ -0,0 +1,40 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { writeAppState } from "@agent-native/core/application-state";
3
+ import { z } from "zod";
4
+
5
+ export const MOBILE_CAPTURE_STATE_KEY = "mobile-capture-state";
6
+
7
+ const mobileCaptureStateSchema = z
8
+ .object({
9
+ view: z.enum(["home", "dictate", "meeting", "video", "clips"]),
10
+ phase: z.enum([
11
+ "idle",
12
+ "ready",
13
+ "recording",
14
+ "paused",
15
+ "saving",
16
+ "processing",
17
+ "review",
18
+ "browsing",
19
+ "playing",
20
+ "error",
21
+ ]),
22
+ captureId: z.string().trim().min(1).max(200).optional(),
23
+ recordingId: z.string().trim().min(1).max(200).optional(),
24
+ })
25
+ .strict();
26
+
27
+ export default defineAction({
28
+ description:
29
+ "Record the privacy-safe view and phase of the Agent Native mobile capture companion.",
30
+ agentTool: false,
31
+ schema: mobileCaptureStateSchema,
32
+ run: async (args) => {
33
+ const state = {
34
+ ...args,
35
+ updatedAt: new Date().toISOString(),
36
+ };
37
+ await writeAppState(MOBILE_CAPTURE_STATE_KEY, state);
38
+ return state;
39
+ },
40
+ });
@@ -20,7 +20,15 @@ export default defineAction({
20
20
  fullText: z.string().optional(),
21
21
  cleanedText: z.string().nullable().optional(),
22
22
  source: z
23
- .enum(["fn-hold", "cmd-shift-space", "manual", "other", "fn", "custom"])
23
+ .enum([
24
+ "fn-hold",
25
+ "cmd-shift-space",
26
+ "manual",
27
+ "mobile",
28
+ "other",
29
+ "fn",
30
+ "custom",
31
+ ])
24
32
  .optional(),
25
33
  targetApp: z.string().nullable().optional(),
26
34
  }),