@sentry/junior 0.267.0 → 0.269.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 (95) hide show
  1. package/README.md +2 -9
  2. package/dist/{actor-DZSokpwj.js → actor-CP9K2Idu.js} +2 -1
  3. package/dist/{adapter-BybMEezj.js → adapter-2-CG25be.js} +3 -51
  4. package/dist/{admin-DOs1_EiW.js → admin-DSs8S5Uy.js} +1 -1
  5. package/dist/{agent-invocations-CxKldAeB.js → agent-invocations-C1RNOCs8.js} +2 -0
  6. package/dist/{agent-runner-DgNATN4D.js → agent-runner-CrDrPUtY.js} +3411 -1640
  7. package/dist/api/schema.d.ts +2 -5630
  8. package/dist/api/schema.js +7 -7
  9. package/dist/api.d.ts +3 -1
  10. package/dist/api.js +268 -287
  11. package/dist/{app-DAB3fzyp.d.ts → app-BrMMzjYm.d.ts} +10 -3
  12. package/dist/app.d.ts +1 -1
  13. package/dist/app.js +371 -223
  14. package/dist/{automation-CmeFBSEh.js → automation-p87hg_AV.js} +74 -74
  15. package/dist/{briefs-CWKjka7v.js → briefs-PtODdLNu.js} +4 -4
  16. package/dist/{catalog-runtime-cMlAI5LY.js → catalog-runtime-BsoSmTM0.js} +1 -1
  17. package/dist/cli/chat.js +16 -26
  18. package/dist/cli/check.js +3 -3
  19. package/dist/cli/main.js +5 -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-BjgbMawj.js → client-Cqf4TboL.js} +20 -15
  25. package/dist/{config-CdB6yJj5.js → config-CPsE4T5L.js} +2 -2
  26. package/dist/{connections-RE_65jzK.js → connections-upFXLmhG.js} +10 -11
  27. package/dist/{conversation-DSo310Ps.js → conversation-CTpD_PGk.js} +7 -84
  28. package/dist/{conversation-privacy-BzgJC5k0.js → conversation-privacy-D6zJc8ff.js} +1 -1
  29. package/dist/{credential-sync-DhEGEpgq.js → credential-sync-CS_6IC-f.js} +30 -34
  30. package/dist/{credentialed-CIGwoLU2.js → credentialed-BU-OW7HP.js} +577 -74
  31. package/dist/{store-B8CABbmR.d.ts → db-BzOCzVb1.d.ts} +17 -7
  32. package/dist/{executor-VYrx_K4w.js → executor-CNS7ONt-.js} +78 -4
  33. package/dist/instrumentation.js +2 -2
  34. package/dist/{assistant-reply-CT7VcXQe.js → interruption-marker-g6Xup3Fk.js} +8 -2
  35. package/dist/{job-queue-C0BZSa7r.js → job-queue-DI-k-Bbk.js} +2 -2
  36. package/dist/{list-1XQP-o6K.js → list-CGoQdXTp.js} +429 -1401
  37. package/dist/locks-D-0h111u.js +48 -0
  38. package/dist/{logging-DuZ1A4ab.js → logging-n7knse-O.js} +1 -1
  39. package/dist/{history-C84mlAwO.js → media-85PDJxO_.js} +83 -1
  40. package/dist/{message-NKSbbnA6.js → message-BpaWd8TT.js} +4 -4
  41. package/dist/nitro.d.ts +1 -1
  42. package/dist/nitro.js +3 -3
  43. package/dist/{oauth-callback-server-qAHLPtml.js → oauth-callback-server-CpGmEw_i.js} +2 -2
  44. package/dist/{prompt-BW9jFsqZ.js → prompt-BlQB0QQt.js} +1 -1
  45. package/dist/{registry-FQutO926.js → registry-By36_xsm.js} +2 -2
  46. package/dist/reporting-schema-CZREhYc-.js +104 -0
  47. package/dist/reporting-window-CVarfId9.js +255 -0
  48. package/dist/{resolve-Bk4yeuHy.js → resolve-CUIBcdPd.js} +3 -3
  49. package/dist/{result-B9e9ATOU.js → result-BemsOMOO.js} +31 -12
  50. package/dist/{runner-C3S2ObH-.js → runner-BVok7PAA.js} +12 -11
  51. package/dist/{sandbox-egress-signals-DqpGzzMB.js → sandbox-egress-signals-BSUWQI95.js} +1 -1
  52. package/dist/{schema-X7Fj4fv1.js → schema-ChnFo5f2.js} +98 -6
  53. package/dist/schema-jK4-1u_H.d.ts +5927 -0
  54. package/dist/{service-account-Do30HUNv.js → service-account-DHvlLnZw.js} +4 -3
  55. package/dist/{service-account-BQasl-qq.js → service-account-hMmESDcr.js} +3 -3
  56. package/dist/{session-stop-DkOjt46_.js → session-stop-BQW_iFcd.js} +499 -23
  57. package/dist/{skills-CQHrwfOa.js → skills-DmJJIuLF.js} +2 -2
  58. package/dist/{spawn-BHuqN14f.js → spawn-ChCm46i-.js} +2 -2
  59. package/dist/{store-6EetVP-m.js → store-1DVhoKOL.js} +4 -4
  60. package/dist/{store-DvNZIsCr.js → store-DCo2n5iJ.js} +3 -3
  61. package/dist/{task-queue-B0DLgd2P.js → task-queue-HMC4QjFm.js} +3 -3
  62. package/dist/task-runner-cA6yjn_E.js +269 -0
  63. package/dist/{thread-state-DRmdJ2QN.js → thread-state-CHy8LS0C.js} +8 -18
  64. package/dist/{turn-failure-response-G11cs2gE.js → turn-failure-response-Dr7erLWs.js} +11 -16
  65. package/dist/{turn-lifecycle-OzLIwvi1.js → turn-lifecycle-bf2fIYi_.js} +1 -1
  66. package/dist/{tasks-0ilmuCux.js → turn-session-routing-BqO0qXLk.js} +959 -44
  67. package/dist/{validation-CxEmt1id.js → validation-DHR1ivfh.js} +1 -1
  68. package/dist/{version-Bapedn4A.js → version-CXCHLgWm.js} +1 -1
  69. package/dist/version.js +1 -1
  70. package/dist/{viewer-DrN3oC2X.js → viewer-DSbD6DF6.js} +503 -241
  71. package/dist/{work-DUQoNcqP.js → work-BE_qTYGi.js} +17 -36
  72. package/dist/{worker-D1l4Ugk0.js → worker-Cm9_sWXg.js} +15 -7
  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/0069_system_identity.sql +25 -0
  78. package/migrations/0070_review_item_foreign_keys.sql +5 -0
  79. package/migrations/AGENTS.md +17 -0
  80. package/migrations/meta/0065_snapshot.json +3511 -0
  81. package/migrations/meta/0066_snapshot.json +4099 -0
  82. package/migrations/meta/0067_snapshot.json +3921 -0
  83. package/migrations/meta/0068_snapshot.json +4176 -0
  84. package/migrations/meta/0069_snapshot.json +3979 -0
  85. package/migrations/meta/0070_snapshot.json +3997 -0
  86. package/migrations/meta/_journal.json +42 -0
  87. package/package.json +5 -5
  88. package/dist/db-Bg-zC3r5.d.ts +0 -5
  89. package/dist/history-BgI33Kie.d.ts +0 -78
  90. package/dist/images-CecjXAAz.js +0 -64
  91. package/dist/task-runner-B-PD_JOj.js +0 -1074
  92. package/dist/turn-session-routing-Dyut9l7Y.js +0 -76
  93. package/dist/user-input-request-BDFBdF8r.js +0 -116
  94. package/dist/web-message-id-CJSUiksE.js +0 -236
  95. package/migrations/README.md +0 -30
@@ -1,26 +1,27 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-nwUcYXjP.js";
2
- import { C as toOptionalTrimmed, i as completeObject, o as embedTexts } from "./client-BjgbMawj.js";
3
- import { i as getChatConfig, n as botConfig, o as getSlackBotToken, v as normalizeSlackEmojiName } from "./config-CdB6yJj5.js";
4
- import { B as isRecord$2, E as withSpan, a as extractGenAiUsageSummary, b as setSpanAttributes, c as getLogContextAttributes, d as logAudit, h as logWarn, m as logInfo, p as logException, y as setSentryUser } from "./logging-DuZ1A4ab.js";
2
+ import { S as toOptionalTrimmed, i as completeObject, o as embedTexts } from "./client-Cqf4TboL.js";
3
+ import { i as getChatConfig, n as botConfig, o as getSlackBotToken, v as normalizeSlackEmojiName } from "./config-CPsE4T5L.js";
4
+ import { B as isRecord$2, E as withSpan, a as extractGenAiUsageSummary, b as setSpanAttributes, c as getLogContextAttributes, d as logAudit, h as logWarn, m as logInfo, p as logException, y as setSentryUser } from "./logging-n7knse-O.js";
5
5
  import { a as getClient$1, n as captureMessage, r as continueTrace } from "./sentry-MDkYAVPV.js";
6
- import { t as canExposeConversationPayload } from "./conversation-privacy-BzgJC5k0.js";
6
+ import { t as canExposeConversationPayload } from "./conversation-privacy-D6zJc8ff.js";
7
7
  import { r as defaultModelId } from "./model-profile-Bg8EoCKD.js";
