@agent-native/core 0.109.0 → 0.109.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.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/docs/content/integrations.mdx +87 -253
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +24 -2
- package/corpus/core/src/a2a/correlation.ts +50 -0
- package/corpus/core/src/a2a/handlers.ts +140 -14
- package/corpus/core/src/a2a/index.ts +1 -0
- package/corpus/core/src/a2a/invoke.ts +10 -1
- package/corpus/core/src/a2a/task-store.ts +146 -6
- package/corpus/core/src/a2a/types.ts +16 -0
- package/corpus/core/src/action.ts +2 -0
- package/corpus/core/src/agent/production-agent.ts +23 -0
- package/corpus/core/src/client/blocks/library/diagram.config.ts +10 -1
- package/corpus/core/src/client/blocks/library/diagram.tsx +9 -4
- package/corpus/core/src/client/use-run-stuck-detection.ts +27 -1
- package/corpus/core/src/db/client.ts +55 -0
- package/corpus/core/src/deploy/build.ts +63 -3
- package/corpus/core/src/observability/traces.ts +172 -19
- package/corpus/core/src/scripts/call-agent.ts +53 -2
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +72 -25
- package/corpus/core/src/server/agent-chat-plugin.ts +107 -6
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
- package/corpus/templates/analytics/changelog/2026-07-17-analytics-now-includes-a-default-alert-for-spikes-in-stuck-agent-chats.md +6 -0
- package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +23 -14
- package/corpus/templates/analytics/docs/schemas/tracked-events.md +5 -1
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +3 -3
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +209 -87
- package/corpus/templates/clips/.agents/skills/dictate/SKILL.md +28 -5
- package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +7 -0
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +23 -0
- package/corpus/templates/clips/AGENTS.md +10 -0
- package/corpus/templates/clips/actions/create-dictation.ts +29 -4
- package/corpus/templates/clips/actions/finalize-recording.ts +542 -65
- package/corpus/templates/clips/actions/get-recording-player-data.ts +16 -6
- package/corpus/templates/clips/actions/set-mobile-capture-state.ts +37 -0
- package/corpus/templates/clips/actions/update-dictation.ts +9 -1
- package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +17 -2
- package/corpus/templates/clips/app/components/player/video-player.tsx +14 -2
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +2 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +14 -3
- package/corpus/templates/clips/app/routes/record.tsx +3 -3
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +19 -5
- package/corpus/templates/clips/changelog/2026-07-17-capture-dictation-background-meeting-audio-and-camera-videos.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-clips-now-retries-storage-verification-in-the-background-ins.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-delayed-upload-verification-keeps-local-backups-retryable.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-fixed-broken-thumbnail-icons-appearing-over-playable-clip-em.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-playback-comments-stay-readable-at-faster-playback-speeds.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +3 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +3 -3
- package/corpus/templates/clips/desktop/src/app.tsx +35 -6
- package/corpus/templates/clips/desktop/src/lib/processing-backup-recovery.ts +19 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +141 -18
- package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +28 -7
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -4
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +32 -15
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +59 -18
- package/corpus/templates/clips/server/db/schema.ts +9 -1
- package/corpus/templates/clips/server/jobs/media-verification.ts +130 -0
- package/corpus/templates/clips/server/lib/builder-media-compression.ts +74 -7
- package/corpus/templates/clips/server/lib/media-verification-state.ts +55 -0
- package/corpus/templates/clips/server/lib/player-thumbnail-url.ts +31 -0
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +10 -3
- package/corpus/templates/clips/server/plugins/auth.ts +5 -5
- package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +12 -2
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +13 -1
- package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +340 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +24 -7
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +91 -7
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +22 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +14 -0
- package/corpus/templates/clips/shared/finalize-recovery.ts +61 -2
- package/corpus/templates/clips/shared/share-meta.ts +1 -1
- package/corpus/templates/plan/changelog/2026-07-17-diagrams-can-opt-into-a-polished-clean-rendering-mode-withou.md +6 -0
- package/corpus/templates/plan/shared/plan-content.ts +3 -0
- package/dist/a2a/client.d.ts +11 -2
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +16 -3
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/correlation.d.ts +10 -0
- package/dist/a2a/correlation.d.ts.map +1 -0
- package/dist/a2a/correlation.js +40 -0
- package/dist/a2a/correlation.js.map +1 -0
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +100 -15
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/index.d.ts +1 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/a2a/invoke.d.ts +4 -1
- package/dist/a2a/invoke.d.ts.map +1 -1
- package/dist/a2a/invoke.js +3 -0
- package/dist/a2a/invoke.js.map +1 -1
- package/dist/a2a/task-store.d.ts +12 -1
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +96 -6
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/a2a/types.d.ts +15 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/action.d.ts +2 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +9 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +14 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/blocks/library/diagram.config.d.ts +2 -0
- package/dist/client/blocks/library/diagram.config.d.ts.map +1 -1
- package/dist/client/blocks/library/diagram.config.js +8 -1
- package/dist/client/blocks/library/diagram.config.js.map +1 -1
- package/dist/client/blocks/library/diagram.d.ts.map +1 -1
- package/dist/client/blocks/library/diagram.js +9 -4
- package/dist/client/blocks/library/diagram.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +25 -1
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/db/client.d.ts +7 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +33 -0
- package/dist/db/client.js.map +1 -1
- package/dist/deploy/build.d.ts +3 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +45 -1
- package/dist/deploy/build.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/observability/traces.d.ts +9 -0
- package/dist/observability/traces.d.ts.map +1 -1
- package/dist/observability/traces.js +137 -19
- package/dist/observability/traces.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +35 -3
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.d.ts +18 -2
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +35 -7
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +93 -6
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/docs/content/integrations.mdx +87 -253
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { getDbExec } from "@agent-native/core/db";
|
|
2
|
+
import { runWithRequestContext } from "@agent-native/core/server";
|
|
3
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
4
|
+
|
|
5
|
+
import finalizeRecording from "../../actions/finalize-recording.js";
|
|
6
|
+
import { getDb, schema } from "../db/index.js";
|
|
7
|
+
import {
|
|
8
|
+
MEDIA_VERIFICATION_STATE_PREFIX,
|
|
9
|
+
parseMediaVerificationMarker,
|
|
10
|
+
} from "../lib/media-verification-state.js";
|
|
11
|
+
import { ownerEmailMatches } from "../lib/recordings.js";
|
|
12
|
+
|
|
13
|
+
const SWEEP_INTERVAL_MS = 60_000;
|
|
14
|
+
const DISPATCH_FALLBACK_GRACE_MS = 30_000;
|
|
15
|
+
const MAX_ATTEMPTS = 10;
|
|
16
|
+
let skippingLogged = false;
|
|
17
|
+
|
|
18
|
+
export async function runMediaVerificationSweepOnce(): Promise<void> {
|
|
19
|
+
const { rows } = await getDbExec().execute({
|
|
20
|
+
sql: `SELECT session_id, key, value FROM application_state WHERE key LIKE ?`,
|
|
21
|
+
args: [`${MEDIA_VERIFICATION_STATE_PREFIX}%`],
|
|
22
|
+
});
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
|
|
25
|
+
for (const row of rows as Array<{
|
|
26
|
+
session_id?: unknown;
|
|
27
|
+
key?: unknown;
|
|
28
|
+
value?: unknown;
|
|
29
|
+
}>) {
|
|
30
|
+
const sessionId =
|
|
31
|
+
typeof row.session_id === "string" ? row.session_id.trim() : "";
|
|
32
|
+
const key = typeof row.key === "string" ? row.key : "";
|
|
33
|
+
const recordingId = key.startsWith(MEDIA_VERIFICATION_STATE_PREFIX)
|
|
34
|
+
? key.slice(MEDIA_VERIFICATION_STATE_PREFIX.length)
|
|
35
|
+
: "";
|
|
36
|
+
const rawValue = typeof row.value === "string" ? row.value : "";
|
|
37
|
+
let rawState: unknown;
|
|
38
|
+
try {
|
|
39
|
+
rawState = JSON.parse(rawValue);
|
|
40
|
+
} catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const marker = parseMediaVerificationMarker(rawState);
|
|
44
|
+
if (
|
|
45
|
+
!sessionId ||
|
|
46
|
+
!recordingId ||
|
|
47
|
+
!marker ||
|
|
48
|
+
marker.recordingId !== recordingId
|
|
49
|
+
) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const nextAttemptAt = Date.parse(marker.nextAttemptAt);
|
|
54
|
+
const leaseUntil = marker.leaseUntil
|
|
55
|
+
? Date.parse(marker.leaseUntil)
|
|
56
|
+
: Number.NEGATIVE_INFINITY;
|
|
57
|
+
const due =
|
|
58
|
+
marker.status === "pending"
|
|
59
|
+
? now >= nextAttemptAt + DISPATCH_FALLBACK_GRACE_MS
|
|
60
|
+
: now >= leaseUntil;
|
|
61
|
+
if (marker.completedAttempts >= MAX_ATTEMPTS || !due) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const [recording] = await getDb()
|
|
67
|
+
.select({
|
|
68
|
+
ownerEmail: schema.recordings.ownerEmail,
|
|
69
|
+
orgId: schema.recordings.orgId,
|
|
70
|
+
})
|
|
71
|
+
.from(schema.recordings)
|
|
72
|
+
.where(
|
|
73
|
+
and(
|
|
74
|
+
eq(schema.recordings.id, recordingId),
|
|
75
|
+
ownerEmailMatches(schema.recordings.ownerEmail, sessionId),
|
|
76
|
+
eq(schema.recordings.status, "processing"),
|
|
77
|
+
isNull(schema.recordings.trashedAt),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
.limit(1);
|
|
81
|
+
if (!recording) continue;
|
|
82
|
+
|
|
83
|
+
await runWithRequestContext(
|
|
84
|
+
{
|
|
85
|
+
userEmail: recording.ownerEmail,
|
|
86
|
+
orgId: recording.orgId ?? undefined,
|
|
87
|
+
},
|
|
88
|
+
async () => {
|
|
89
|
+
await finalizeRecording.run({
|
|
90
|
+
id: recordingId,
|
|
91
|
+
mediaVerificationRetryAttempt: Math.min(
|
|
92
|
+
MAX_ATTEMPTS,
|
|
93
|
+
marker.completedAttempts + 1,
|
|
94
|
+
),
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
console.warn("[media-verification] sweep item failed", {
|
|
100
|
+
key: String(row.key ?? ""),
|
|
101
|
+
recordingId,
|
|
102
|
+
error: err instanceof Error ? err.message : String(err),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export default function registerMediaVerificationJob(): void {
|
|
109
|
+
const isProd = process.env.NODE_ENV === "production";
|
|
110
|
+
const flag = process.env.RUN_BACKGROUND_JOBS;
|
|
111
|
+
const enabled = flag === "1" || (isProd && flag !== "0");
|
|
112
|
+
if (!enabled) {
|
|
113
|
+
if (process.env.DEBUG && !skippingLogged) {
|
|
114
|
+
console.log(
|
|
115
|
+
"[media-verification] Skipping background sweep (set RUN_BACKGROUND_JOBS=1 to enable in dev).",
|
|
116
|
+
);
|
|
117
|
+
skippingLogged = true;
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
setInterval(() => {
|
|
123
|
+
runMediaVerificationSweepOnce().catch((err) =>
|
|
124
|
+
console.error("[media-verification] interval failed:", err),
|
|
125
|
+
);
|
|
126
|
+
}, SWEEP_INTERVAL_MS);
|
|
127
|
+
console.log(
|
|
128
|
+
`[media-verification] Recurring recovery sweep every ${SWEEP_INTERVAL_MS / 1000}s.`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
resolveBuilderPrivateKey,
|
|
10
10
|
runWithRequestContext,
|
|
11
11
|
} from "@agent-native/core/server";
|
|
12
|
-
import { and, eq } from "drizzle-orm";
|
|
12
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
13
13
|
|
|
14
14
|
import type { MediaWorkerCallback } from "../../shared/media-worker-contract.js";
|
|
15
15
|
import { getDb, schema } from "../db/index.js";
|
|
@@ -648,6 +648,21 @@ async function processCompressionState(
|
|
|
648
648
|
try {
|
|
649
649
|
const compressed = await triggerBuilderCompression(state);
|
|
650
650
|
if (compressed) {
|
|
651
|
+
if (!compressed.durationMs) {
|
|
652
|
+
if (attempts >= MAX_TRIGGER_ATTEMPTS) {
|
|
653
|
+
return markCompressionFailed(
|
|
654
|
+
{ ...state, attempts },
|
|
655
|
+
`Builder media compression did not provide a verified duration after ${attempts} attempts`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
return writeCompressionState(state, {
|
|
659
|
+
status: "triggered",
|
|
660
|
+
attempts,
|
|
661
|
+
lastTriggeredAt: new Date().toISOString(),
|
|
662
|
+
nextAttemptAt: isoAfter(TRIGGERED_POLL_DELAY_MS),
|
|
663
|
+
detail: "Compressed media is not ready with a verified duration yet",
|
|
664
|
+
});
|
|
665
|
+
}
|
|
651
666
|
return swapRecordingToCompressed(
|
|
652
667
|
state,
|
|
653
668
|
compressed.url,
|
|
@@ -943,7 +958,12 @@ export async function runBuilderMediaCompressionSweepOnce(): Promise<void> {
|
|
|
943
958
|
key?: unknown;
|
|
944
959
|
value?: unknown;
|
|
945
960
|
}>) {
|
|
946
|
-
const sessionId =
|
|
961
|
+
const sessionId =
|
|
962
|
+
typeof row.session_id === "string" ? row.session_id.trim() : "";
|
|
963
|
+
const key = typeof row.key === "string" ? row.key : "";
|
|
964
|
+
const recordingId = key.startsWith(BUILDER_MEDIA_COMPRESSION_STATE_PREFIX)
|
|
965
|
+
? key.slice(BUILDER_MEDIA_COMPRESSION_STATE_PREFIX.length)
|
|
966
|
+
: "";
|
|
947
967
|
const rawValue = typeof row.value === "string" ? row.value : "";
|
|
948
968
|
let value: Record<string, unknown> | null = null;
|
|
949
969
|
try {
|
|
@@ -952,14 +972,61 @@ export async function runBuilderMediaCompressionSweepOnce(): Promise<void> {
|
|
|
952
972
|
continue;
|
|
953
973
|
}
|
|
954
974
|
const state = parseState(value);
|
|
955
|
-
if (
|
|
956
|
-
|
|
957
|
-
|
|
975
|
+
if (
|
|
976
|
+
!sessionId ||
|
|
977
|
+
!recordingId ||
|
|
978
|
+
!state ||
|
|
979
|
+
state.recordingId !== recordingId ||
|
|
980
|
+
isFinalStatus(state.status) ||
|
|
981
|
+
!isDue(state)
|
|
982
|
+
) {
|
|
983
|
+
continue;
|
|
984
|
+
}
|
|
958
985
|
try {
|
|
986
|
+
const [recording] = await getDb()
|
|
987
|
+
.select({
|
|
988
|
+
ownerEmail: schema.recordings.ownerEmail,
|
|
989
|
+
orgId: schema.recordings.orgId,
|
|
990
|
+
videoUrl: schema.recordings.videoUrl,
|
|
991
|
+
})
|
|
992
|
+
.from(schema.recordings)
|
|
993
|
+
.where(
|
|
994
|
+
and(
|
|
995
|
+
eq(schema.recordings.id, recordingId),
|
|
996
|
+
ownerEmailMatches(schema.recordings.ownerEmail, sessionId),
|
|
997
|
+
eq(schema.recordings.status, "ready"),
|
|
998
|
+
isNull(schema.recordings.trashedAt),
|
|
999
|
+
),
|
|
1000
|
+
)
|
|
1001
|
+
.limit(1);
|
|
1002
|
+
const expectedTarget = extractBuilderMediaTarget(
|
|
1003
|
+
recording?.videoUrl ?? "",
|
|
1004
|
+
);
|
|
1005
|
+
if (
|
|
1006
|
+
!recording ||
|
|
1007
|
+
!expectedTarget ||
|
|
1008
|
+
state.sourceUrl !== expectedTarget.sourceUrl ||
|
|
1009
|
+
(state.mediaWorker &&
|
|
1010
|
+
(state.mediaWorker.jobId !==
|
|
1011
|
+
mediaWorkerCompressionJobId(recordingId) ||
|
|
1012
|
+
state.mediaWorker.outputUrl !== expectedTarget.compressedUrl))
|
|
1013
|
+
) {
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const trustedState: BuilderMediaCompressionState = {
|
|
1018
|
+
...state,
|
|
1019
|
+
...expectedTarget,
|
|
1020
|
+
ownerEmail: recording.ownerEmail,
|
|
1021
|
+
orgId: recording.orgId,
|
|
1022
|
+
};
|
|
959
1023
|
await runWithRequestContext(
|
|
960
|
-
{
|
|
1024
|
+
{
|
|
1025
|
+
userEmail: recording.ownerEmail,
|
|
1026
|
+
orgId: recording.orgId ?? undefined,
|
|
1027
|
+
},
|
|
961
1028
|
async () => {
|
|
962
|
-
await processCompressionState(
|
|
1029
|
+
await processCompressionState(trustedState);
|
|
963
1030
|
},
|
|
964
1031
|
);
|
|
965
1032
|
} catch (err) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { appStateGet } from "@agent-native/core/application-state";
|
|
2
|
+
|
|
3
|
+
export const MEDIA_VERIFICATION_STATE_PREFIX = "recording-media-verification-";
|
|
4
|
+
|
|
5
|
+
export type MediaVerificationMarker = {
|
|
6
|
+
recordingId: string;
|
|
7
|
+
status: "pending" | "leased";
|
|
8
|
+
completedAttempts: number;
|
|
9
|
+
nextAttemptAt: string;
|
|
10
|
+
leaseUntil: string | null;
|
|
11
|
+
updatedAt: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function mediaVerificationStateKey(recordingId: string): string {
|
|
15
|
+
return `${MEDIA_VERIFICATION_STATE_PREFIX}${recordingId}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseMediaVerificationMarker(
|
|
19
|
+
value: unknown,
|
|
20
|
+
): MediaVerificationMarker | null {
|
|
21
|
+
if (!value || typeof value !== "object") return null;
|
|
22
|
+
const state = value as Record<string, unknown>;
|
|
23
|
+
if (
|
|
24
|
+
typeof state.recordingId !== "string" ||
|
|
25
|
+
!state.recordingId ||
|
|
26
|
+
(state.status !== "pending" && state.status !== "leased") ||
|
|
27
|
+
typeof state.completedAttempts !== "number" ||
|
|
28
|
+
!Number.isInteger(state.completedAttempts) ||
|
|
29
|
+
state.completedAttempts < 0 ||
|
|
30
|
+
typeof state.nextAttemptAt !== "string" ||
|
|
31
|
+
!Number.isFinite(Date.parse(state.nextAttemptAt)) ||
|
|
32
|
+
(state.leaseUntil !== null &&
|
|
33
|
+
(typeof state.leaseUntil !== "string" ||
|
|
34
|
+
!Number.isFinite(Date.parse(state.leaseUntil)))) ||
|
|
35
|
+
typeof state.updatedAt !== "string"
|
|
36
|
+
) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return state as MediaVerificationMarker;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function isMediaVerificationPending(args: {
|
|
43
|
+
ownerEmail: string;
|
|
44
|
+
recordingId: string;
|
|
45
|
+
recordingStatus: string;
|
|
46
|
+
}): Promise<boolean> {
|
|
47
|
+
if (args.recordingStatus !== "processing") return false;
|
|
48
|
+
|
|
49
|
+
const value = await appStateGet(
|
|
50
|
+
args.ownerEmail,
|
|
51
|
+
mediaVerificationStateKey(args.recordingId),
|
|
52
|
+
).catch(() => null);
|
|
53
|
+
const marker = parseMediaVerificationMarker(value);
|
|
54
|
+
return marker?.recordingId === args.recordingId;
|
|
55
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type PlayerThumbnailRecording = {
|
|
2
|
+
id: string;
|
|
3
|
+
thumbnailUrl?: string | null;
|
|
4
|
+
animatedThumbnailUrl?: string | null;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function appendQueryParam(url: string, key: string, value: string): string {
|
|
8
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
9
|
+
return `${url}${separator}${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function localRecordingThumbnailRoute(recordingId: string): string {
|
|
13
|
+
return `/api/thumbnail/${encodeURIComponent(recordingId)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolvePlayerThumbnailUrl(
|
|
17
|
+
recording: PlayerThumbnailRecording,
|
|
18
|
+
options: {
|
|
19
|
+
accessToken?: string | null;
|
|
20
|
+
appPath?: (path: string) => string;
|
|
21
|
+
} = {},
|
|
22
|
+
): string | null {
|
|
23
|
+
if (!recording.thumbnailUrl && !recording.animatedThumbnailUrl) return null;
|
|
24
|
+
|
|
25
|
+
let resolved = localRecordingThumbnailRoute(recording.id);
|
|
26
|
+
if (options.accessToken) {
|
|
27
|
+
resolved = appendQueryParam(resolved, "t", options.accessToken);
|
|
28
|
+
}
|
|
29
|
+
if (options.appPath) resolved = options.appPath(resolved);
|
|
30
|
+
return resolved;
|
|
31
|
+
}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
signScopedAgentAccessToken,
|
|
8
8
|
} from "@agent-native/core/server";
|
|
9
9
|
|
|
10
|
-
export type PostFinalizeJobKind = "seekable" | "transcript";
|
|
10
|
+
export type PostFinalizeJobKind = "media-ready" | "seekable" | "transcript";
|
|
11
11
|
|
|
12
12
|
export const POST_FINALIZE_JOB_TOKEN_KIND = "clips-post-finalize-job";
|
|
13
13
|
|
|
@@ -47,10 +47,12 @@ export async function dispatchPostFinalizeJob(args: {
|
|
|
47
47
|
delayMs?: number;
|
|
48
48
|
retryAttempt?: number;
|
|
49
49
|
regenerate?: boolean;
|
|
50
|
+
requireAccepted?: boolean;
|
|
50
51
|
}): Promise<void> {
|
|
52
|
+
const { requireAccepted = false, ...job } = args;
|
|
51
53
|
const token = signScopedAgentAccessToken({
|
|
52
54
|
resourceKind: POST_FINALIZE_JOB_TOKEN_KIND,
|
|
53
|
-
resourceId: postFinalizeJobResourceId(
|
|
55
|
+
resourceId: postFinalizeJobResourceId(job.recordingId, job.kind),
|
|
54
56
|
ttlSeconds: 10 * 60,
|
|
55
57
|
});
|
|
56
58
|
const basePath = normalizeBasePath(
|
|
@@ -62,7 +64,7 @@ export async function dispatchPostFinalizeJob(args: {
|
|
|
62
64
|
const usesDurableBackground =
|
|
63
65
|
dispatchPathTargetsNetlifyBackgroundFunction(dispatchPath);
|
|
64
66
|
const body = JSON.stringify({
|
|
65
|
-
...
|
|
67
|
+
...job,
|
|
66
68
|
token,
|
|
67
69
|
...(usesDurableBackground
|
|
68
70
|
? {
|
|
@@ -100,6 +102,11 @@ export async function dispatchPostFinalizeJob(args: {
|
|
|
100
102
|
});
|
|
101
103
|
});
|
|
102
104
|
|
|
105
|
+
if (requireAccepted) {
|
|
106
|
+
await dispatch;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
103
110
|
await Promise.race([
|
|
104
111
|
dispatch,
|
|
105
112
|
new Promise<void>((resolve) => setTimeout(resolve, DISPATCH_SETTLE_MS)),
|
|
@@ -37,12 +37,12 @@ export default createAuthPlugin({
|
|
|
37
37
|
"/api/media",
|
|
38
38
|
"/api/clips-latest.json",
|
|
39
39
|
"/api/clips-updater.json",
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// upload POSTs stay behind auth under /api/uploads/*.
|
|
40
|
+
// Public media-serving routes enforce resolveAccess + password/expiry
|
|
41
|
+
// checks themselves. They need to bypass the app auth shell so anonymous
|
|
42
|
+
// viewers and crawlers can fetch public share media. Chunk upload POSTs
|
|
43
|
+
// stay behind auth under /api/uploads/*.
|
|
45
44
|
"/api/video",
|
|
45
|
+
"/api/thumbnail",
|
|
46
46
|
"/api/auth/google-calendar",
|
|
47
47
|
"/_agent-native/google/auth-url",
|
|
48
48
|
"/_agent-native/google/callback",
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import registerBuilderMediaCompressionJob from "../jobs/builder-media-compression.js";
|
|
9
|
+
import registerMediaVerificationJob from "../jobs/media-verification.js";
|
|
9
10
|
import registerMeetingRemindersJob from "../jobs/meeting-reminders.js";
|
|
10
11
|
import registerPollCalendarsJob from "../jobs/poll-calendars.js";
|
|
11
12
|
import registerStaleMeetingSweeperJob from "../jobs/stale-meeting-sweeper.js";
|
|
@@ -16,6 +17,7 @@ export default () => {
|
|
|
16
17
|
// background loop is off.
|
|
17
18
|
registerMeetingRemindersJob();
|
|
18
19
|
registerBuilderMediaCompressionJob();
|
|
20
|
+
registerMediaVerificationJob();
|
|
19
21
|
registerPollCalendarsJob();
|
|
20
22
|
registerStaleMeetingSweeperJob();
|
|
21
23
|
};
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "h3";
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
|
|
14
|
+
import finalizeRecording from "../../../../actions/finalize-recording.js";
|
|
14
15
|
import { ensureRecordingSeekable } from "../../../../actions/lib/ensure-seekable-video.js";
|
|
15
16
|
import requestTranscript from "../../../../actions/request-transcript.js";
|
|
16
17
|
import { getDb, schema } from "../../../db/index.js";
|
|
@@ -22,7 +23,7 @@ import {
|
|
|
22
23
|
|
|
23
24
|
const bodySchema = z.object({
|
|
24
25
|
recordingId: z.string().min(1).max(200),
|
|
25
|
-
kind: z.enum(["seekable", "transcript"]),
|
|
26
|
+
kind: z.enum(["media-ready", "seekable", "transcript"]),
|
|
26
27
|
token: z.string().min(1),
|
|
27
28
|
delayMs: z.number().int().min(0).max(30_000).optional(),
|
|
28
29
|
retryAttempt: z.number().int().min(1).max(10).optional(),
|
|
@@ -61,7 +62,8 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
61
62
|
setResponseStatus(event, 404);
|
|
62
63
|
return { ok: false, error: "Recording not found" };
|
|
63
64
|
}
|
|
64
|
-
|
|
65
|
+
const requiredStatus = kind === "media-ready" ? "processing" : "ready";
|
|
66
|
+
if (recording.status !== requiredStatus) {
|
|
65
67
|
return {
|
|
66
68
|
ok: true,
|
|
67
69
|
recordingId,
|
|
@@ -84,6 +86,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
84
86
|
kind,
|
|
85
87
|
retryAttempt,
|
|
86
88
|
regenerate,
|
|
89
|
+
requireAccepted: kind === "media-ready",
|
|
87
90
|
});
|
|
88
91
|
return {
|
|
89
92
|
ok: true,
|
|
@@ -100,6 +103,13 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
100
103
|
});
|
|
101
104
|
return { ok: true, kind, result };
|
|
102
105
|
}
|
|
106
|
+
if (kind === "media-ready") {
|
|
107
|
+
const result = await finalizeRecording.run({
|
|
108
|
+
id: recordingId,
|
|
109
|
+
mediaVerificationRetryAttempt: retryAttempt ?? 1,
|
|
110
|
+
});
|
|
111
|
+
return { ok: true, kind, result };
|
|
112
|
+
}
|
|
103
113
|
|
|
104
114
|
const result = await requestTranscript.run({
|
|
105
115
|
recordingId,
|
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
} from "../../../shared/transcript-segments.js";
|
|
43
43
|
import { resolveTranscriptPresentation } from "../../../shared/transcript-status.js";
|
|
44
44
|
import { getDb, schema } from "../../db/index.js";
|
|
45
|
+
import { isMediaVerificationPending } from "../../lib/media-verification-state.js";
|
|
46
|
+
import { resolvePlayerThumbnailUrl } from "../../lib/player-thumbnail-url.js";
|
|
45
47
|
import { resolvePlayerVideoUrl } from "../../lib/player-video-url.js";
|
|
46
48
|
import {
|
|
47
49
|
getOrganizationRoleForEmail,
|
|
@@ -367,6 +369,10 @@ export default defineEventHandler(async (event) => {
|
|
|
367
369
|
resolvedVideoUrl,
|
|
368
370
|
protectedMediaToken,
|
|
369
371
|
);
|
|
372
|
+
const playbackThumbnailUrl = resolvePlayerThumbnailUrl(rec, {
|
|
373
|
+
accessToken: protectedMediaToken,
|
|
374
|
+
appPath,
|
|
375
|
+
});
|
|
370
376
|
|
|
371
377
|
const canExposeAgentContext =
|
|
372
378
|
(rec.visibility === "public" || tokenAllowsAgentAccess || viewerIsOwner) &&
|
|
@@ -393,13 +399,18 @@ export default defineEventHandler(async (event) => {
|
|
|
393
399
|
// Referer of any outbound link the share page renders.
|
|
394
400
|
setResponseHeader(event, "Referrer-Policy", "no-referrer");
|
|
395
401
|
const transcriptPresentation = resolveTranscriptPresentation(transcript);
|
|
402
|
+
const verificationPending = await isMediaVerificationPending({
|
|
403
|
+
ownerEmail: rec.ownerEmail,
|
|
404
|
+
recordingId,
|
|
405
|
+
recordingStatus: rec.status,
|
|
406
|
+
});
|
|
396
407
|
|
|
397
408
|
return {
|
|
398
409
|
recording: {
|
|
399
410
|
id: rec.id,
|
|
400
411
|
title: rec.title,
|
|
401
412
|
description: rec.description,
|
|
402
|
-
thumbnailUrl:
|
|
413
|
+
thumbnailUrl: playbackThumbnailUrl,
|
|
403
414
|
animatedThumbnailUrl: rec.animatedThumbnailUrl,
|
|
404
415
|
sourceAppName: rec.sourceAppName,
|
|
405
416
|
durationMs: rec.durationMs,
|
|
@@ -411,6 +422,7 @@ export default defineEventHandler(async (event) => {
|
|
|
411
422
|
hasAudio: Boolean(rec.hasAudio),
|
|
412
423
|
hasCamera: Boolean(rec.hasCamera),
|
|
413
424
|
status: rec.status,
|
|
425
|
+
verificationPending,
|
|
414
426
|
uploadProgress: rec.uploadProgress,
|
|
415
427
|
failureReason: rec.failureReason,
|
|
416
428
|
// Don't leak the password to clients; just indicate whether one was set.
|