@okrlinkhub/agent-factory 2.0.0 → 2.0.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.
@@ -1,9 +1,28 @@
1
1
  import { v } from "convex/values";
2
- import { internal } from "./_generated/api.js";
3
- import { internalMutation, internalQuery, mutation, query, } from "./_generated/server.js";
2
+ import { api, internal } from "./_generated/api.js";
3
+ import { action, internalMutation, internalQuery, mutation, query, } from "./_generated/server.js";
4
4
  import { computeRetryDelayMs, DEFAULT_CONFIG, providerConfigValidator } from "./config.js";
5
5
  import { canTransitionWorkerStatus, isWorkerClaimable, isWorkerRunning, isWorkerTerminal, workerStatusValidator, } from "./workerLifecycle.js";
6
6
  const queueStatusValidator = v.union(v.literal("queued"), v.literal("processing"), v.literal("done"), v.literal("failed"), v.literal("dead_letter"));
7
+ const telegramAttachmentKindValidator = v.union(v.literal("photo"), v.literal("video"), v.literal("audio"), v.literal("voice"), v.literal("document"));
8
+ const telegramAttachmentStatusValidator = v.union(v.literal("ready"), v.literal("expired"));
9
+ const telegramAttachmentValidator = v.object({
10
+ kind: telegramAttachmentKindValidator,
11
+ status: telegramAttachmentStatusValidator,
12
+ storageId: v.id("_storage"),
13
+ telegramFileId: v.string(),
14
+ fileName: v.optional(v.string()),
15
+ mimeType: v.optional(v.string()),
16
+ sizeBytes: v.optional(v.number()),
17
+ expiresAt: v.number(),
18
+ });
19
+ const telegramAttachmentCandidateValidator = v.object({
20
+ kind: telegramAttachmentKindValidator,
21
+ telegramFileId: v.string(),
22
+ fileName: v.optional(v.string()),
23
+ mimeType: v.optional(v.string()),
24
+ sizeBytes: v.optional(v.number()),
25
+ });
7
26
  const queuePayloadValidator = v.object({
8
27
  provider: v.string(),
9
28
  providerUserId: v.string(),
@@ -11,6 +30,7 @@ const queuePayloadValidator = v.object({
11
30
  externalMessageId: v.optional(v.string()),
12
31
  rawUpdateJson: v.optional(v.string()),
13
32
  metadata: v.optional(v.record(v.string(), v.string())),
33
+ attachments: v.optional(v.array(telegramAttachmentValidator)),
14
34
  });
15
35
  const snapshotReasonValidator = v.union(v.literal("drain"), v.literal("signal"), v.literal("manual"));
16
36
  const DATA_SNAPSHOT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
@@ -102,6 +122,7 @@ const workerSpawnOpenClawEnvValidator = v.object({
102
122
  });
103
123
  const messageRuntimeConfigValidator = v.object({
104
124
  systemPrompt: v.optional(v.string()),
125
+ telegramAttachmentRetentionMs: v.optional(v.number()),
105
126
  });
106
127
  const globalSkillStatusValidator = v.union(v.literal("active"), v.literal("disabled"));
107
128
  const globalSkillReleaseChannelValidator = v.union(v.literal("stable"), v.literal("canary"));
@@ -133,6 +154,7 @@ const RUNTIME_CONFIG_KEYS = {
133
154
  provider: "provider",
134
155
  message: "message",
135
156
  };
157
+ const DEFAULT_TELEGRAM_ATTACHMENT_RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
136
158
  export const enqueueMessage = mutation({
137
159
  args: {
138
160
  conversationId: v.string(),
@@ -435,6 +457,47 @@ export const getMessageRuntimeConfig = internalQuery({
435
457
  return row.messageConfig;
436
458
  },
437
459
  });
460
+ export const getTelegramIngressRuntimeConfig = internalQuery({
461
+ args: {
462
+ agentKey: v.string(),
463
+ },
464
+ returns: v.object({
465
+ botToken: v.union(v.null(), v.string()),
466
+ attachmentRetentionMs: v.number(),
467
+ }),
468
+ handler: async (ctx, args) => {
469
+ const profile = await ctx.db
470
+ .query("agentProfiles")
471
+ .withIndex("by_agentKey", (q) => q.eq("agentKey", args.agentKey))
472
+ .unique();
473
+ const botToken = profile ? await resolveActiveTelegramBotToken(ctx, profile.secretsRef) : null;
474
+ const row = await ctx.db
475
+ .query("runtimeConfig")
476
+ .withIndex("by_key", (q) => q.eq("key", RUNTIME_CONFIG_KEYS.message))
477
+ .unique();
478
+ return {
479
+ botToken,
480
+ attachmentRetentionMs: resolveTelegramAttachmentRetentionMs(row?.messageConfig?.telegramAttachmentRetentionMs),
481
+ };
482
+ },
483
+ });
484
+ export const prepareTelegramAttachmentsForEnqueue = action({
485
+ args: {
486
+ agentKey: v.string(),
487
+ attachments: v.array(telegramAttachmentCandidateValidator),
488
+ },
489
+ returns: v.array(telegramAttachmentValidator),
490
+ handler: async (ctx, args) => {
491
+ const ingressConfig = await ctx.runQuery(internal.queue.getTelegramIngressRuntimeConfig, {
492
+ agentKey: args.agentKey,
493
+ });
494
+ if (!ingressConfig.botToken) {
495
+ throw new Error(`missing active telegram bot token for agent '${args.agentKey}'`);
496
+ }
497
+ const expiresAt = Date.now() + ingressConfig.attachmentRetentionMs;
498
+ return await Promise.all(args.attachments.map((attachment) => persistTelegramAttachmentFromCandidate(ctx, ingressConfig.botToken, attachment, expiresAt)));
499
+ },
500
+ });
438
501
  export const upsertMessageRuntimeConfig = internalMutation({
439
502
  args: {
440
503
  messageConfig: messageRuntimeConfigValidator,
@@ -1407,18 +1470,7 @@ export const getHydrationBundleForClaimedJob = query({
1407
1470
  .query("conversationHydrationCache")
1408
1471
  .withIndex("by_conversationId", (q) => q.eq("conversationId", message.conversationId))
1409
1472
  .first();
1410
- let telegramBotToken = null;
1411
- const telegramSecretRefs = profile.secretsRef.filter((ref) => ref === "telegram.botToken" || ref.startsWith("telegram.botToken."));
1412
- for (const telegramSecretRef of telegramSecretRefs) {
1413
- const activeSecret = await ctx.db
1414
- .query("secrets")
1415
- .withIndex("by_secretRef_and_active", (q) => q.eq("secretRef", telegramSecretRef).eq("active", true))
1416
- .unique();
1417
- if (activeSecret) {
1418
- telegramBotToken = decryptSecretValue(activeSecret.encryptedValue, activeSecret.algorithm);
1419
- break;
1420
- }
1421
- }
1473
+ const telegramBotToken = await resolveActiveTelegramBotToken(ctx, profile.secretsRef);
1422
1474
  const contextHistory = conversationCache && conversationCache.snapshotKey === snapshotKey
1423
1475
  ? conversationCache.deltaContext
1424
1476
  : conversation.contextHistory;
@@ -2050,6 +2102,42 @@ export const expireOldDataSnapshots = internalMutation({
2050
2102
  return rows.length;
2051
2103
  },
2052
2104
  });
2105
+ export const expireOldTelegramAttachments = internalMutation({
2106
+ args: {
2107
+ nowMs: v.optional(v.number()),
2108
+ limit: v.optional(v.number()),
2109
+ },
2110
+ returns: v.number(),
2111
+ handler: async (ctx, args) => {
2112
+ const nowMs = args.nowMs ?? Date.now();
2113
+ const limit = args.limit ?? 100;
2114
+ const rows = await ctx.db
2115
+ .query("messageAttachments")
2116
+ .withIndex("by_status_and_expiresAt", (q) => q.eq("status", "ready").lte("expiresAt", nowMs))
2117
+ .take(limit);
2118
+ for (const row of rows) {
2119
+ await ctx.db.patch(row._id, {
2120
+ status: "expired",
2121
+ });
2122
+ const message = await ctx.db.get(row.messageId);
2123
+ if (message?.payload.attachments?.length) {
2124
+ await ctx.db.patch(message._id, {
2125
+ payload: {
2126
+ ...message.payload,
2127
+ attachments: message.payload.attachments.map((attachment) => attachment.storageId === row.storageId
2128
+ ? {
2129
+ ...attachment,
2130
+ status: "expired",
2131
+ }
2132
+ : attachment),
2133
+ },
2134
+ });
2135
+ }
2136
+ await ctx.storage.delete?.(row.storageId);
2137
+ }
2138
+ return rows.length;
2139
+ },
2140
+ });
2053
2141
  export const getWorkerStats = query({
2054
2142
  args: {},
2055
2143
  returns: v.object({
@@ -2218,6 +2306,23 @@ async function enqueueMessageRecord(ctx, args) {
2218
2306
  attempts: 0,
2219
2307
  maxAttempts: args.maxAttempts ?? DEFAULT_CONFIG.retry.maxAttempts,
2220
2308
  });
2309
+ for (const attachment of payload.attachments ?? []) {
2310
+ await ctx.db.insert("messageAttachments", {
2311
+ messageId,
2312
+ conversationId: args.conversationId,
2313
+ agentKey: args.agentKey,
2314
+ provider: payload.provider,
2315
+ kind: attachment.kind,
2316
+ status: attachment.status,
2317
+ storageId: attachment.storageId,
2318
+ telegramFileId: attachment.telegramFileId,
2319
+ fileName: attachment.fileName,
2320
+ mimeType: attachment.mimeType,
2321
+ sizeBytes: attachment.sizeBytes,
2322
+ createdAt: nowMs,
2323
+ expiresAt: attachment.expiresAt,
2324
+ });
2325
+ }
2221
2326
  try {
2222
2327
  await ctx.scheduler.runAfter(0, internal.scheduler.reconcileWorkerPoolFromEnqueue, {
2223
2328
  workspaceId: "default",
@@ -2354,10 +2459,16 @@ function dedupeMessagesById(messages) {
2354
2459
  }
2355
2460
  function normalizeMessageRuntimeConfig(messageConfig) {
2356
2461
  const systemPrompt = normalizeSystemPrompt(messageConfig?.systemPrompt);
2357
- if (systemPrompt === null) {
2462
+ const telegramAttachmentRetentionMs = normalizeTelegramAttachmentRetentionMs(messageConfig?.telegramAttachmentRetentionMs);
2463
+ if (systemPrompt === null && telegramAttachmentRetentionMs === undefined) {
2358
2464
  return null;
2359
2465
  }
2360
- return { systemPrompt };
2466
+ return {
2467
+ ...(systemPrompt === null ? {} : { systemPrompt }),
2468
+ ...(telegramAttachmentRetentionMs === undefined
2469
+ ? {}
2470
+ : { telegramAttachmentRetentionMs }),
2471
+ };
2361
2472
  }
2362
2473
  function normalizeSystemPrompt(systemPrompt) {
2363
2474
  if (typeof systemPrompt !== "string") {
@@ -2366,6 +2477,110 @@ function normalizeSystemPrompt(systemPrompt) {
2366
2477
  const normalizedSystemPrompt = systemPrompt.trim();
2367
2478
  return normalizedSystemPrompt.length > 0 ? normalizedSystemPrompt : null;
2368
2479
  }
2480
+ function normalizeTelegramAttachmentRetentionMs(retentionMs) {
2481
+ if (typeof retentionMs !== "number" || !Number.isFinite(retentionMs)) {
2482
+ return undefined;
2483
+ }
2484
+ const normalizedRetentionMs = Math.floor(retentionMs);
2485
+ if (normalizedRetentionMs <= 0) {
2486
+ return undefined;
2487
+ }
2488
+ return normalizedRetentionMs;
2489
+ }
2490
+ function resolveTelegramAttachmentRetentionMs(retentionMs) {
2491
+ return (normalizeTelegramAttachmentRetentionMs(retentionMs) ??
2492
+ DEFAULT_TELEGRAM_ATTACHMENT_RETENTION_MS);
2493
+ }
2494
+ async function resolveActiveTelegramBotToken(ctx, secretRefs) {
2495
+ const telegramSecretRefs = secretRefs.filter((ref) => ref === "telegram.botToken" || ref.startsWith("telegram.botToken."));
2496
+ for (const telegramSecretRef of telegramSecretRefs) {
2497
+ const activeSecret = await ctx.db
2498
+ .query("secrets")
2499
+ .withIndex("by_secretRef_and_active", (q) => q.eq("secretRef", telegramSecretRef).eq("active", true))
2500
+ .unique();
2501
+ if (activeSecret) {
2502
+ return decryptSecretValue(activeSecret.encryptedValue, activeSecret.algorithm);
2503
+ }
2504
+ }
2505
+ return null;
2506
+ }
2507
+ async function persistTelegramAttachmentFromCandidate(ctx, telegramBotToken, attachment, expiresAt) {
2508
+ const filePath = await fetchTelegramFilePath(telegramBotToken, attachment.telegramFileId);
2509
+ const downloaded = await downloadTelegramFile(telegramBotToken, filePath);
2510
+ const uploadTarget = await ctx.runMutation(api.queue.generateMediaUploadUrl, {});
2511
+ const uploadResponse = await fetch(uploadTarget.uploadUrl, {
2512
+ method: "POST",
2513
+ headers: downloaded.mimeType.length > 0
2514
+ ? {
2515
+ "Content-Type": downloaded.mimeType,
2516
+ }
2517
+ : undefined,
2518
+ body: downloaded.blob,
2519
+ });
2520
+ const uploadPayload = (await uploadResponse.json().catch(() => ({})));
2521
+ if (!uploadResponse.ok || !uploadPayload.storageId) {
2522
+ throw new Error(`Convex storage upload failed for Telegram ${attachment.kind} attachment`);
2523
+ }
2524
+ return {
2525
+ kind: attachment.kind,
2526
+ status: "ready",
2527
+ storageId: uploadPayload.storageId,
2528
+ telegramFileId: attachment.telegramFileId,
2529
+ fileName: attachment.fileName,
2530
+ mimeType: downloaded.mimeType || attachment.mimeType,
2531
+ sizeBytes: attachment.sizeBytes ?? downloaded.blob.size,
2532
+ expiresAt,
2533
+ };
2534
+ }
2535
+ async function fetchTelegramFilePath(telegramBotToken, telegramFileId) {
2536
+ const telegramApiBaseUrl = `https://api.telegram.org/bot${encodeURIComponent(telegramBotToken)}`;
2537
+ const response = await fetch(`${telegramApiBaseUrl}/getFile?file_id=${encodeURIComponent(telegramFileId)}`);
2538
+ const payload = (await response.json().catch(() => ({})));
2539
+ if (!response.ok || payload.ok !== true || typeof payload.result?.file_path !== "string") {
2540
+ throw new Error(`Telegram getFile failed: ${typeof payload.description === "string" ? payload.description : "missing file_path"}`);
2541
+ }
2542
+ return payload.result.file_path;
2543
+ }
2544
+ async function downloadTelegramFile(telegramBotToken, filePath) {
2545
+ const response = await fetch(`https://api.telegram.org/file/bot${encodeURIComponent(telegramBotToken)}/${filePath}`);
2546
+ if (!response.ok) {
2547
+ throw new Error(`Telegram file download failed with status ${response.status}`);
2548
+ }
2549
+ const blob = await response.blob();
2550
+ const mimeType = response.headers.get("Content-Type") ??
2551
+ blob.type ??
2552
+ inferMimeTypeFromFilePath(filePath) ??
2553
+ "application/octet-stream";
2554
+ return {
2555
+ blob: mimeType === blob.type ? blob : new Blob([await blob.arrayBuffer()], { type: mimeType }),
2556
+ mimeType,
2557
+ };
2558
+ }
2559
+ function inferMimeTypeFromFilePath(filePath) {
2560
+ const normalizedPath = filePath.toLowerCase();
2561
+ if (normalizedPath.endsWith(".jpg") || normalizedPath.endsWith(".jpeg")) {
2562
+ return "image/jpeg";
2563
+ }
2564
+ if (normalizedPath.endsWith(".png")) {
2565
+ return "image/png";
2566
+ }
2567
+ if (normalizedPath.endsWith(".webp")) {
2568
+ return "image/webp";
2569
+ }
2570
+ if (normalizedPath.endsWith(".mp4")) {
2571
+ return "video/mp4";
2572
+ }
2573
+ if (normalizedPath.endsWith(".mp3")) {
2574
+ return "audio/mpeg";
2575
+ }
2576
+ if (normalizedPath.endsWith(".ogg")) {
2577
+ return "audio/ogg";
2578
+ }
2579
+ if (normalizedPath.endsWith(".pdf")) {
2580
+ return "application/pdf";
2581
+ }
2582
+ return null;
2583
+ }
2369
2584
  async function buildGlobalSkillMaterialization(skill) {
2370
2585
  const skillDirName = normalizeGlobalSkillDirName(skill.slug);
2371
2586
  const scriptExt = skill.moduleFormat === "cjs" ? "cjs" : "mjs";