@sentry/junior 0.266.0 → 0.268.0

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 (87) hide show
  1. package/dist/{actor-tlxzqaRe.js → actor-CP9K2Idu.js} +9 -1
  2. package/dist/{adapter-eRFA_naf.js → adapter-04AcD2SO.js} +5 -7
  3. package/dist/{admin-tILpP51p.js → admin-DIU1QyG3.js} +1 -1
  4. package/dist/{agent-invocations-CxKldAeB.js → agent-invocations-C1RNOCs8.js} +2 -0
  5. package/dist/{agent-runner-BtYK7bht.js → agent-runner-CAYfFmU7.js} +2636 -601
  6. package/dist/api/schema.d.ts +2 -5454
  7. package/dist/api/schema.js +6 -5
  8. package/dist/api.d.ts +6 -1
  9. package/dist/api.js +255 -1208
  10. package/dist/{app-Csbi9dW7.d.ts → app-z6Uuyq3j.d.ts} +23 -4
  11. package/dist/app.d.ts +2 -2
  12. package/dist/app.js +1869 -378
  13. package/dist/{automation-BKpY9LPa.js → automation-CmeFBSEh.js} +42 -14
  14. package/dist/{briefs-DgfWMU7e.js → briefs-DQbV2jNr.js} +4 -4
  15. package/dist/{catalog-runtime-DAJjAbSx.js → catalog-runtime-BD9GSpc6.js} +1 -1
  16. package/dist/cli/chat.js +15 -15
  17. package/dist/cli/check.js +3 -3
  18. package/dist/cli/init.js +0 -1
  19. package/dist/cli/main.js +3 -3
  20. package/dist/cli/plugins.js +4 -4
  21. package/dist/cli/snapshot-create.js +4 -4
  22. package/dist/cli/upgrade.d.ts +1 -1
  23. package/dist/cli/upgrade.js +3 -3
  24. package/dist/{client-B0YZfxNy.js → client-C1yDYjoS.js} +2 -2
  25. package/dist/{config-C0UwYqDK.js → config-DrTeF80T.js} +32 -10
  26. package/dist/{connections-Bwu3VORi.js → connections-D46f8vvi.js} +12 -12
  27. package/dist/{conversation-DF4rUcjb.js → conversation-DSo310Ps.js} +8 -1
  28. package/dist/{conversation-privacy-CY7JVjxn.js → conversation-privacy-CxlOCiUo.js} +1 -1
  29. package/dist/{credential-sync-VGAMUTGp.js → credential-sync-sm9fegLY.js} +29 -34
  30. package/dist/{credentialed-enF5Bf3y.js → credentialed-lukuD-cc.js} +2933 -1026
  31. package/dist/db-BU4LQD8s.d.ts +6 -0
  32. package/dist/{executor-TnWbvL09.js → executor-4vhvsMZJ.js} +83 -6
  33. package/dist/instrumentation.js +6 -2
  34. package/dist/{assistant-reply-UyzAfZQo.js → interruption-marker-CkncDT8d.js} +8 -2
  35. package/dist/{job-queue-DBpHmKnd.js → job-queue-In9QKz-I.js} +128 -3
  36. package/dist/{web-input-B-bMjD1V.js → list-BkgnXEEB.js} +1611 -817
  37. package/dist/{logging-Ce0mXZ8J.js → logging-DLSpkSSr.js} +11 -3
  38. package/dist/{message-VZ3DFpGb.js → message-BObyg59s.js} +4 -4
  39. package/dist/nitro.d.ts +1 -1
  40. package/dist/nitro.js +3 -3
  41. package/dist/{oauth-callback-server-CpqWLjVY.js → oauth-callback-server-B-WbSdoH.js} +2 -2
  42. package/dist/{prompt-BcCGlmg2.js → prompt-BW9jFsqZ.js} +1 -1
  43. package/dist/{registry-1BEIPdbT.js → registry-BgecKva0.js} +2 -2
  44. package/dist/{web-message-id-CJSUiksE.js → reporting-window-DVCD_10C.js} +1 -10
  45. package/dist/{resolve-BSHHkEGE.js → resolve-Cpa4Bu1O.js} +3 -3
  46. package/dist/{result-CwOpzqdh.js → result-D3TSmZZ8.js} +9 -9
  47. package/dist/{runner-CEaX04kz.js → runner-DS5OKhEp.js} +11 -11
  48. package/dist/{sandbox-egress-signals-BwtGHwqa.js → sandbox-egress-signals-CnDjY5rx.js} +1 -1
  49. package/dist/{schema-RsaBHxRH.js → schema-0050HFnB.js} +82 -5
  50. package/dist/schema-ZngqcOWM.d.ts +5813 -0
  51. package/dist/{service-account-6wC76eBZ.js → service-account-CKV2WTY7.js} +3 -3
  52. package/dist/{service-account-Dg4lIbHm.js → service-account-DGjnDw9y.js} +3 -3
  53. package/dist/{session-stop-tZOw9AJD.js → session-stop-Dam1EI7B.js} +527 -134
  54. package/dist/{skills-DzyGN7wd.js → skills-CfAnfImD.js} +2 -2
  55. package/dist/{spawn-6rokq7l_.js → spawn-cmq9p4Ly.js} +2 -2
  56. package/dist/{store-DcKkzrdt.js → store-C5PN02QO.js} +4 -4
  57. package/dist/{store-CG0VJyye.d.ts → store-xMuuXRE0.d.ts} +23 -9
  58. package/dist/store-zPEoJPBB.js +295 -0
  59. package/dist/{task-queue-Bg4RS2Gy.js → task-queue-_mf28dSu.js} +10 -5
  60. package/dist/{task-runner-8N5LIkSg.js → task-runner-DMI2prDO.js} +25 -14
  61. package/dist/{tasks-DDT-J3Rf.js → tasks-SB9z-VTU.js} +1551 -54
  62. package/dist/{thread-state-cprEaxgl.js → thread-state-BbbPh0Sn.js} +8 -18
  63. package/dist/{turn-failure-response-C8sMdsdX.js → turn-failure-response-BKKmnMB5.js} +11 -16
  64. package/dist/{turn-lifecycle-DaBVoz7h.js → turn-lifecycle-oP0kVeGz.js} +1 -1
  65. package/dist/{validation-C2lllinq.js → validation--JsAGLwc.js} +1 -1
  66. package/dist/{version-oaLfxxnf.js → version-jHwJp4hC.js} +1 -1
  67. package/dist/version.js +1 -1
  68. package/dist/{viewer-BpE6Z3qf.js → viewer-CpXTGlep.js} +329 -230
  69. package/dist/web-message-id-DlHNANtd.js +132 -0
  70. package/dist/{work-VX1SHWha.js → work-3N-GHY9m.js} +10 -10
  71. package/dist/{worker-hook-y_Y.js → worker-CCdbcItd.js} +30 -19
  72. package/migrations/0064_any_resource_event_triggers.sql +2 -0
  73. package/migrations/0065_user_customization.sql +1 -0
  74. package/migrations/0066_review_items.sql +53 -0
  75. package/migrations/0067_review_item_dedupe_tenant.sql +2 -0
  76. package/migrations/0068_review_item_slack_cards.sql +10 -0
  77. package/migrations/meta/0064_snapshot.json +3505 -0
  78. package/migrations/meta/0065_snapshot.json +3511 -0
  79. package/migrations/meta/0066_snapshot.json +4099 -0
  80. package/migrations/meta/0067_snapshot.json +3921 -0
  81. package/migrations/meta/0068_snapshot.json +4176 -0
  82. package/migrations/meta/_journal.json +36 -1
  83. package/package.json +19 -4
  84. package/dist/db-BnRqQ7G5.d.ts +0 -5
  85. package/dist/images-BKQqSi5W.js +0 -64
  86. package/dist/store-FALy-JQb.js +0 -1424
  87. package/dist/turn-session-routing-a9CHX-w6.js +0 -76