8
- import { d as parseSlackTeamId, f as parseSlackUserId, i as isActorUserId, o as parseActorUserId, p as parseSlackMessageTs, u as parseSlackChannelReferenceId } from "./actor-DZSokpwj.js";
9
- import { B as parseDestination, I as saveUserTimezone, P as readUserTimezone, a as resolveViewerUser, i as readIdentityById, k as hasAgentTurnUsage, m as getSqlExecutor, p as getDb, r as readActorIdentity, u as getConversationEventStore, v as createSqlStore } from "./viewer-DrN3oC2X.js";
10
- import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, s as juniorConversations } from "./agent-invocations-CxKldAeB.js";
11
- import { _ as juniorConversationAnnotations, b as juniorAttachments, f as juniorConversationBriefs, h as juniorConversationEvents, i as juniorWorkspaces, n as juniorSnapshots, r as juniorWorkspaceRepos, u as juniorConversationParticipants, v as juniorCodeChanges, y as juniorCodeRepositories } from "./executor-VYrx_K4w.js";
12
- import { o as decodeStoredConversationEvent } from "./history-C84mlAwO.js";
13
- import { _ as CredentialUnavailableError, a as resolveAuthTokenPlaceholder, h as credentialSubjectSchema, m as credentialLogContext, o as resolvePluginCommandEnv, p as credentialContextSchema } from "./registry-FQutO926.js";
14
- import { n as pluginCatalogRuntime } from "./catalog-runtime-cMlAI5LY.js";
15
- import { c as withLock, i as getStateAdapter, o as acquireActiveLock, r as getDefaultRedisStateAdapterFor } from "./adapter-BybMEezj.js";
8
+ import { d as parseSlackChannelReferenceId, f as parseSlackTeamId, i as isActorUserId, m as parseSlackMessageTs, p as parseSlackUserId, s as parseActorUserId } from "./actor-CP9K2Idu.js";
9
+ import { C as getSqlExecutor, D as createSqlStore, L as readUserTimezone, S as getDb, X as parseDestination, g as createCache, i as readActorIdentity, l as resolveViewerUser, n as findUserByEmailFromSql, o as readIdentityById, p as parseAvatarUrl, q as hasAgentTurnUsage, s as readIdentityByIdFromSql, y as getConversationEventStore, z as saveUserTimezone } from "./viewer-DSbD6DF6.js";
10
+ import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, s as juniorConversations } from "./agent-invocations-C1RNOCs8.js";
11
+ import { C as juniorCodeRepositories, S as juniorCodeChanges, a as juniorReviewItemAssignees, c as juniorReviewItems, d as juniorAutomations, g as juniorConversationBriefs, i as juniorWorkspaces, m as juniorConversationParticipants, n as juniorSnapshots, o as juniorReviewItemConversations, r as juniorWorkspaceRepos, w as juniorAttachments, x as juniorConversationAnnotations, y as juniorConversationEvents } from "./executor-CNS7ONt-.js";
12
+ import { l as decodeStoredConversationEvent } from "./media-85PDJxO_.js";
13
+ import { _ as CredentialUnavailableError, a as resolveAuthTokenPlaceholder, h as credentialSubjectSchema, m as credentialLogContext, o as resolvePluginCommandEnv, p as credentialContextSchema } from "./registry-By36_xsm.js";
14
+ import { n as pluginCatalogRuntime } from "./catalog-runtime-BsoSmTM0.js";
15
+ import { r as acquireActiveLock } from "./locks-D-0h111u.js";
16
+ import { n as getDefaultRedisStateAdapterFor, r as getStateAdapter } from "./adapter-2-CG25be.js";
16
17
  import { i as isValidTimeZone } from "./cadence-R7Em4RjE.js";
17
- import { C as conversationReportEventSchema, Q as parseBriefInput, W as readMessageAttachments, Z as conversationBriefSchema, w as conversationReportMessageEventSchema, z as readMessageCardRefs } from "./conversation-DSo310Ps.js";
18
- import { n as briefEntriesFromReportEvents, s as generateBrief, t as BRIEF_PROMPT } from "./prompt-BW9jFsqZ.js";
18
+ import { C as conversationReportEventSchema, J as conversationBriefSchema, W as readMessageAttachments, Y as parseBriefInput, w as conversationReportMessageEventSchema, z as readMessageCardRefs } from "./conversation-CTpD_PGk.js";
19
+ import { n as briefEntriesFromReportEvents, s as generateBrief, t as BRIEF_PROMPT } from "./prompt-BlQB0QQt.js";
19
20
  import path from "node:path";
20
21
  import { AsyncLocalStorage } from "node:async_hooks";
21
22
  import { z } from "zod";
22
23
  import { createHash, createHmac, createPrivateKey, createPublicKey, generateKeyPairSync, randomBytes, randomUUID, sign, timingSafeEqual } from "node:crypto";
23
- import { and, arrayContains, asc, desc, eq, exists, getTableColumns, gte, inArray, isNotNull, isNull, lt, lte, max, ne, notExists, or, sql } from "drizzle-orm";
24
+ import { and, arrayContains, asc, count, desc, eq, exists, getTableColumns, gte, ilike, inArray, isNotNull, isNull, lt, lte, max, ne, notExists, or, sql } from "drizzle-orm";
24
25
  import { EgressAuthRequired, EgressPolicyDenied, actorSchema, codeChangeInputSchema, conversationAnnotationInputSchema, conversationEventPresentationSchema, defineConversationEvent, eventDataSchema, eventInputSchema, eventMatchSchema, eventMatches, eventTypeSchema, missingToolAnnotationKeys, normalizeEventIdentifier, objectAnnotationSchema, objectIconPath, objectTypeSchema, pluginAuthorizationSchema, pluginCredentialHeaderTransformSchema, pluginCredentialResultSchema, pluginEventsSchema, pluginGrantSchema, pluginProviderAccountSchema, pluginStoredTokensSchema, promptContextSchema, promptMessageSchema, sourceSchema, stableEventMatchKey } from "@sentry/junior-plugin-api";
25
26
  import { alias, union } from "drizzle-orm/pg-core";
26
27
  import fs from "node:fs/promises";
@@ -166,39 +167,6 @@ function renderJuniorNativeConversationEvent(args) {
166
167
  const presentation = definition.renderEvent(definition.parse(args.content));
167
168
  return presentation === void 0 ? void 0 : conversationEventPresentationSchema.parse(presentation);
168
169
  }
169
- // src/chat/state/cache.ts
170
- const MAX_CACHE_KEY_LENGTH = 512;
171
- const LOAD_WAIT_MS = 1e4;
172
- /**
173
- * Create a cache in the state adapter under `junior:cache:{namespace}:`.
174
- * Entries are wrapped, so `null` and `false` values are cache hits.
175
- */
176
- function createCache(state, namespace) {
177
- return { async getOrLoad(key, load, options) {
178
- if (!key.trim()) throw new Error("Cache key is required");
179
- if (key.length > MAX_CACHE_KEY_LENGTH) throw new Error("Cache key exceeds the maximum length");
180
- const cacheKey = `junior:cache:${namespace}:${key}`;
181
- const cached = async () => options?.refresh ? void 0 : await state.get(cacheKey) ?? void 0;
182
- await state.connect();
183
- const hit = await cached();
184
- if (hit) return hit.value;
185
- const attempt = await withLock(state, `${cacheKey}:load`, async () => {
186
- const loadedMeanwhile = await cached();
187
- if (loadedMeanwhile) return loadedMeanwhile.value;
188
- const entry = await load();
189
- if (entry.ttlMs !== void 0) {
190
- if (!(entry.ttlMs > 0)) throw new Error("Cache ttlMs must be positive");
191
- await state.set(cacheKey, { value: entry.value }, entry.ttlMs);
192
- }
193
- return entry.value;
194
- }, {
195
- keepAlive: true,
196
- waitMs: LOAD_WAIT_MS
197
- });
198
- if (attempt.acquired) return attempt.value;
199
- return (await load()).value;
200
- } };
201
- }
202
170
  // src/chat/plugins/cache.ts
203
171
  /** Create the cache scoped to one plugin. */
