@agent-native/core 0.109.2 → 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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +8 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/create.ts +18 -12
- package/corpus/core/src/client/require-session.tsx +19 -3
- package/corpus/core/src/client/use-action.ts +145 -2
- package/corpus/core/src/client/use-session.ts +79 -48
- package/corpus/core/src/db/client.ts +13 -7
- package/corpus/core/src/db/create-get-db.ts +20 -13
- package/corpus/core/src/db/request-telemetry.ts +154 -0
- package/corpus/core/src/integrations/index.ts +1 -0
- package/corpus/core/src/integrations/plugin.ts +9 -3
- package/corpus/core/src/integrations/remote-push-delivery-job.ts +48 -0
- package/corpus/core/src/integrations/remote-push-delivery.ts +370 -0
- package/corpus/core/src/integrations/remote-push-store.ts +275 -26
- package/corpus/core/src/server/better-auth-instance.ts +11 -3
- package/corpus/core/src/server/core-routes-plugin.ts +36 -14
- package/corpus/core/src/server/framework-request-handler.ts +11 -1
- package/corpus/core/src/server/http-response-telemetry.ts +233 -31
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
- package/corpus/templates/clips/actions/finalize-recording.ts +3 -1
- package/corpus/templates/clips/actions/set-mobile-capture-state.ts +4 -1
- package/corpus/templates/clips/app/components/library/library-layout.tsx +1 -6
- package/corpus/templates/clips/changelog/2026-07-18-new-recording-is-now-emphasized-as-the-primary-action-in-the.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-18-recordings-now-remain-safely-recoverable-until-the-stored-me.md +6 -0
- package/corpus/templates/tasks/AGENTS.md +3 -0
- package/corpus/templates/tasks/actions/render-task-list-inline.ts +202 -0
- package/corpus/templates/tasks/changelog/2026-07-18-task-lists-can-now-appear-as-interactive-widgets-inline-in-c.md +6 -0
- package/corpus/templates/tasks/docs/features/f8-chat.mdx +2 -0
- package/corpus/templates/tasks/server/plugins/agent-chat.ts +2 -0
- package/dist/cli/create.d.ts +10 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +17 -13
- package/dist/cli/create.js.map +1 -1
- package/dist/client/require-session.d.ts.map +1 -1
- package/dist/client/require-session.js +6 -3
- package/dist/client/require-session.js.map +1 -1
- package/dist/client/use-action.d.ts.map +1 -1
- package/dist/client/use-action.js +117 -1
- package/dist/client/use-action.js.map +1 -1
- package/dist/client/use-session.d.ts.map +1 -1
- package/dist/client/use-session.js +72 -50
- package/dist/client/use-session.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +7 -1
- package/dist/db/client.js.map +1 -1
- package/dist/db/create-get-db.d.ts +3 -6
- package/dist/db/create-get-db.d.ts.map +1 -1
- package/dist/db/create-get-db.js +9 -2
- package/dist/db/create-get-db.js.map +1 -1
- package/dist/db/request-telemetry.d.ts +23 -0
- package/dist/db/request-telemetry.d.ts.map +1 -0
- package/dist/db/request-telemetry.js +95 -0
- package/dist/db/request-telemetry.js.map +1 -0
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/index.d.ts +1 -0
- package/dist/integrations/index.d.ts.map +1 -1
- package/dist/integrations/index.js +1 -0
- package/dist/integrations/index.js.map +1 -1
- package/dist/integrations/plugin.d.ts.map +1 -1
- package/dist/integrations/plugin.js +8 -3
- package/dist/integrations/plugin.js.map +1 -1
- package/dist/integrations/remote-push-delivery-job.d.ts +4 -0
- package/dist/integrations/remote-push-delivery-job.d.ts.map +1 -0
- package/dist/integrations/remote-push-delivery-job.js +44 -0
- package/dist/integrations/remote-push-delivery-job.js.map +1 -0
- package/dist/integrations/remote-push-delivery.d.ts +11 -0
- package/dist/integrations/remote-push-delivery.d.ts.map +1 -0
- package/dist/integrations/remote-push-delivery.js +265 -0
- package/dist/integrations/remote-push-delivery.js.map +1 -0
- package/dist/integrations/remote-push-store.d.ts +33 -0
- package/dist/integrations/remote-push-store.d.ts.map +1 -1
- package/dist/integrations/remote-push-store.js +195 -26
- package/dist/integrations/remote-push-store.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +3 -3
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/better-auth-instance.js +7 -3
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts +8 -0
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +23 -8
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/framework-request-handler.d.ts.map +1 -1
- package/dist/server/framework-request-handler.js +9 -1
- package/dist/server/framework-request-handler.js.map +1 -1
- package/dist/server/http-response-telemetry.d.ts +3 -2
- package/dist/server/http-response-telemetry.d.ts.map +1 -1
- package/dist/server/http-response-telemetry.js +160 -27
- package/dist/server/http-response-telemetry.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +19 -0
|
@@ -1,18 +1,58 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
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
|
|
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
|
|
132
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
@@ -204,7 +204,9 @@ async function verifyServedMediaUrl(videoUrl: string): Promise<number | null> {
|
|
|
204
204
|
const statusOk = response.status === 200 || response.status === 206;
|
|
205
205
|
if (statusOk) {
|
|
206
206
|
const servedBytes = servedMediaSizeBytes(response);
|
|
207
|
-
if (
|
|
207
|
+
if (servedBytes === null) {
|
|
208
|
+
lastFailure = "Stored media byte count could not be verified";
|
|
209
|
+
} else if (await responseHasReadableMediaBytes(response)) {
|
|
208
210
|
// Builder stable video URLs intentionally replace the source object
|
|
209
211
|
// with a smaller compressed generation at the same URL. A positive,
|
|
210
212
|
// readable object is the invariant here; source-byte equality is
|
|
@@ -6,7 +6,7 @@ export const MOBILE_CAPTURE_STATE_KEY = "mobile-capture-state";
|
|
|
6
6
|
|
|
7
7
|
const mobileCaptureStateSchema = z
|
|
8
8
|
.object({
|
|
9
|
-
view: z.enum(["home", "dictate", "meeting", "video"]),
|
|
9
|
+
view: z.enum(["home", "dictate", "meeting", "video", "clips"]),
|
|
10
10
|
phase: z.enum([
|
|
11
11
|
"idle",
|
|
12
12
|
"ready",
|
|
@@ -15,9 +15,12 @@ const mobileCaptureStateSchema = z
|
|
|
15
15
|
"saving",
|
|
16
16
|
"processing",
|
|
17
17
|
"review",
|
|
18
|
+
"browsing",
|
|
19
|
+
"playing",
|
|
18
20
|
"error",
|
|
19
21
|
]),
|
|
20
22
|
captureId: z.string().trim().min(1).max(200).optional(),
|
|
23
|
+
recordingId: z.string().trim().min(1).max(200).optional(),
|
|
21
24
|
})
|
|
22
25
|
.strict();
|
|
23
26
|
|
|
@@ -384,12 +384,7 @@ export function LibraryLayout({ children }: LibraryLayoutProps) {
|
|
|
384
384
|
) : (
|
|
385
385
|
<>
|
|
386
386
|
<div className="px-3 py-3">
|
|
387
|
-
<Button
|
|
388
|
-
className="w-full gap-1.5"
|
|
389
|
-
variant="outline"
|
|
390
|
-
size="sm"
|
|
391
|
-
asChild
|
|
392
|
-
>
|
|
387
|
+
<Button className="w-full gap-1.5" size="sm" asChild>
|
|
393
388
|
<NavLink to="/record">
|
|
394
389
|
<IconPlayerRecord className="h-4 w-4" />
|
|
395
390
|
{t("navigation.newRecording")}
|
|
@@ -38,6 +38,7 @@ Tasks is a task-list-first agent-native app. The task list at `/tasks` is the de
|
|
|
38
38
|
| `update-visible-task-fields` | POST | Replace which custom fields appear on task cards (max 3) |
|
|
39
39
|
| `view-screen` | — | Read navigation, UI bulk selection, visible tasks, and inbox snapshot |
|
|
40
40
|
| `navigate` | — | Move UI to a view: `tasks`, `inbox`, `fields`, `extensions`, `team` (`home`/`ask` → `tasks`) |
|
|
41
|
+
| `render-task-list-inline` | — | Render an interactive task-list widget inline in chat without leaving the current view |
|
|
41
42
|
|
|
42
43
|
## Store Functions And Transactions
|
|
43
44
|
|
|
@@ -128,6 +129,8 @@ Default navigation shape on `/tasks`:
|
|
|
128
129
|
- Capture in chat → `create-inbox-item` by default; use `create-task` only when the user asks to add directly to the task list.
|
|
129
130
|
- Call `view-screen` before ambiguous edits when the user says "this task", "these tasks", "this inbox item", or "the list".
|
|
130
131
|
- On `/tasks` or `/inbox`, `view-screen` returns `list` (with `items`), optional `selectedItem` (`inListSnapshot`), and optional `selection` (`selectedItems`, `selectedIdsNotInVisibleList`) when bulk-select is active.
|
|
132
|
+
- When the user asks to see, review, or manage tasks while `navigation.view` is not `tasks`, call `render-task-list-inline` instead of navigating away. Pass `includeDone: true` when completed tasks should be included. The widget can add tasks and toggle completion through the existing task actions.
|
|
133
|
+
- When the user is already on `/tasks`, use `view-screen` and the native task list for task-list context unless the user explicitly asks for an inline widget.
|
|
131
134
|
- Prefer `selection.selectedItems` when the user has UI rows selected; fall back to `selectedItem` for a single deep-link highlight.
|
|
132
135
|
- `delete-task`, `bulk-delete-tasks`, and `delete-inbox-item` only after explicit user confirmation in chat.
|
|
133
136
|
- Use `navigation.includeDone` and `list` from `view-screen` to match what the user sees on `/tasks`.
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER,
|
|
3
|
+
defineAction,
|
|
4
|
+
} from "@agent-native/core";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
export const TASK_LIST_INLINE_CONTENT = `
|
|
8
|
+
<script>
|
|
9
|
+
document.addEventListener("alpine:init", () => {
|
|
10
|
+
Alpine.data("taskListWidget", () => ({
|
|
11
|
+
tasks: [],
|
|
12
|
+
draft: "",
|
|
13
|
+
includeDone: false,
|
|
14
|
+
isLoading: true,
|
|
15
|
+
isSaving: false,
|
|
16
|
+
pending: {},
|
|
17
|
+
error: "",
|
|
18
|
+
hasCompletedTasks: false,
|
|
19
|
+
init() {
|
|
20
|
+
this.includeDone = Boolean(window.slotContext?.includeDone);
|
|
21
|
+
window.onSlotContext?.((context) => {
|
|
22
|
+
if (context?.includeDone === undefined) return;
|
|
23
|
+
this.includeDone = Boolean(context.includeDone);
|
|
24
|
+
void this.refresh();
|
|
25
|
+
});
|
|
26
|
+
void this.refresh();
|
|
27
|
+
},
|
|
28
|
+
parseResult(result) {
|
|
29
|
+
if (typeof result === "string") {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(result);
|
|
32
|
+
} catch {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result && typeof result === "object" ? result : {};
|
|
37
|
+
},
|
|
38
|
+
async refresh() {
|
|
39
|
+
this.isLoading = true;
|
|
40
|
+
this.error = "";
|
|
41
|
+
try {
|
|
42
|
+
const data = this.parseResult(
|
|
43
|
+
await appAction("list-tasks", { includeDone: this.includeDone }),
|
|
44
|
+
);
|
|
45
|
+
this.tasks = Array.isArray(data.tasks) ? data.tasks : [];
|
|
46
|
+
this.hasCompletedTasks = data.hasCompletedTasks === true;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
this.error = error?.message || "Could not load tasks.";
|
|
49
|
+
} finally {
|
|
50
|
+
this.isLoading = false;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
summaryLabel() {
|
|
54
|
+
if (this.isLoading) return "Loading your task list…";
|
|
55
|
+
const remaining = this.tasks.filter((task) => !task.done).length;
|
|
56
|
+
return this.includeDone
|
|
57
|
+
? this.tasks.length + " shown · " + remaining + " remaining"
|
|
58
|
+
: remaining + " remaining";
|
|
59
|
+
},
|
|
60
|
+
setIncludeDone(value) {
|
|
61
|
+
this.includeDone = Boolean(value);
|
|
62
|
+
void this.refresh();
|
|
63
|
+
},
|
|
64
|
+
async toggle(task, done) {
|
|
65
|
+
const previous = task.done;
|
|
66
|
+
task.done = Boolean(done);
|
|
67
|
+
this.pending[task.id] = true;
|
|
68
|
+
this.error = "";
|
|
69
|
+
try {
|
|
70
|
+
await appAction("update-task", { taskId: task.id, done: task.done });
|
|
71
|
+
if (!this.includeDone && task.done) {
|
|
72
|
+
this.tasks = this.tasks.filter((item) => item.id !== task.id);
|
|
73
|
+
}
|
|
74
|
+
this.hasCompletedTasks = this.hasCompletedTasks || task.done;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
task.done = previous;
|
|
77
|
+
this.error = error?.message || "Could not update that task.";
|
|
78
|
+
} finally {
|
|
79
|
+
delete this.pending[task.id];
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
async addTask() {
|
|
83
|
+
const title = this.draft.trim();
|
|
84
|
+
if (!title || this.isSaving) return;
|
|
85
|
+
this.isSaving = true;
|
|
86
|
+
this.error = "";
|
|
87
|
+
try {
|
|
88
|
+
await appAction("create-task", { title });
|
|
89
|
+
this.draft = "";
|
|
90
|
+
await this.refresh();
|
|
91
|
+
} catch (error) {
|
|
92
|
+
this.error = error?.message || "Could not add that task.";
|
|
93
|
+
} finally {
|
|
94
|
+
this.isSaving = false;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
}));
|
|
98
|
+
});
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<div x-data="taskListWidget" class="w-full max-w-xl rounded-xl border border-border bg-card p-4 text-card-foreground shadow-sm">
|
|
102
|
+
<div class="flex items-start justify-between gap-3">
|
|
103
|
+
<div class="min-w-0">
|
|
104
|
+
<h2 class="text-sm font-semibold">Tasks</h2>
|
|
105
|
+
<p class="mt-1 text-xs text-muted-foreground" x-text="summaryLabel()"></p>
|
|
106
|
+
</div>
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
class="shrink-0 rounded-md px-2 py-1 text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
|
110
|
+
@click="refresh()"
|
|
111
|
+
:disabled="isLoading"
|
|
112
|
+
>Refresh</button>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<form class="mt-4 flex gap-2" @submit.prevent="addTask()">
|
|
116
|
+
<input
|
|
117
|
+
x-model="draft"
|
|
118
|
+
class="min-w-0 flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm outline-none placeholder:text-muted-foreground focus:ring-2 focus:ring-ring"
|
|
119
|
+
aria-label="New task"
|
|
120
|
+
placeholder="Add a task"
|
|
121
|
+
:disabled="isSaving"
|
|
122
|
+
/>
|
|
123
|
+
<button
|
|
124
|
+
type="submit"
|
|
125
|
+
class="rounded-md bg-primary px-3 py-2 text-sm font-medium text-primary-foreground transition-opacity hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50"
|
|
126
|
+
:disabled="isSaving || !draft.trim()"
|
|
127
|
+
>Add</button>
|
|
128
|
+
</form>
|
|
129
|
+
|
|
130
|
+
<label class="mt-3 flex items-center gap-2 text-xs text-muted-foreground">
|
|
131
|
+
<input
|
|
132
|
+
type="checkbox"
|
|
133
|
+
class="size-3.5 accent-primary"
|
|
134
|
+
:checked="includeDone"
|
|
135
|
+
@change="setIncludeDone($event.target.checked)"
|
|
136
|
+
/>
|
|
137
|
+
<span>Show completed</span>
|
|
138
|
+
</label>
|
|
139
|
+
|
|
140
|
+
<div x-show="error" class="mt-3 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive" role="alert">
|
|
141
|
+
<span x-text="error"></span>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<div x-show="isLoading" class="mt-4 space-y-2" aria-label="Loading tasks">
|
|
145
|
+
<div class="h-10 animate-pulse rounded-md bg-muted"></div>
|
|
146
|
+
<div class="h-10 animate-pulse rounded-md bg-muted"></div>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div x-show="!isLoading && tasks.length === 0" class="mt-5 rounded-lg border border-dashed border-border px-3 py-5 text-center">
|
|
150
|
+
<p class="text-sm font-medium" x-text="hasCompletedTasks && !includeDone ? 'All tasks complete' : 'No tasks yet'"></p>
|
|
151
|
+
<p class="mt-1 text-xs text-muted-foreground" x-text="hasCompletedTasks && !includeDone ? 'Show completed to review them.' : 'Add a task above to get started.'"></p>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<ul x-show="!isLoading && tasks.length > 0" class="mt-4 divide-y divide-border rounded-lg border border-border" aria-label="Tasks list">
|
|
155
|
+
<template x-for="task in tasks" :key="task.id">
|
|
156
|
+
<li class="flex items-center gap-3 px-3 py-3" :class="task.done ? 'text-muted-foreground' : ''">
|
|
157
|
+
<input
|
|
158
|
+
type="checkbox"
|
|
159
|
+
class="size-4 shrink-0 accent-primary"
|
|
160
|
+
:checked="task.done"
|
|
161
|
+
:disabled="pending[task.id]"
|
|
162
|
+
@change="toggle(task, $event.target.checked)"
|
|
163
|
+
:aria-label="task.done ? 'Mark ' + task.title + ' incomplete' : 'Mark ' + task.title + ' complete'"
|
|
164
|
+
/>
|
|
165
|
+
<span class="min-w-0 flex-1 break-words text-sm" :class="task.done ? 'line-through' : ''" x-text="task.title"></span>
|
|
166
|
+
</li>
|
|
167
|
+
</template>
|
|
168
|
+
</ul>
|
|
169
|
+
</div>
|
|
170
|
+
`;
|
|
171
|
+
|
|
172
|
+
export default defineAction({
|
|
173
|
+
description:
|
|
174
|
+
"Render the current task list as an interactive widget inline in chat without navigating away from the current page. Use when the user asks to see, review, or manage tasks while they are not on /tasks.",
|
|
175
|
+
schema: z.object({
|
|
176
|
+
includeDone: z
|
|
177
|
+
.boolean()
|
|
178
|
+
.default(false)
|
|
179
|
+
.describe("When true, show completed tasks in the widget."),
|
|
180
|
+
}),
|
|
181
|
+
http: false,
|
|
182
|
+
readOnly: true,
|
|
183
|
+
chatUI: {
|
|
184
|
+
renderer: ACTION_CHAT_UI_INLINE_EXTENSION_RENDERER,
|
|
185
|
+
title: "Task list",
|
|
186
|
+
description: "Render the task list inline in the conversation.",
|
|
187
|
+
},
|
|
188
|
+
run: async (args) => ({
|
|
189
|
+
ok: true,
|
|
190
|
+
inlineExtension: {
|
|
191
|
+
mode: "transient" as const,
|
|
192
|
+
id: `tasks-inline-${Date.now().toString(36)}-${Math.random()
|
|
193
|
+
.toString(36)
|
|
194
|
+
.slice(2, 8)}`,
|
|
195
|
+
name: "Task list",
|
|
196
|
+
description: args.includeDone ? "Open and completed tasks" : "Open tasks",
|
|
197
|
+
content: TASK_LIST_INLINE_CONTENT,
|
|
198
|
+
context: { includeDone: args.includeDone },
|
|
199
|
+
initialHeight: 360,
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
});
|
|
@@ -24,6 +24,7 @@ The **chat** is a first-class surface of the Tasks app — not a side panel bolt
|
|
|
24
24
|
- **Triage inbox** — list, edit, delete, reorder, and **mark ready** ([F4](./f4-inbox.mdx)).
|
|
25
25
|
- **Filter** the task list the same way as the UI (e.g. hide completed tasks).
|
|
26
26
|
- **Ask what's on screen** — the agent can read the user's current view and selection before acting.
|
|
27
|
+
- **Keep the current page open while reviewing tasks** — when the user is elsewhere in the app, the agent can show an interactive task-list widget inline in chat.
|
|
27
28
|
|
|
28
29
|
<!-- status: todo -->
|
|
29
30
|
|
|
@@ -51,6 +52,7 @@ Every UI capability maps to agent tools (actions). The agent uses these — not
|
|
|
51
52
|
| **[F2 Custom fields](./f2-custom-fields.mdx)** | Create, rename, configure options; set field values; delete fields |
|
|
52
53
|
| **Navigation** | Open inbox, main task page, or a specific task via `navigate` |
|
|
53
54
|
| **Context** | Read current screen and bulk `selection` via `view-screen` |
|
|
55
|
+
| **Inline task list** | Render the task list in chat with `render-task-list-inline` when the user is not on `/tasks` |
|
|
54
56
|
|
|
55
57
|
<!-- status: todo -->
|
|
56
58
|
|