@@ -0,0 +1,6 @@
1
+ import "./store-xMuuXRE0.js";
2
+ import "./schema-ZngqcOWM.js";
3
+ import { z } from "zod";
4
+ import "drizzle-orm/pg-core";
5
+ import "drizzle-orm/migrator";
6
+ import "drizzle-orm/pg-core/session";
@@ -1,6 +1,6 @@
1
- import "./config-C0UwYqDK.js";
2
- import { f as logException } from "./logging-Ce0mXZ8J.js";
3
- import { a as juniorAgentInvocations, c as juniorIdentities, d as juniorDestinations, f as timestamptz, i as juniorAgentBindings, l as juniorUsers, s as juniorConversations } from "./agent-invocations-CxKldAeB.js";
1
+ import "./config-DrTeF80T.js";
2
+ import { p as logException } from "./logging-DLSpkSSr.js";
3
+ import { a as juniorAgentInvocations, c as juniorIdentities, d as juniorDestinations, f as timestamptz, i as juniorAgentBindings, l as juniorUsers, s as juniorConversations } from "./agent-invocations-C1RNOCs8.js";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
5
5
  import { sql } from "drizzle-orm";
6
6
  import { bigint, boolean, check, date, doublePrecision, foreignKey, index, integer, jsonb, pgTable, primaryKey, text, uniqueIndex, uuid } from "drizzle-orm/pg-core";