204
172
  function createPluginCache(plugin) {
@@ -214,6 +182,8 @@ const slackUserInfoSchema = z.object({
214
182
  tz: z.string().nullish(),
215
183
  profile: z.object({
216
184
  email: z.string().nullish(),
185
+ image_512: z.string().nullish(),
186
+ is_custom_image: z.boolean().nullish(),
217
187
  real_name: z.string().nullish()
218
188
  }).optional()
219
189
  })
@@ -258,10 +228,14 @@ async function lookupSlackUser(teamId, userId) {
258
228
  const parsed = slackUserInfoSchema.safeParse(await response.json());
259
229
  if (!parsed.success) return null;
260
230
  const payload = parsed.data;
231
+ const userName = payload.user.name?.trim() || void 0;
232
+ const fullName = payload.user.profile?.real_name?.trim() || void 0;
233
+ const avatarUrl = payload.user.profile?.is_custom_image ? parseAvatarUrl(payload.user.profile.image_512, "slack") : void 0;
261
234
  const result = {
262
- userName: payload.user.name?.trim() || void 0,
263
- fullName: payload.user.profile?.real_name?.trim() || void 0,
235
+ userName,
236
+ fullName,
264
237
  email: payload.user.profile?.email?.trim() || void 0,
238
+ ...avatarUrl ? { avatarUrl } : void 0,
265
239
  timezone: payload.user.tz?.trim() || void 0
266
240
  };
267
241
  writeToCache(teamId, userId, result);
@@ -1599,6 +1573,8 @@ async function readConversationAccessFromSql(db, conversationIds, viewer) {
1599
1573
  rootParentConversationId: rootConversation.parentConversationId,
1600
1574
  rootRootConversationId: rootConversation.rootConversationId,
1601
1575
  visibility: rootDestination.visibility,
1576
+ provider: rootDestination.provider,
1577
+ providerTenantId: rootDestination.providerTenantId,
1602
1578
  rootEmailNormalized: rootIdentity.emailNormalized,
1603
1579
  rootEmailVerified: rootIdentity.emailVerified,
1604
1580
  rootUserId: rootIdentity.userId
@@ -1619,7 +1595,11 @@ async function readConversationAccessFromSql(db, conversationIds, viewer) {
1619
1595
  return [row.conversationId, {
1620
1596
  canViewPrivateContent,
1621
1597
  isParticipant,
1622
- visibility
1598
+ visibility,
1599
+ ...validRootConversationId !== void 0 && row.provider !== null && row.providerTenantId ? { tenant: {
1600
+ provider: row.provider,
1601
+ providerTenantId: row.providerTenantId
1602
+ } } : void 0
1623
1603
  }];
1624
1604
  }));
1625
1605
  }
@@ -2030,6 +2010,10 @@ function getDashboardConversationLink(conversationId) {
2030
2010
  function getDashboardTaskLink(taskId) {
2031
2011
  return resolveDashboardPath(`/automations/${encodeURIComponent(taskId)}`);
2032
2012
  }
2013
+ /** Build the review desk URL that selects one Review item. */
2014
+ function getDashboardReviewItemLink(itemId) {
2015
+ return resolveDashboardPath(`/review?item=${encodeURIComponent(itemId)}`);
2016
+ }
2033
2017
  /** Public icon asset URL. Headless and local installs keep text-only identity. */
2034
2018
  function getDashboardObjectIconLink(icon) {
2035
2019
  if (!dashboardConversationLinkOptions) return void 0;
@@ -3075,19 +3059,6 @@ function getSlackToolContext(context) {
3075
3059
  threadTs: parseSlackMessageTs(location.threadTs)
3076
3060
  };
3077
3061
  }
3078
- /**
3079
- * Return whether a person started this Turn with a Slack message.
3080
- *
3081
- * Only these Turns act with that person's Slack authority. Event, automation,
3082
- * Agent invocation, and dashboard Turns in the same Conversation do not.
3083
- */
3084
- function isSlackMessageTurn(context) {
3085
- return context.source.kind === "slack" && context.destination.platform === "slack" && context.actor?.platform === "slack" && context.actor.teamId === context.source.teamId;
3086
- }
3087
- /** Disable reason for tools that need a Slack message Turn. */
3088
- function slackMessageTurnDisabledReason(context) {
3089
- return isSlackMessageTurn(context) ? void 0 : "It needs a Turn that a person started with a Slack message.";
3090
- }
3091
3062
  // src/chat/slack/tool-support/channel-capabilities.ts
3092
3063
  /** Resolve channel capabilities from a Slack channel ID. */
3093
3064
  function resolveChannelCapabilities(channelId) {
@@ -5555,6 +5526,24 @@ async function deleteWorkspaceSnapshotBuilders$1(names) {
5555
5526
  }
5556
5527
  if (failures.length > 0) throw failures[0];
5557
5528
  }
5529
+ const DOCKER_SNAPSHOT_PACKAGES = /* @__PURE__ */ new Set([
5530
+ "spal-release",
5531
+ "ripgrep",
5532
+ "gh",
5533
+ "jq"
5534
+ ]);
5535
+ /**
5536
+ * Keep only the small Docker base from the declared runtime dependencies.
5537
+ *
5538
+ * Postinstall commands are skipped, because they often download large tools.
5539
+ * A base package that no plugin declares is not installed.
5540
+ */
5541
+ function selectSnapshotRuntime(runtime) {
5542
+ return {
5543
+ dependencies: runtime.dependencies.filter((dependency) => "package" in dependency && DOCKER_SNAPSHOT_PACKAGES.has(dependency.package)),
5544
+ postinstall: []
5545
+ };
5546
+ }
5558
5547
  /** Local Docker implementation of the sandbox provider contract. */
5559
5548
  const dockerSandboxProvider = {
5560
5549
  errors: dockerSandboxErrors,
@@ -5562,7 +5551,8 @@ const dockerSandboxProvider = {
5562
5551
  getSession,
5563
5552
  createWorkspaceSnapshotBuilder: createWorkspaceSnapshotBuilder$1,
5564
5553
  getWorkspaceSnapshotBuilder: getWorkspaceSnapshotBuilder$1,
5565
- deleteWorkspaceSnapshotBuilders: deleteWorkspaceSnapshotBuilders$1
5554
+ deleteWorkspaceSnapshotBuilders: deleteWorkspaceSnapshotBuilders$1,
5555
+ selectSnapshotRuntime
5566
5556
  };
5567
5557
  // src/chat/sandbox/vercel/credentials.ts
5568
5558
  /**
@@ -5812,6 +5802,22 @@ async function postSlackMessage(input) {
5812
5802
  }) } : void 0
5813
5803
  };
5814
5804
  }
5805
+ /** Delete a previously posted Slack message through the shared outbound boundary. */
5806
+ async function deleteSlackMessage(input) {
5807
+ const channelId = requireSlackConversationId(input.channelId, "Slack message deletion");
5808
+ const timestamp = requireSlackMessageTimestamp(input.timestamp, "Slack message deletion");
5809
+ await withSlackRetries(() => getSlackClient().chat.delete({
5810
+ channel: channelId,
5811
+ ts: timestamp
5812
+ }), 3, {
5813
+ action: "chat.delete",
5814
+ idempotent: true,
5815
+ spanAttributes: {
5816
+ "app.slack.channel_id": channelId,
5817
+ "app.slack.message_ts": timestamp
5818
+ }
5819
+ });
5820
+ }
5815
5821
  /** Replace the text and blocks of a message that Junior posted. */
5816
5822
  async function updateSlackMessage(input) {
5817
5823
  const channelId = requireSlackConversationId(input.channelId, "Slack message update");
@@ -7066,9 +7072,12 @@ function workspaceRecipe(workspace) {
7066
7072
  }
7067
7073
  /** Build the complete profile that selects one reusable sandbox snapshot. */
7068
7074
  function create(runtime, workspace) {
7069
- const dependencies = mergeDependencies([...GLOBAL_RUNTIME_DEPENDENCIES, ...pluginCatalogRuntime.getRuntimeDependencies()]);
7070
- const pluginPostinstall = pluginCatalogRuntime.getRuntimePostinstall();
7071
- const postinstall = [...GLOBAL_RUNTIME_POSTINSTALL, ...pluginPostinstall];
7075
+ const declared = {
7076
+ dependencies: mergeDependencies([...GLOBAL_RUNTIME_DEPENDENCIES, ...pluginCatalogRuntime.getRuntimeDependencies()]),
7077
+ postinstall: [...GLOBAL_RUNTIME_POSTINSTALL, ...pluginCatalogRuntime.getRuntimePostinstall()]
7078
+ };
7079
+ const { dependencies, postinstall } = getSandboxProvider().selectSnapshotRuntime?.(declared) ?? declared;
7080
+ const pluginPostinstall = postinstall.filter((command) => !GLOBAL_RUNTIME_POSTINSTALL.includes(command));
7072
7081
  if (dependencies.length === 0 && postinstall.length === 0 && !workspace) return null;
7073
7082
  const rebuildEpoch = process.env.SANDBOX_SNAPSHOT_REBUILD_EPOCH?.trim() ?? "";
7074
7083
  const floating = dependencies.some((dependency) => isFloating(dependency)) || pluginPostinstall.length > 0 || Boolean(workspace);
@@ -7526,6 +7535,488 @@ function createCodeChangePublisher(provider) {
7526
7535
  }
7527
7536
  };
7528
7537
  }
7538
+ // src/chat/review-items/assignees.ts
7539
+ /** Text that the agent writes to assign Everyone. */
7540
+ const EVERYONE_ASSIGNEE = "everyone";
7541
+ /**
7542
+ * Resolve one assignee: Everyone, an email, or a mention. A mention must be
7543
+ * in the tenant of the item.
7544
+ */
7545
+ async function resolveAssignee(db, value, tenant, findMentionedUserId) {
7546
+ const text = value.trim();
7547
+ if (text.toLowerCase() === "everyone") return { everyone: true };
7548
+ const mentionedUserId = await findMentionedUserId(text, tenant);
7549
+ if (mentionedUserId) return { userId: mentionedUserId };
7550
+ if (text.includes("@")) {
7551
+ const user = await findUserByEmailFromSql(db, text);
7552
+ if (!user) throw new ToolInputError(`No Junior user has the email ${text}.`);
7553
+ return { userId: user.id };
7554
+ }
7555
+ throw new ToolInputError(`Unknown assignee ${JSON.stringify(text)}. Use ${EVERYONE_ASSIGNEE}, an email, or a mention such as <@U123>.`);
7556
+ }
7557
+ /** Resolve the assignees of a Review item. One is required. */
7558
+ async function resolveAssignees(db, values, tenant, findMentionedUserId) {
7559
+ if (values.length === 0) throw new ToolInputError("A review item needs at least one assignee.");
7560
+ const resolved = await Promise.all(values.map((value) => resolveAssignee(db, value, tenant, findMentionedUserId)));
7561
+ return {
7562
+ everyone: resolved.some((assignee) => "everyone" in assignee),
7563
+ userIds: [...new Set(resolved.flatMap((assignee) => "userId" in assignee ? [assignee.userId] : []))]
7564
+ };
7565
+ }
7566
+ /** Return which of the Users take part in one root Conversation. */
7567
+ async function conversationParticipantUserIds(db, rootConversationId, userIds) {
7568
+ if (userIds.length === 0) return /* @__PURE__ */ new Set();
7569
+ const rows = await db.select({ userId: juniorConversationParticipants.userId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.rootConversationId, rootConversationId), inArray(juniorConversationParticipants.userId, [...userIds])));
7570
+ return new Set(rows.map((row) => row.userId));
7571
+ }
7572
+ /** Return the User who created an Automation, if Junior knows them. */
7573
+ async function automationCreatorUserId(db, automationId) {
7574
+ const [row] = await db.select({ userId: juniorIdentities.userId }).from(juniorAutomations).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorAutomations.creatorIdentityId)).where(eq(juniorAutomations.id, automationId)).limit(1);
7575
+ return row?.userId ?? void 0;
7576
+ }
7577
+ // src/chat/review-items/access.ts
7578
+ /** Return whether two tenants are the same. Two missing tenants match. */
7579
+ function isSameTenant(a, b) {
7580
+ return a?.provider === b?.provider && a?.providerTenantId === b?.providerTenantId;
7581
+ }
7582
+ /**
7583
+ * The reader for one person: items assigned to them, and public items in the
7584
+ * provider tenants of their identities.
7585
+ */
7586
+ function reviewItemReaderForUser(user) {
7587
+ return {
7588
+ tenants: user.identities.flatMap((identity) => identity.providerTenantId ? [{
7589
+ provider: identity.provider,
7590
+ providerTenantId: identity.providerTenantId
7591
+ }] : []),
7592
+ userId: user.id
7593
+ };
7594
+ }
7595
+ /** SQL: the Review item has this assigned User. */
7596
+ function reviewItemAssignedTo(userId) {
7597
+ return sql`exists (select 1 from ${juniorReviewItemAssignees} where ${juniorReviewItemAssignees.itemId} = ${juniorReviewItems.id} and ${juniorReviewItemAssignees.userId} = ${userId})`;
7598
+ }
7599
+ /**
7600
+ * SQL: the Review item needs this User. Only an open `action` item that is
7601
+ * assigned to the User needs them. A notice, an item for Everyone, and an
7602
+ * item without assignees need no one. An assigned User can always see the
7603
+ * item, so this rule needs no visibility check. The nav count and the
7604
+ * "Waiting on you" marker use this rule.
7605
+ */
7606
+ function reviewItemNeedsUser(userId) {
7607
+ return and(eq(juniorReviewItems.status, "open"), eq(juniorReviewItems.kind, "action"), reviewItemAssignedTo(userId));
7608
+ }
7609
+ /** SQL: the Review item links this Conversation. */
7610
+ function reviewItemLinkedTo(conversationId) {
7611
+ return sql`exists (select 1 from ${juniorReviewItemConversations} where ${juniorReviewItemConversations.itemId} = ${juniorReviewItems.id} and ${juniorReviewItemConversations.conversationId} = ${conversationId})`;
7612
+ }
7613
+ function publicIn(tenants) {
7614
+ return tenants.map((tenant) => and(eq(juniorReviewItems.visibility, "public"), eq(juniorReviewItems.provider, tenant.provider), eq(juniorReviewItems.providerTenantId, tenant.providerTenantId)));
7615
+ }
7616
+ /**
7617
+ * SQL form of `canReadReviewItem`, for search. Keep the two the same; the
7618
+ * Review item access test checks that they agree.
7619
+ */
7620
+ function reviewItemVisibleTo(reader) {
7621
+ return or(sql`false`, ...publicIn(reader.tenants ?? []), ...reader.userId ? [reviewItemAssignedTo(reader.userId)] : [], ...reader.conversationId ? [reviewItemLinkedTo(reader.conversationId)] : [], ...reader.automationId ? [eq(juniorReviewItems.automationId, reader.automationId)] : []);
7622
+ }
7623
+ /** Return whether a reader can see one Review item. */
7624
+ function canReadReviewItem(item, reader) {
7625
+ return item.visibility === "public" && item.tenant !== void 0 && (reader.tenants ?? []).some((tenant) => isSameTenant(tenant, item.tenant)) || reader.userId !== void 0 && item.assignees.some((assignee) => assignee.userId === reader.userId) || reader.conversationId !== void 0 && item.conversationIds.includes(reader.conversationId) || reader.automationId !== void 0 && item.automationId === reader.automationId;
7626
+ }
7627
+ /**
7628
+ * Read the scope of an agent. The agent reads as its Conversation and
7629
+ * Automation, not as its Actor: an assignment lets a person see the item on
7630
+ * the review desk, not let an agent read it into another Conversation.
7631
+ */
7632
+ async function readReviewItemAgentScope(context) {
7633
+ const user = (await context.resolveActorIdentity?.())?.user;
7634
+ const automationId = context.source.kind === "scheduled_automation" || context.source.kind === "event_automation" ? context.source.automationId : void 0;
7635
+ const current = (await readConversationAccessFromSql(getDb(), [context.conversationId], user)).get(context.conversationId);
7636
+ const tenant = current?.tenant;
7637
+ return {
7638
+ inPrivateConversation: current?.visibility !== "public",
7639
+ reader: {
7640
+ conversationId: context.conversationId,
7641
+ ...automationId ? { automationId } : void 0,
7642
+ ...tenant ? { tenants: [tenant] } : void 0
7643
+ },
7644
+ ...tenant ? { tenant } : void 0,
7645
+ ...user ? { user } : void 0
7646
+ };
7647
+ }
7648
+ /**
7649
+ * Dedupe keys with this prefix belong to plugins. Only the plugin that added
7650
+ * the item, or a person, can change it. An agent cannot use the prefix.
7651
+ */
7652
+ const PLUGIN_REVIEW_ITEM_KEY_PREFIX = "plugin:";
7653
+ /**
7654
+ * Whether an agent can change an item. Only an agent in the tenant of the
7655
+ * item can change it. A public item changes only from a public
7656
+ * Conversation, so private content never reaches its readers. A private
7657
+ * item changes only from the Conversation or Automation that added it. An
7658
+ * agent never changes an item that a plugin added.
7659
+ */
7660
+ function canAgentChangeReviewItem(item, scope) {
7661
+ if (item.dedupeKey.startsWith("plugin:")) return false;
7662
+ if (!isSameTenant(item.tenant, scope.tenant)) return false;
7663
+ if (item.visibility === "public") return !scope.inPrivateConversation;
7664
+ return item.addedByConversationId === scope.reader.conversationId || scope.reader.automationId !== void 0 && item.automationId === scope.reader.automationId;
7665
+ }
7666
+ /**
7667
+ * Check the Conversations that an agent links. Every link must be in the
7668
+ * tenant of the item. `allowedIds` (the current Conversation and the links
7669
+ * that the item already has) always pass. A private item gets no other
7670
+ * links. A public item can get other links only to public Conversations.
7671
+ */
7672
+ async function requireReviewItemLinks(args) {
7673
+ const access = await readConversationAccessFromSql(getDb(), args.conversationIds, args.scope.user);
7674
+ for (const conversationId of args.conversationIds) {
7675
+ const conversation = access.get(conversationId);
7676
+ if (!conversation || !isSameTenant(conversation.tenant, args.tenant)) throw new ToolInputError(`Conversation ${conversationId} is not found.`);
7677
+ if (args.allowedIds.has(conversationId)) continue;
7678
+ if (args.visibility === "private") throw new ToolInputError("A private item links only to the Conversation that added it.");
7679
+ if (conversation.visibility !== "public") throw new ToolInputError(conversation.canViewPrivateContent ? `Conversation ${conversationId} is private. Only public Conversations can be linked.` : `Conversation ${conversationId} is not found.`);
7680
+ }
7681
+ }
7682
+ /**
7683
+ * Check who an agent assigns a private item to. It cannot go to Everyone.
7684
+ * Each User must be the current Actor, a person in one of the given
7685
+ * Conversations (the current one and, for an existing item, the one that
7686
+ * added it), or the creator of the Automation that added the item. An
7687
+ * Automation run often has no Actor and no people yet, and its creator wrote
7688
+ * the work. A public item can go to anyone.
7689
+ */
7690
+ async function requireReviewItemAssignees(args) {
7691
+ if (args.visibility === "public") return;
7692
+ if (args.assignees.everyone) throw new ToolInputError("A private item cannot be assigned to Everyone. Assign Users instead.");
7693
+ const creatorId = args.automationId ? await automationCreatorUserId(getDb(), args.automationId) : void 0;
7694
+ const others = args.assignees.userIds.filter((id) => id !== args.scope.user?.id && id !== creatorId);
7695
+ const inConversation = /* @__PURE__ */ new Set();
7696
+ for (const conversationId of new Set(args.conversationIds)) for (const id of await conversationParticipantUserIds(getDb(), conversationId, others)) inConversation.add(id);
7697
+ if (others.some((id) => !inConversation.has(id))) throw new ToolInputError("A private item can be assigned only to people in its Conversation.");
7698
+ }
7699
+ /**
7700
+ * Return whether a User can act on an open Review item: an assigned User,
7701
+ * anyone for an item assigned to Everyone, and anyone for an item with no
7702
+ * assignees. The caller checks that the User can see the item.
7703
+ */
7704
+ function canActOnReviewItem(item, userId) {
7705
+ if (item.status !== "open") return false;
7706
+ if (item.assignedToEveryone) return true;
7707
+ if (item.assignees.length === 0) return true;
7708
+ return item.assignees.some((assignee) => assignee.userId === userId);
7709
+ }
7710
+ /** Return whether a User is already the only assignee, so take does nothing. */
7711
+ function isOnlyAssignee(item, userId) {
7712
+ return !item.assignedToEveryone && item.assignees.length === 1 && item.assignees[0]?.userId === userId;
7713
+ }
7714
+ // src/chat/review-items/store.ts
7715
+ /**
7716
+ * Close reason of an action item that a person answered with a reply in
7717
+ * its thread instead of on the card or the review desk.
7718
+ */
7719
+ const REVIEW_ITEM_REPLIED_REASON = "Replied in thread";
7720
+ async function readUsers(db, userIds) {
7721
+ if (userIds.length === 0) return /* @__PURE__ */ new Map();
7722
+ const rows = await db.select({
7723
+ displayName: juniorUsers.displayName,
7724
+ email: juniorUsers.primaryEmail,
7725
+ userId: juniorUsers.id
7726
+ }).from(juniorUsers).where(inArray(juniorUsers.id, [...new Set(userIds)]));
7727
+ return new Map(rows.map((row) => [row.userId, {
7728
+ email: row.email,
7729
+ userId: row.userId,
7730
+ ...row.displayName ? { displayName: row.displayName } : void 0
7731
+ }]));
7732
+ }
7733
+ async function hydrate(db, rows) {
7734
+ if (rows.length === 0) return [];
7735
+ const ids = rows.map((row) => row.id);
7736
+ const [assigneeRows, conversationRows] = await Promise.all([db.select().from(juniorReviewItemAssignees).where(inArray(juniorReviewItemAssignees.itemId, ids)), db.select().from(juniorReviewItemConversations).where(inArray(juniorReviewItemConversations.itemId, ids))]);
7737
+ const users = await readUsers(db, [...assigneeRows.map((row) => row.userId), ...rows.flatMap((row) => row.closedByUserId ? [row.closedByUserId] : [])]);
7738
+ return rows.map((row) => {
7739
+ const closedBy = row.closedByUserId ? users.get(row.closedByUserId) : void 0;
7740
+ return {
7741
+ addedByConversationId: row.addedByConversationId,
7742
+ assignedToEveryone: row.assignedToEveryone,
7743
+ assignees: assigneeRows.filter((assignee) => assignee.itemId === row.id).flatMap((assignee) => users.get(assignee.userId) ?? []).sort((a, b) => a.email.localeCompare(b.email)),
7744
+ body: row.body,
7745
+ conversationIds: conversationRows.filter((link) => link.itemId === row.id).map((link) => link.conversationId).sort(),
7746
+ createdAt: row.createdAt,
7747
+ dedupeKey: row.dedupeKey,
7748
+ id: row.id,
7749
+ kind: row.kind,
7750
+ status: row.status,
7751
+ title: row.title,
7752
+ updatedAt: row.updatedAt,
7753
+ visibility: row.visibility,
7754
+ ...row.answer ? { answer: row.answer } : void 0,
7755
+ ...row.automationId ? { automationId: row.automationId } : void 0,
7756
+ ...row.closeReason ? { closeReason: row.closeReason } : void 0,
7757
+ ...row.closedAt ? { closedAt: row.closedAt } : void 0,
7758
+ ...closedBy ? { closedBy } : void 0,
7759
+ ...row.request ? { request: row.request } : void 0,
7760
+ ...row.provider && row.providerTenantId ? { tenant: {
7761
+ provider: row.provider,
7762
+ providerTenantId: row.providerTenantId
7763
+ } } : void 0,
7764
+ ...row.subjectId ? { subject: {
7765
+ id: row.subjectId,
7766
+ ...row.subjectTitle ? { title: row.subjectTitle } : void 0,
7767
+ ...row.subjectUrl ? { url: row.subjectUrl } : void 0
7768
+ } } : void 0
7769
+ };
7770
+ });
7771
+ }
7772
+ function contentColumns(content) {
7773
+ return {
7774
+ assignedToEveryone: content.assignedToEveryone,
7775
+ body: content.body,
7776
+ kind: content.kind,
7777
+ request: content.request ?? null,
7778
+ subjectId: content.subject?.id ?? null,
7779
+ subjectTitle: content.subject?.title ?? null,
7780
+ subjectUrl: content.subject?.url ?? null,
7781
+ title: content.title,
7782
+ visibility: content.visibility
7783
+ };
7784
+ }
7785
+ async function replaceLinks(db, itemId, content) {
7786
+ await db.delete(juniorReviewItemAssignees).where(eq(juniorReviewItemAssignees.itemId, itemId));
7787
+ await db.delete(juniorReviewItemConversations).where(eq(juniorReviewItemConversations.itemId, itemId));
7788
+ const userIds = [...new Set(content.assigneeUserIds)];
7789
+ if (userIds.length > 0) await db.insert(juniorReviewItemAssignees).values(userIds.map((userId) => ({
7790
+ itemId,
7791
+ userId
7792
+ })));
7793
+ const conversationIds = [...new Set(content.conversationIds)];
7794
+ if (conversationIds.length > 0) await db.insert(juniorReviewItemConversations).values(conversationIds.map((conversationId) => ({
7795
+ itemId,
7796
+ conversationId
7797
+ })));
7798
+ }
7799
+ /** Read one Review item by id. */
7800
+ async function getReviewItem(db, id) {
7801
+ return (await hydrate(db, await db.select().from(juniorReviewItems).where(eq(juniorReviewItems.id, id))))[0];
7802
+ }
7803
+ /**
7804
+ * Read the action items that the agent added in one Conversation and that a
7805
+ * reply can answer: the open items, and the items that a reply closed.
7806
+ */
7807
+ async function listReplyActionItems(db, conversationId) {
7808
+ return await hydrate(db, await db.select().from(juniorReviewItems).where(and(eq(juniorReviewItems.addedByConversationId, conversationId), eq(juniorReviewItems.kind, "action"), or(eq(juniorReviewItems.status, "open"), eq(juniorReviewItems.closeReason, REVIEW_ITEM_REPLIED_REASON)))));
7809
+ }
7810
+ /**
7811
+ * Read the open Review item with one dedupe key in one provider tenant, if
7812
+ * there is one. Dedupe keys are unique only inside a tenant.
7813
+ */
7814
+ async function getOpenReviewItemByDedupeKey(db, dedupeKey, tenant) {
7815
+ return (await hydrate(db, await db.select().from(juniorReviewItems).where(and(eq(juniorReviewItems.dedupeKey, dedupeKey), eq(juniorReviewItems.status, "open"), tenant ? and(eq(juniorReviewItems.provider, tenant.provider), eq(juniorReviewItems.providerTenantId, tenant.providerTenantId)) : isNull(juniorReviewItems.provider)))))[0];
7816
+ }
7817
+ /**
7818
+ * Add one open Review item. Return undefined when another open item in the
7819
+ * same tenant already uses its dedupe key.
7820
+ */
7821
+ async function insertReviewItem(db, input) {
7822
+ const now = new Date(input.nowMs);
7823
+ const id = await db.transaction(async (tx) => {
7824
+ const inserted = await tx.insert(juniorReviewItems).values({
7825
+ ...contentColumns(input),
7826
+ addedByConversationId: input.addedByConversationId,
7827
+ automationId: input.automationId ?? null,
7828
+ createdAt: now,
7829
+ dedupeKey: input.dedupeKey,
7830
+ id: randomUUID(),
7831
+ provider: input.tenant?.provider ?? null,
7832
+ providerTenantId: input.tenant?.providerTenantId ?? null,
7833
+ status: "open",
7834
+ updatedAt: now
7835
+ }).onConflictDoNothing().returning({ id: juniorReviewItems.id });
7836
+ if (!inserted[0]) return void 0;
7837
+ await replaceLinks(tx, inserted[0].id, input);
7838
+ return inserted[0].id;
7839
+ });
7840
+ if (!id) return void 0;
7841
+ const item = await getReviewItem(db, id);
7842
+ if (!item) throw new Error(`Review item ${id} is missing after insert`);
7843
+ return item;
7844
+ }
7845
+ /** Replace the content of one open Review item. Return false when closed. */
7846
+ async function updateReviewItemContent(db, input) {
7847
+ return await db.transaction(async (tx) => {
7848
+ if (!(await tx.update(juniorReviewItems).set({
7849
+ ...contentColumns(input),
7850
+ updatedAt: new Date(input.nowMs)
7851
+ }).where(and(eq(juniorReviewItems.id, input.id), eq(juniorReviewItems.status, "open"))).returning({ id: juniorReviewItems.id }))[0]) return false;
7852
+ await replaceLinks(tx, input.id, input);
7853
+ return true;
7854
+ });
7855
+ }
7856
+ /**
7857
+ * Close one open Review item. `closedByUserId` is the person who closed it,
7858
+ * and it is unset when an agent closes it. Return false when the item was
7859
+ * already closed, so only the first close wins.
7860
+ */
7861
+ async function closeReviewItem(db, input) {
7862
+ const now = new Date(input.nowMs);
7863
+ return (await db.update(juniorReviewItems).set({
7864
+ answer: input.answer ?? null,
7865
+ closeReason: input.reason ?? null,
7866
+ closedAt: now,
7867
+ closedByUserId: input.closedByUserId ?? null,
7868
+ status: input.status,
7869
+ updatedAt: now
7870
+ }).where(and(eq(juniorReviewItems.id, input.id), eq(juniorReviewItems.status, "open"))).returning({ id: juniorReviewItems.id })).length > 0;
7871
+ }
7872
+ /**
7873
+ * Dismiss the open Review items that an Automation or a set of
7874
+ * Conversations added, when no agent can act on them any more. No person
7875
+ * closes them. It sends no message and does not change the Slack cards. A
7876
+ * close by Conversations skips the items of an Automation, because a later
7877
+ * run of the Automation can still change them. An Automation delete closes
7878
+ * those.
7879
+ */
7880
+ async function dismissOpenReviewItems(db, input) {
7881
+ const now = new Date(input.nowMs);
7882
+ await db.update(juniorReviewItems).set({
7883
+ closeReason: input.reason,
7884
+ closedAt: now,
7885
+ status: "dismissed",
7886
+ updatedAt: now
7887
+ }).where(and(eq(juniorReviewItems.status, "open"), "automationId" in input ? eq(juniorReviewItems.automationId, input.automationId) : and(inArray(juniorReviewItems.addedByConversationId, [...input.addedByConversationIds]), isNull(juniorReviewItems.automationId))));
7888
+ }
7889
+ /**
7890
+ * Make one User the only assignee of an open Review item. Return false when
7891
+ * the item is closed.
7892
+ */
7893
+ async function takeReviewItem(db, input) {
7894
+ return await db.transaction(async (tx) => {
7895
+ if (!(await tx.update(juniorReviewItems).set({
7896
+ assignedToEveryone: false,
7897
+ updatedAt: new Date(input.nowMs)
7898
+ }).where(and(eq(juniorReviewItems.id, input.id), eq(juniorReviewItems.status, "open"))).returning({ id: juniorReviewItems.id }))[0]) return false;
7899
+ await tx.delete(juniorReviewItemAssignees).where(eq(juniorReviewItemAssignees.itemId, input.id));
7900
+ await tx.insert(juniorReviewItemAssignees).values({
7901
+ itemId: input.id,
7902
+ userId: input.userId
7903
+ });
7904
+ return true;
7905
+ });
7906
+ }
7907
+ function searchConditions(reader, filters) {
7908
+ const conditions = [reviewItemVisibleTo(reader), inArray(juniorReviewItems.status, [...filters.statuses])];
7909
+ if (filters.after) conditions.push(or(lt(juniorReviewItems.updatedAt, filters.after.updatedAt), and(eq(juniorReviewItems.updatedAt, filters.after.updatedAt), lt(juniorReviewItems.id, filters.after.id))));
7910
+ if (filters.subjectId) conditions.push(eq(juniorReviewItems.subjectId, filters.subjectId));
7911
+ if (filters.automationId) conditions.push(eq(juniorReviewItems.automationId, filters.automationId));
7912
+ if (filters.conversationId) conditions.push(reviewItemLinkedTo(filters.conversationId));
7913
+ if (filters.assignee) conditions.push("everyone" in filters.assignee ? eq(juniorReviewItems.assignedToEveryone, true) : reviewItemAssignedTo(filters.assignee.userId));
7914
+ if (filters.query) {
7915
+ const pattern = `%${filters.query.replace(/[\\%_]/g, "\\$&")}%`;
7916
+ conditions.push(or(ilike(juniorReviewItems.title, pattern), ilike(juniorReviewItems.body, pattern)));
7917
+ }
7918
+ return conditions;
7919
+ }
7920
+ /** Count the open Review items that need a User (`reviewItemNeedsUser`). */
7921
+ async function countReviewItemsNeedingUser(db, userId) {
7922
+ return (await db.select({ count: count() }).from(juniorReviewItems).where(reviewItemNeedsUser(userId)))[0]?.count ?? 0;
7923
+ }
7924
+ /**
7925
+ * Find the Review items that a reader can see. Open items come first, so
7926
+ * the limit never drops an open item for a closed one. Then the newest
7927
+ * change comes first: `updated_at`, then `id`, both descending. `after`
7928
+ * pages through that order.
7929
+ */
7930
+ async function searchReviewItems(db, reader, filters) {
7931
+ return await hydrate(db, await db.select().from(juniorReviewItems).where(and(...searchConditions(reader, filters))).orderBy(desc(eq(juniorReviewItems.status, "open")), desc(juniorReviewItems.updatedAt), desc(juniorReviewItems.id)).limit(filters.limit));
7932
+ }
7933
+ /**
7934
+ * List the Conversations that wait on a User: each is linked to an open item
7935
+ * that needs the User (`reviewItemNeedsUser`).
7936
+ */
7937
+ async function listWaitingConversationIds(db, userId) {
7938
+ return (await db.selectDistinct({ conversationId: juniorReviewItemConversations.conversationId }).from(juniorReviewItemConversations).innerJoin(juniorReviewItems, eq(juniorReviewItems.id, juniorReviewItemConversations.itemId)).where(reviewItemNeedsUser(userId))).map((row) => row.conversationId).sort();
7939
+ }
7940
+ // src/chat/review-items/plugin.ts
7941
+ /**
7942
+ * Resolve the Users to assign. An Identity without a User has no one to
7943
+ * assign. A private item goes only to people in its Conversation, the same
7944
+ * rule that `requireReviewItemAssignees` applies to agents.
7945
+ */
7946
+ async function assigneeUserIds(db, identityIds, conversationId, isPrivate) {
7947
+ const userIds = [];
7948
+ for (const identityId of new Set(identityIds)) {
7949
+ const user = (await readIdentityByIdFromSql(db, identityId))?.user;
7950
+ if (!user) continue;
7951
+ if (isPrivate && !(await readConversationAccessFromSql(db, [conversationId], user)).get(conversationId)?.isParticipant) throw new Error("A private review item can be assigned only to people in its Conversation");
7952
+ userIds.push(user.id);
7953
+ }
7954
+ return userIds;
7955
+ }
7956
+ /**
7957
+ * Connect a plugin to the review desk for one Conversation. Dedupe keys are
7958
+ * `plugin:<name>:<key>`, so a plugin changes only its own items, and agents
7959
+ * cannot change them. A change sends no message to the Conversation. It
7960
+ * only shows the new state through `show`. An item is private when the
7961
+ * Conversation is not public.
7962
+ */
7963
+ function createPluginReviewItems(args) {
7964
+ const { conversationId, show } = args;
7965
+ const scopedKey = (dedupeKey) => `${PLUGIN_REVIEW_ITEM_KEY_PREFIX}${args.plugin}:${dedupeKey}`;
7966
+ const readAccess = async (db) => {
7967
+ const access = (await readConversationAccessFromSql(db, [conversationId])).get(conversationId);
7968
+ if (!access) throw new Error(`Conversation ${conversationId} is not found`);
7969
+ return access;
7970
+ };
7971
+ return {
7972
+ async upsert(notice) {
7973
+ const db = getDb();
7974
+ const access = await readAccess(db);
7975
+ const existing = await getOpenReviewItemByDedupeKey(db, scopedKey(notice.dedupeKey), access.tenant);
7976
+ const isPrivate = access.visibility !== "public" || existing?.visibility === "private";
7977
+ if (existing && isPrivate && existing.addedByConversationId !== conversationId) throw new Error("A private review item links only to the Conversation that added it");
7978
+ const content = {
7979
+ assignedToEveryone: false,
7980
+ assigneeUserIds: await assigneeUserIds(db, notice.assigneeIdentityIds, conversationId, isPrivate),
7981
+ body: notice.body,
7982
+ conversationIds: [.../* @__PURE__ */ new Set([conversationId, ...existing?.conversationIds ?? []])],
7983
+ kind: "notice",
7984
+ title: notice.title,
7985
+ visibility: isPrivate ? "private" : "public",
7986
+ ...notice.subject ? { subject: notice.subject } : void 0
7987
+ };
7988
+ if (existing) {
7989
+ await updateReviewItemContent(db, {
7990
+ ...content,
7991
+ id: existing.id,
7992
+ nowMs: Date.now()
7993
+ });
7994
+ await show(existing.id);
7995
+ return;
7996
+ }
7997
+ const item = await insertReviewItem(db, {
7998
+ ...content,
7999
+ addedByConversationId: conversationId,
8000
+ dedupeKey: scopedKey(notice.dedupeKey),
8001
+ nowMs: Date.now(),
8002
+ ...access.tenant ? { tenant: access.tenant } : void 0
8003
+ });
8004
+ if (item) await show(item.id);
8005
+ },
8006
+ async close(input) {
8007
+ const db = getDb();
8008
+ const access = await readAccess(db);
8009
+ const item = await getOpenReviewItemByDedupeKey(db, scopedKey(input.dedupeKey), access.tenant);
8010
+ if (!item) return;
8011
+ if (await closeReviewItem(db, {
8012
+ id: item.id,
8013
+ nowMs: Date.now(),
8014
+ reason: input.reason,
8015
+ status: input.status
8016
+ })) await show(item.id);
8017
+ }
8018
+ };
8019
+ }
7529
8020
  // src/chat/briefs/registration.ts