@@ -292,7 +292,10 @@ const juniorStats = pgTable("junior_stats", {
292
292
  * One row per Automation of either kind. `record` holds the fields that have
293
293
  * no column. Each write also copies the nested fields that queries filter or
294
294
  * search on into columns: `team_id`, `channel_id`, `creator_slack_user_id`,
295
- * `instruction`, and the event trigger `namespace` and `identifier`.
295
+ * `instruction`, and the event trigger `namespace` and `identifier`. An event
296
+ * trigger with no identifier runs for every resource of its namespace, so its
297
+ * `identifier` is null. The match index finds those rows with
298
+ * `identifier is null`.
296
299
  */
297
300
  const juniorAutomations = pgTable("junior_automations", {
298
301
  id: text("id").primaryKey(),
@@ -317,7 +320,7 @@ const juniorAutomations = pgTable("junior_automations", {
317
320
  check("junior_automations_kind_check", sql`${table.kind} in ('scheduled', 'event')`),
318
321
  check("junior_automations_status_check", sql`${table.status} in ('active', 'blocked', 'paused', 'completed', 'deleted')`),
319
322
  check("junior_automations_record_check", sql`jsonb_typeof(${table.record}) = 'object'`),
320
- check("junior_automations_trigger_check", sql`(${table.kind} = 'scheduled' and ${table.namespace} is null and ${table.identifier} is null) or (${table.kind} = 'event' and ${table.namespace} is not null and ${table.identifier} is not null and ${table.nextRunAtMs} is null and ${table.runNowAtMs} is null and ${table.status} <> 'completed')`),
323
+ check("junior_automations_trigger_check", sql`(${table.kind} = 'scheduled' and ${table.namespace} is null and ${table.identifier} is null) or (${table.kind} = 'event' and ${table.namespace} is not null and ${table.nextRunAtMs} is null and ${table.runNowAtMs} is null and ${table.status} <> 'completed')`),
321
324
  index("junior_automations_team_idx").on(table.teamId, table.createdAtMs, table.id).where(sql`${table.status} <> 'deleted'`),
322
325
  index("junior_automations_creator_identity_idx").on(table.creatorIdentityId, table.createdAtMs, table.id),
323
326
  index("junior_automations_run_now_due_idx").on(table.runNowAtMs, table.createdAtMs, table.id).where(sql`${table.status} = 'active' AND ${table.runNowAtMs} IS NOT NULL`),
@@ -379,6 +382,76 @@ const juniorAutomationRuns = pgTable("junior_automation_runs", {
379
382
  index("junior_automation_runs_completed_idx").on(table.completedAtMs),
380
383
  index("junior_automation_runs_incomplete_idx").on(table.automationId, table.scheduledForMs).where(sql`${table.status} in ('pending', 'running')`)
381
384
  ]);
385
+ // src/db/schema/review-items.ts
386
+ /**
387
+ * One Review item: something that needs a person. See
388
+ * `chat/review-items/README.md`. An action item stores its `request`, and
389
+ * its `answer` after an assignee submits it. `closed_at` is set for `done`
390
+ * and `dismissed`. `closed_by_user_id` is null when an agent closed it.
391
+ */
392
+ const juniorReviewItems = pgTable("junior_review_items", {
393
+ id: text("id").primaryKey(),
394
+ kind: text("kind").$type().notNull(),
395
+ status: text("status").$type().notNull(),
396
+ /**
397
+ * `private` when the Conversation that added it is not public. A private
398
+ * item stays private and links only to that Conversation.
399
+ */
400
+ visibility: text("visibility").$type().notNull(),
401
+ title: text("title").notNull(),
402
+ body: text("body").notNull(),
403
+ request: jsonb("request").$type(),
404
+ answer: jsonb("answer").$type(),
405
+ assignedToEveryone: boolean("assigned_to_everyone").notNull().default(false),
406
+ dedupeKey: text("dedupe_key").notNull(),
407
+ subjectId: text("subject_id"),
408
+ subjectTitle: text("subject_title"),
409
+ subjectUrl: text("subject_url"),
410
+ /** Conversation of the agent that added the item. */
411
+ addedByConversationId: text("added_by_conversation_id").notNull(),
412
+ /**
413
+ * Provider tenant, such as a Slack workspace, of the Conversation that
414
+ * added the item. A public item is visible only inside it.
415
+ */
416
+ provider: text("provider"),
417
+ providerTenantId: text("provider_tenant_id"),
418
+ automationId: text("automation_id").references(() => juniorAutomations.id, { onDelete: "set null" }),
419
+ closedAt: timestamptz("closed_at"),
420
+ closedByUserId: text("closed_by_user_id").references(() => juniorUsers.id),
421
+ closeReason: text("close_reason"),
422
+ createdAt: timestamptz("created_at").notNull(),
423
+ updatedAt: timestamptz("updated_at").notNull()
424
+ }, (table) => [
425
+ check("junior_review_items_kind_check", sql`(${table.kind} = 'notice' and ${table.request} is null and ${table.answer} is null) or (${table.kind} = 'action' and ${table.request} is not null)`),
426
+ check("junior_review_items_status_check", sql`(${table.status} = 'open' and ${table.closedAt} is null) or (${table.status} in ('done', 'dismissed') and ${table.closedAt} is not null)`),
427
+ check("junior_review_items_tenant_check", sql`(${table.provider} is null) = (${table.providerTenantId} is null)`),
428
+ check("junior_review_items_visibility_check", sql`${table.visibility} = 'public' or not ${table.assignedToEveryone}`),
429
+ uniqueIndex("junior_review_items_open_dedupe_uidx").on(sql`coalesce(${table.provider}, '')`, sql`coalesce(${table.providerTenantId}, '')`, table.dedupeKey).where(sql`${table.status} = 'open'`),
430
+ index("junior_review_items_status_idx").on(table.status, table.updatedAt, table.id),
431
+ index("junior_review_items_subject_idx").on(table.subjectId, table.status),
432
+ index("junior_review_items_automation_idx").on(table.automationId, table.status)
433
+ ]);
434
+ /** The assigned Users of a Review item. Everyone is a flag on the item. */
435
+ const juniorReviewItemAssignees = pgTable("junior_review_item_assignees", {
436
+ itemId: text("item_id").notNull().references(() => juniorReviewItems.id, { onDelete: "cascade" }),
437
+ userId: text("user_id").notNull().references(() => juniorUsers.id, { onDelete: "cascade" })
438
+ }, (table) => [primaryKey({ columns: [table.itemId, table.userId] }), index("junior_review_item_assignees_user_idx").on(table.userId)]);
439
+ /** The Conversations linked to a Review item. */
440
+ const juniorReviewItemConversations = pgTable("junior_review_item_conversations", {
441
+ itemId: text("item_id").notNull().references(() => juniorReviewItems.id, { onDelete: "cascade" }),
442
+ conversationId: text("conversation_id").notNull().references(() => juniorConversations.conversationId, { onDelete: "cascade" })
443
+ }, (table) => [primaryKey({ columns: [table.itemId, table.conversationId] }), index("junior_review_item_conversations_conversation_idx").on(table.conversationId)]);
444
+ /**
445
+ * The Slack message that shows a Review item in one linked Slack thread.
446
+ * Junior updates it in place after each change.
447
+ */
448
+ const juniorReviewItemSlackCards = pgTable("junior_review_item_slack_cards", {
449
+ itemId: text("item_id").notNull().references(() => juniorReviewItems.id, { onDelete: "cascade" }),
450
+ conversationId: text("conversation_id").notNull(),
451
+ teamId: text("team_id").notNull(),
452
+ channelId: text("channel_id").notNull(),
453
+ messageTs: text("message_ts").notNull()
454
+ }, (table) => [primaryKey({ columns: [table.itemId, table.conversationId] })]);
382
455
  // src/db/schema/workspaces.ts
383
456
  /** Named recipe used to prepare a reusable Sandbox. */
384
457
  const juniorWorkspaces = pgTable("junior_workspaces", {
@@ -452,6 +525,10 @@ const juniorSqlSchema = {
452
525
  juniorLocationConfigurations,
453
526
  juniorDestinations,
454
527
  juniorIdentities,
528
+ juniorReviewItemAssignees,
529
+ juniorReviewItemConversations,
530
+ juniorReviewItemSlackCards,
531
+ juniorReviewItems,
455
532
  juniorSnapshots,
456
533
  juniorStats,
457
534
  juniorAutomationRuns,
@@ -706,4 +783,4 @@ function createJuniorSqlExecutor(args) {
706
783
  statementTimeoutMs
707
784
  });
708
785
  }
709
- export { juniorConversationAnnotations as _, juniorAutomationRuns as a, juniorAttachments as b, juniorStats as c, juniorConversationBindings as d, juniorConversationBriefs as f, juniorApiTokens as g, juniorConversationEvents as h, juniorWorkspaces as i, juniorLocationConfigurations as l, juniorConversationMetrics as m, juniorSnapshots as n, juniorAutomationVersions as o, CONVERSATION_METRICS as p, juniorWorkspaceRepos as r, juniorAutomations as s, createJuniorSqlExecutor as t, juniorConversationParticipants as u, juniorCodeChanges as v, juniorArtifacts as x, juniorCodeRepositories as y };
786
+ export { juniorCodeRepositories as C, juniorCodeChanges as S, juniorArtifacts as T, CONVERSATION_METRICS as _, juniorReviewItemAssignees as a, juniorApiTokens as b, juniorReviewItems as c, juniorAutomations as d, juniorStats as f, juniorConversationBriefs as g, juniorConversationBindings as h, juniorWorkspaces as i, juniorAutomationRuns as l, juniorConversationParticipants as m, juniorSnapshots as n, juniorReviewItemConversations as o, juniorLocationConfigurations as p, juniorWorkspaceRepos as r, juniorReviewItemSlackCards as s, createJuniorSqlExecutor as t, juniorAutomationVersions as u, juniorConversationMetrics as v, juniorAttachments as w, juniorConversationAnnotations as x, juniorConversationEvents as y };
@@ -1,6 +1,6 @@
1
- import { F as getDeploymentTelemetryAttributes, P as getDeploymentServiceVersion } from "./logging-Ce0mXZ8J.js";
1
+ import { F as getDeploymentServiceVersion, I as getDeploymentTelemetryAttributes } from "./logging-DLSpkSSr.js";
2
2
  import { a as getClient, c as init, o as getGlobalScope } from "./sentry-MDkYAVPV.js";
3
- import { n as getCurrentConversationPrivacy } from "./conversation-privacy-CY7JVjxn.js";
3
+ import { n as getCurrentConversationPrivacy } from "./conversation-privacy-CxlOCiUo.js";
4
4
  import { t as consumePrivateTraceResultMarker } from "./private-trace-result-CE7_lEdq.js";
5
5
  // src/chat/sentry-payload-filter.ts
6
6
  const PAYLOAD_ATTRIBUTE_KEYS = /* @__PURE__ */ new Set([
@@ -164,6 +164,10 @@ function initSentry() {
164
164
  environment: process.env.SENTRY_ENVIRONMENT ?? process.env.VERCEL_ENV ?? process.env.NODE_ENV,
165
165
  release: serviceVersion,
166
166
  tracesSampleRate: getSampleRate(process.env.SENTRY_TRACES_SAMPLE_RATE, 1),
167
+ ignoreSpans: [{
168
+ op: "http.server",
169
+ attributes: { "url.path": "/_vercel/ping" }
170
+ }],
167
171
  dataCollection: {
168
172
  httpBodies: ["incomingRequest"],
169
173
  databaseQueryData: false
@@ -1,4 +1,4 @@
1
- import { Wt as extractAssistantText } from "./tasks-DDT-J3Rf.js";
1
+ import { Ln as extractAssistantText } from "./tasks-SB9z-VTU.js";
2
2
  // src/chat/no-reply.ts
3
3
  const NO_REPLY_MARKER = "[[NO_REPLY]]";
4
4
  /**
@@ -40,4 +40,10 @@ function getAssistantReplyText(message) {
40
40
  const decision = decideReply(message);
41
41
  return decision.kind === "deliver" ? decision.text : void 0;
42
42
  }
43
- export { isNoReplyMarker as a, NO_REPLY_MARKER as i, getAssistantReplyText as n, sanitizeAssistantText as r, decideReply as t };
43
+ // src/chat/interruption-marker.ts
44
+ const INTERRUPTED_MARKER = "\n\n[Response interrupted before completion]";
45
+ /** Return the marker added when a visible reply ended mid-execution. */
46
+ function getInterruptionMarker() {
47
+ return INTERRUPTED_MARKER;
48
+ }
49
+ export { NO_REPLY_MARKER as a, sanitizeAssistantText as i, decideReply as n, isNoReplyMarker as o, getAssistantReplyText as r, getInterruptionMarker as t };
@@ -1,5 +1,5 @@
1
- import { i as getChatConfig } from "./config-C0UwYqDK.js";
2
- import { T as withSpan } from "./logging-Ce0mXZ8J.js";
1
+ import { i as getChatConfig } from "./config-DrTeF80T.js";
2
+ import { E as withSpan, m as logInfo, p as logException } from "./logging-DLSpkSSr.js";
3
3
  import { AsyncLocalStorage } from "node:async_hooks";
4
4
  import { z } from "zod";
5
5
  import { createHash, createHmac, timingSafeEqual } from "node:crypto";
@@ -40,6 +40,131 @@ function runWithTurnRequestDeadline(callback, startedAtMs = Date.now()) {
40
40
  if (requestDeadlineStorage.getStore()) return callback();
41
41
  return requestDeadlineStorage.run(createTurnRequestDeadline(startedAtMs), callback);
42
42
  }
43
+ // src/chat/queue/bullmq.ts
44
+ const MAX_ATTEMPTS = 50;
45
+ const MAX_BACKOFF_MS = 3e4;
46
+ const COMPLETED_JOB_AGE_SECONDS = 3600;
47
+ const FAILED_JOB_AGE_SECONDS = 86400;
48
+ /**
49
+ * Import a module by a name held in a variable.
50
+ *
51
+ * Bundlers follow a literal `import("bullmq")` and put BullMQ into the
52
+ * production server, even though production never selects this provider.
53
+ * Node resolves this import when the app runs instead.
54
+ */
55
+ function importAtRuntime(specifier) {
56
+ return import(
57
+ /* @vite-ignore */
58
+ specifier
59
+ );
60
+ }
61
+ function bullmqConfig() {
62
+ const config = getChatConfig().queue;
63
+ if (config.provider !== "bullmq") throw new Error("The BullMQ queue provider is not configured");
64
+ return config;
65
+ }
66
+ /** Load BullMQ and open a Redis client that the caller must close. */
67
+ async function connect() {
68
+ const [bullmq, redis] = await Promise.all([importAtRuntime("bullmq"), importAtRuntime("redis")]).catch((error) => {
69
+ throw new Error("JUNIOR_QUEUE_PROVIDER=bullmq needs the bullmq and redis packages installed", { cause: error });
70
+ });
71
+ const client = redis.createClient({ url: bullmqConfig().redisUrl });
72
+ try {
73
+ await client.connect();
74
+ return {
75
+ bullmq,
76
+ connection: bullmq.createNodeRedisClient(client)
77
+ };
78
+ } catch (error) {
79
+ client.destroy();
80
+ throw error;
81
+ }
82
+ }
83
+ /** BullMQ job ids must not contain ":", and Junior's keys do. */
84
+ function toJobId(idempotencyKey) {
85
+ return `k-${createHash("sha256").update(idempotencyKey).digest("base64url")}`;
86
+ }
87
+ function metadataFor(job, consumer) {
88
+ return {
89
+ consumerGroup: consumer.consumerGroup,
90
+ createdAt: new Date(job.timestamp),
91
+ deliveryCount: job.attemptsMade + 1,
92
+ messageId: job.id ?? "",
93
+ topicName: consumer.topic
94
+ };
95
+ }
96
+ async function processJob(job, consumer) {
97
+ const { payload, retentionSeconds } = job.data;
98
+ if (retentionSeconds !== void 0 && Date.now() - job.timestamp > retentionSeconds * 1e3) return;
99
+ const metadata = metadataFor(job, consumer);
100
+ try {
101
+ await consumer.handler(payload, metadata);
102
+ } catch (error) {
103
+ if (consumer.retry(error, metadata)?.acknowledge) return;
104
+ throw error;
105
+ }
106
+ }
107
+ /** Start one BullMQ worker for each consumer, on a Redis client it owns. */
108
+ async function startWorker(consumers) {
109
+ const { bullmq, connection } = await connect();
110
+ const concurrency = bullmqConfig().workerConcurrency;
111
+ const workers = consumers.map((consumer) => {
112
+ const worker = new bullmq.Worker(consumer.topic, (job) => processJob(job, consumer), {
113
+ concurrency,
114
+ connection,
115
+ ...consumer.visibilityTimeoutSeconds !== void 0 && { lockDuration: consumer.visibilityTimeoutSeconds * 1e3 },
116
+ settings: { backoffStrategy: (attemptsMade) => Math.min(1e3 * 2 ** (attemptsMade - 1), MAX_BACKOFF_MS) }
117
+ });
118
+ worker.on("error", (error) => {
119
+ logException(error, "queue.bullmq.worker.exception", { "messaging.destination.name": consumer.topic });
120
+ });
121
+ return worker;
122
+ });
123
+ logInfo("queue.bullmq.worker.started", {
124
+ "app.queue.worker_concurrency": concurrency,
125
+ "messaging.destination.names": consumers.map((consumer) => consumer.topic)
126
+ });
127
+ return { close: async () => {
128
+ try {
129
+ const failures = (await Promise.allSettled(workers.map((worker) => worker.close()))).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
130
+ if (failures.length > 0) throw new AggregateError(failures, "BullMQ workers did not close");
131
+ } finally {
132
+ await connection.quit();
133
+ }
134
+ } };
135
+ }
136
+ /** Add one job on a Redis client that is open only for this send. */
137
+ async function send$1(topic, payload, options = {}) {
138
+ const { bullmq, connection } = await connect();
139
+ const queue = new bullmq.Queue(topic, { connection });
140
+ try {
141
+ const job = await queue.add(topic, {
142
+ payload,
143
+ retentionSeconds: options.retentionSeconds
144
+ }, {
145
+ attempts: MAX_ATTEMPTS,
146
+ backoff: { type: "custom" },
147
+ delay: options.delaySeconds ? options.delaySeconds * 1e3 : void 0,
148
+ jobId: options.idempotencyKey ? toJobId(options.idempotencyKey) : void 0,
149
+ removeOnComplete: { age: COMPLETED_JOB_AGE_SECONDS },
150
+ removeOnFail: { age: FAILED_JOB_AGE_SECONDS }
151
+ });
152
+ return job.id ? { messageId: job.id } : {};
153
+ } finally {
154
+ try {
155
+ await queue.close();
156
+ } finally {
157
+ await connection.quit();
158
+ }
159
+ }
160
+ }
161
+ /** Queue provider backed by BullMQ, for local development only. */
162
+ const bullmqQueueProvider = {
163
+ messagingSystem: "bullmq",
164
+ send: send$1,
165
+ createCallback: () => async () => new Response("This queue provider does not push messages", { status: 404 }),
166
+ startWorker
167
+ };
43
168
  // src/chat/queue/vercel.ts
44
169
  function defaultQueueClientOptions() {
45
170
  if (process.env.VERCEL_DEPLOYMENT_ID?.trim()) return {};
@@ -76,7 +201,7 @@ const vercelQueueProvider = {
76
201
  // src/chat/queue/provider.ts
77
202
  /** Return the queue provider for this deployment. */
78
203
  function getQueueProvider() {
79
- return vercelQueueProvider;
204
+ return getChatConfig().queue.provider === "bullmq" ? bullmqQueueProvider : vercelQueueProvider;
80
205
  }
81
206
  // src/chat/queue/sign.ts
82
207
  /** Sign and check queue messages before public callbacks run them. */