7530
8021
  var registration_exports = /* @__PURE__ */ __exportAll({
7531
8022
  coreTaskRegistrations: () => coreTaskRegistrations,
@@ -7686,6 +8177,13 @@ function pluginMcpContext(plugin, context) {
7686
8177
  }
7687
8178
  };
7688
8179
  }
8180
+ /** Time one plugin hook call so slow plugins show up in turn traces. */
8181
+ async function withPluginHookSpan(pluginName, hookName, callback) {
8182
+ return await withSpan(`plugin.hook ${hookName}`, "plugin.hook", {}, callback, {
8183
+ "app.plugin.name": pluginName,
8184
+ "app.plugin.hook": hookName
8185
+ });
8186
+ }
7689
8187
  function safeErrorMessage(error) {
7690
8188
  return error instanceof Error ? error.message : String(error);
7691
8189
  }
@@ -7833,7 +8331,7 @@ async function getPluginUserPromptContributions(args) {
7833
8331
  const hook = plugin.hooks?.userPrompt;
7834
8332
  if (!hook) continue;
7835
8333
  try {
7836
- const rawResult = await hook({ ...invocationPluginContext(plugin, args.context, args.turnId) });
8334
+ const rawResult = await withPluginHookSpan(pluginName, "userPrompt", async () => await hook({ ...invocationPluginContext(plugin, args.context, args.turnId) }));
7837
8335
  if (rawResult === void 0) continue;
7838
8336
  if (!Array.isArray(rawResult)) {
7839
8337
  logInvalidPromptContributions({
@@ -8070,6 +8568,11 @@ function getPluginRoutes(options) {
8070
8568
  plugin: pluginName
8071
8569
  }) },
8072
8570
  codeChanges: createCodeChangePublisher(pluginName),
8571
+ reviewItems: { forConversation: (conversationId) => createPluginReviewItems({
8572
+ conversationId,
8573
+ plugin: pluginName,
8574
+ show: options.showReviewItem ?? (async () => {})
8575
+ }) },
8073
8576
  users: { readIdentity: readIdentityById },
8074
8577
  events: {
8075
8578
  async hasMatch(event) {
@@ -8476,11 +8979,11 @@ function createPluginHookRunner(input = {}) {
8476
8979
  const hook = plugin.hooks?.sandboxPrepare;
8477
8980
  if (!hook) continue;
8478
8981
  logInfo("agent.plugin.sandbox_preparation.started", { "app.plugin.name": pluginName });
8479
- await hook({
8982
+ await withPluginHookSpan(pluginName, "sandboxPrepare", async () => await hook({
8480
8983
  ...basePluginContext$1(plugin),
8481
8984
  actor: input.actor,
8482
8985
  sandbox: sandboxCapability
8483
- });
8986
+ }));
8484
8987
  }
8485
8988
  },
8486
8989
  async beforeToolExecute(tool) {
@@ -8493,7 +8996,7 @@ function createPluginHookRunner(input = {}) {
8493
8996
  if (!hook) continue;
8494
8997
  let replacement;
8495
8998
  let blocked;
8496
- await hook({
8999
+ await withPluginHookSpan(pluginName, "beforeToolExecute", async () => await hook({
8497
9000
  ...basePluginContext$1(plugin),
8498
9001
  actor: input.actor,
8499
9002
  actors,
@@ -8522,7 +9025,7 @@ function createPluginHookRunner(input = {}) {
8522
9025
  replacement = input;
8523
9026
  }
8524
9027
  }
8525
- });
9028
+ }));
8526
9029
  if (blocked) throw blocked;
8527
9030
  if (replacement !== void 0) {
8528
9031
  if (!isRecord(replacement)) throw new Error(`Plugin "${pluginName}" replaced tool input with a non-object value`);
@@ -9457,4 +9960,4 @@ async function executeCredentialedEgressRequest(input) {
9457
9960
  });
9458
9961
  }
9459
9962
  }
9460
- export { postSlackMessage as $, createPluginAnnotations as $n, downloadPrivateSlackFile as $t, setWorkspaceSnapshotBuild as A, createPluginState as An, slackMessageTurnDisabledReason as At, parseOAuthStatePayload as B, conversationNotArchivedForEmail as Bn, deliverWatch as Bt, listWorkspaces as C, SANDBOX_JUNIOR_ROOT as Cn, normalizeSandboxEgressTracePropagationDomains as Ct, invalidateReadySnapshot as D, sandboxSkillDir as Dn, resolveChannelCapabilities as Dt, WorkspaceValidationError as E, SANDBOX_WORKSPACE_ROOT as En, workspaceRepoCheckoutPath as Et, getSandboxProvider as F, conversationArchiveTimes as Fn, cancelSubscriptions as Ft, normalizeSlackReplyMarkdown as G, readConversationEventPage as Gn, bindEventAutomationCredentialSubject as Gt, startOAuthFlow as H, viewerConversationMembership as Hn, getWatch as Ht, isVercelSandboxEgressRequest as I, conversationArchivedForEmail as In, cancelWatch as It, truncateStatusText as J, matchSlackAttachments as Jn, verifyEventAutomationCredentialSubject as Jt, normalizeSlackStatusText as K, buildSentryConversationUrl as Kn, bindScheduledAutomationCredentialSubject as Kt, proxyVercelSandboxEgressRequest as L, conversationArchivedForUser as Ln, collectEventMatchKeys as Lt, hash as M, createPluginModel as Mn, RESOURCE_SUBSCRIPTION_MAX_TTL_MS as Mt, isStale as N, createPluginLogger as Nn, RESOURCE_WATCH_TOOL_SOURCE as Nt, loadSnapshotsForProfile as O, sandboxSkillFile as On, getSlackToolContext as Ot, ensureDockerDaemon as P, readConversationAccessFromSql as Pn, STOP_WATCHING_TOOL_NAME as Pt, openSlackModal as Q, saveObjectAnnotations as Qn, SlackActionError as Qt, deliverOAuthAuthorization as R, conversationHasParticipantUser as Rn, createTimerWatch as Rt, getWorkspaceByName as S, SANDBOX_DATA_ROOT as Sn, resolveSandboxEgressProviderForHost as St, updateWorkspace as T, SANDBOX_SKILLS_ROOT as Tn, tryWorkspaceRepoCheckoutPath as Tt, escapeSlackMrkdwnText as U, createPluginConversationEvents as Un, listWatches as Ut, resolveBaseUrl as V, conversationNotArchivedForUser as Vn, findMatchingWatches as Vt, formatSlackLink as W, readLatestConversationBrief as Wn, takeDueTimerWatches as Wt, getSlackMessagePermalink as X, recordSlackAttachment as Xn, verifySlackDirectCredentialSubject as Xt, addReactionToMessage as Y, readAttachmentVisionSummary as Yn, verifyScheduledAutomationCredentialSubject as Yt, openSlackDirectMessage as Z, resolveRootVisibility as Zn, verifyTimerWatchCredentialSubject as Zt, registration_exports as _, PluginToolDeniedError as _n, parseSandboxEgressCredentialToken as _t, agent_hooks_exports as a, normalizeSlackConversationId as an, createPluginCache as ar, isAbortError as at, deleteWorkspace as b, runNonInteractiveCommand as bn, buildSandboxEgressNetworkPolicy as bt, getPluginRoutes as c, sleep$1 as cn, agentsInstructionsUpdatedEvent as cr, wrapSandboxSetupError as ct, getPluginTools as d, JUNIOR_THREAD_STATE_TTL_MS as dn, conversationNoteEvent as dr, isWorkspaceSnapshotNotReadyError as dt, getFilePermalink as en, listConversationAnnotations as er, removeReactionFromMessage as et, getPluginUserPromptContributions as f, getDashboardConversationLink as fn, workspaceSnapshotNotReadyUserMessage as ft, coreTaskRegistrations as g, ToolInputError as gn, createSandboxEgressCredentialToken as gt, validatePlugins as h, setDashboardConversationLinkOptions as hn, consumeSandboxEgressPermissionDeniedSignal as ht, resolvePluginOAuthAccount as i, isDmChannel as in, lookupSlackUser as ir, slackExternalRefIdSchema as it, create as j, createPluginEmbedder as jn, RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS as jt, setWorkspaceSnapshot as k, sandboxSkillPathResolution as kn, isSlackMessageTurn as kt, getPluginSlackConversationLink as l, getWorkspaceTeamId as ln, authenticationLinkedEvent as lr, WorkspaceSnapshotNotReadyError as lt, setPlugins as m, getDashboardTaskLink as mn, consumeSandboxEgressAuthRequiredSignal as mt, createUserTokenStore as n, getSlackClient as nn, defaultTimezone as nr, uploadFilesToConversation as nt, createPluginHookRunner as o, runWithSlackInstallationToken as on, createCache as or, isAlreadyExistsError as ot, getPlugins as p, getDashboardObjectIconLink as pn, clearSandboxEgressSignals as pt, slackMention as q, getSlackFileId as qn, bindSlackDirectCredentialSubject as qt, getOAuthAccountProviders as r, isConversationScopedChannel as rn, resolveUserTimezone as rr, slackEntitySchema as rt, getPluginApiRoutes as s, withSlackRetries as sn, JUNIOR_NATIVE_EVENT_NAMESPACE as sr, throwSandboxOperationError as st, executeCredentialedEgressRequest as t, getHeaderString as tn, listConversationAnnotationsById as tr, updateSlackMessage as tt, getPluginSystemPromptContributions as u, runWithWorkspaceTeamId as un, authenticationUnlinkedEvent as ur, getWorkspaceSnapshotNotReadyError as ut, setBriefsConfig as v, buildCommandScript as vn, parseSandboxEgressAuthRequiredSignal as vt, listWorkspacesByRepository as w, SANDBOX_REPOS_ROOT as wn, SANDBOX_RUNTIME as wt, getWorkspace as x, SANDBOX_COMMAND_PATH as xn, resolveSandboxCommandEnvironment as xt, createWorkspace as y, buildNonInteractiveShellScript as yn, sandboxEgressSignalsResponseSchema as yt, formatProviderLabel as z, conversationIsMemberRoot as zn, createWatch as zt };
9963
+ export { loadSnapshotsForProfile as $, sandboxSkillDir as $n, workspaceRepoCheckoutPath as $t, updateReviewItemContent as A, normalizeSlackConversationId as An, lookupSlackUser as Ar, slackEntitySchema as At, EVERYONE_ASSIGNEE as B, getDashboardTaskLink as Bn, clearSandboxEgressSignals as Bt, getOpenReviewItemByDedupeKey as C, SlackActionError as Cn, resolveRootVisibility as Cr, openSlackDirectMessage as Ct, listWaitingConversationIds as D, getSlackClient as Dn, listConversationAnnotationsById as Dr, removeReactionFromMessage as Dt, listReplyActionItems as E, getHeaderString as En, listConversationAnnotations as Er, postSlackMessage as Et, isOnlyAssignee as F, runWithWorkspaceTeamId as Fn, authenticationUnlinkedEvent as Fr, wrapSandboxSetupError as Ft, deleteWorkspace as G, buildNonInteractiveShellScript as Gn, parseSandboxEgressAuthRequiredSignal as Gt, resolveAssignee as H, ToolInputError as Hn, consumeSandboxEgressPermissionDeniedSignal as Ht, readReviewItemAgentScope as I, JUNIOR_THREAD_STATE_TTL_MS as In, conversationNoteEvent as Ir, WorkspaceSnapshotNotReadyError as It, listWorkspaces as J, SANDBOX_DATA_ROOT as Jn, resolveSandboxCommandEnvironment as Jt, getWorkspace as K, runNonInteractiveCommand as Kn, sandboxEgressSignalsResponseSchema as Kt, requireReviewItemAssignees as L, getDashboardConversationLink as Ln, getWorkspaceSnapshotNotReadyError as Lt, canActOnReviewItem as M, withSlackRetries as Mn, JUNIOR_NATIVE_EVENT_NAMESPACE as Mr, isAbortError as Mt, canAgentChangeReviewItem as N, sleep$1 as Nn, agentsInstructionsUpdatedEvent as Nr, isAlreadyExistsError as Nt, searchReviewItems as O, isConversationScopedChannel as On, defaultTimezone as Or, updateSlackMessage as Ot, canReadReviewItem as P, getWorkspaceTeamId as Pn, authenticationLinkedEvent as Pr, throwSandboxOperationError as Pt, invalidateReadySnapshot as Q, SANDBOX_WORKSPACE_ROOT as Qn, tryWorkspaceRepoCheckoutPath as Qt, requireReviewItemLinks as R, getDashboardObjectIconLink as Rn, isWorkspaceSnapshotNotReadyError as Rt, dismissOpenReviewItems as S, verifyTimerWatchCredentialSubject as Sn, recordSlackAttachment as Sr, getSlackMessagePermalink as St, insertReviewItem as T, getFilePermalink as Tn, createPluginAnnotations as Tr, postSlackEphemeralMessage as Tt, resolveAssignees as U, PluginToolDeniedError as Un, createSandboxEgressCredentialToken as Ut, automationCreatorUserId as V, setDashboardConversationLinkOptions as Vn, consumeSandboxEgressAuthRequiredSignal as Vt, createWorkspace as W, buildCommandScript as Wn, parseSandboxEgressCredentialToken as Wt, updateWorkspace as X, SANDBOX_REPOS_ROOT as Xn, normalizeSandboxEgressTracePropagationDomains as Xt, listWorkspacesByRepository as Y, SANDBOX_JUNIOR_ROOT as Yn, resolveSandboxEgressProviderForHost as Yt, WorkspaceValidationError as Z, SANDBOX_SKILLS_ROOT as Zn, SANDBOX_RUNTIME as Zt, registration_exports as _, bindScheduledAutomationCredentialSubject as _n, readConversationEventPage as _r, normalizeSlackStatusText as _t, agent_hooks_exports as a, STOP_WATCHING_TOOL_NAME as an, createPluginLogger as ar, ensureDockerDaemon as at, closeReviewItem as b, verifyScheduledAutomationCredentialSubject as bn, matchSlackAttachments as br, addReactionToMessage as bt, getPluginRoutes as c, collectEventMatchKeys as cn, conversationArchivedForEmail as cr, proxyVercelSandboxEgressRequest as ct, getPluginTools as d, deliverWatch as dn, conversationIsMemberRoot as dr, parseOAuthStatePayload as dt, resolveChannelCapabilities as en, sandboxSkillFile as er, setWorkspaceSnapshot as et, getPluginUserPromptContributions as f, findMatchingWatches as fn, conversationNotArchivedForEmail as fr, resolveBaseUrl as ft, coreTaskRegistrations as g, bindEventAutomationCredentialSubject as gn, readLatestConversationBrief as gr, normalizeSlackReplyMarkdown as gt, validatePlugins as h, takeDueTimerWatches as hn, createPluginConversationEvents as hr, formatSlackLink as ht, resolvePluginOAuthAccount as i, RESOURCE_WATCH_TOOL_SOURCE as in, createPluginModel as ir, isStale as it, PLUGIN_REVIEW_ITEM_KEY_PREFIX as j, runWithSlackInstallationToken as jn, createPluginCache as jr, slackExternalRefIdSchema as jt, takeReviewItem as k, isDmChannel as kn, resolveUserTimezone as kr, uploadFilesToConversation as kt, getPluginSlackConversationLink as l, createTimerWatch as ln, conversationArchivedForUser as lr, deliverOAuthAuthorization as lt, setPlugins as m, listWatches as mn, viewerConversationMembership as mr, escapeSlackMrkdwnText as mt, createUserTokenStore as n, RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS as nn, createPluginState as nr, create as nt, createPluginHookRunner as o, cancelSubscriptions as on, readConversationAccessFromSql as or, getSandboxProvider as ot, getPlugins as p, getWatch as pn, conversationNotArchivedForUser as pr, startOAuthFlow as pt, getWorkspaceByName as q, SANDBOX_COMMAND_PATH as qn, buildSandboxEgressNetworkPolicy as qt, getOAuthAccountProviders as r, RESOURCE_SUBSCRIPTION_MAX_TTL_MS as rn, createPluginEmbedder as rr, hash as rt, getPluginApiRoutes as s, cancelWatch as sn, conversationArchiveTimes as sr, isVercelSandboxEgressRequest as st, executeCredentialedEgressRequest as t, getSlackToolContext as tn, sandboxSkillPathResolution as tr, setWorkspaceSnapshotBuild as tt, getPluginSystemPromptContributions as u, createWatch as un, conversationHasParticipantUser as ur, formatProviderLabel as ut, setBriefsConfig as v, bindSlackDirectCredentialSubject as vn, buildSentryConversationUrl as vr, slackMention as vt, getReviewItem as w, downloadPrivateSlackFile as wn, saveObjectAnnotations as wr, openSlackModal as wt, countReviewItemsNeedingUser as x, verifySlackDirectCredentialSubject as xn, readAttachmentVisionSummary as xr, deleteSlackMessage as xt, REVIEW_ITEM_REPLIED_REASON as y, verifyEventAutomationCredentialSubject as yn, getSlackFileId as yr, truncateStatusText as yt, reviewItemReaderForUser as z, getDashboardReviewItemLink as zn, workspaceSnapshotNotReadyUserMessage as zt };