@sentry/junior 0.268.0 → 0.270.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.
- package/README.md +2 -9
- package/dist/{adapter-04AcD2SO.js → adapter-BQNTKVTF.js} +3 -49
- package/dist/{admin-DIU1QyG3.js → admin-CKVEoXa4.js} +1 -1
- package/dist/{agent-runner-CAYfFmU7.js → agent-runner-MSQSMFkq.js} +4538 -3251
- package/dist/api/schema.d.ts +2 -3
- package/dist/api/schema.js +7 -7
- package/dist/api.d.ts +3 -1
- package/dist/api.js +190 -301
- package/dist/{app-z6Uuyq3j.d.ts → app-Bz8t4P8p.d.ts} +9 -3
- package/dist/app.d.ts +1 -1
- package/dist/app.js +129 -209
- package/dist/{automation-CmeFBSEh.js → automation-Dwh_xcqM.js} +74 -74
- package/dist/{briefs-DQbV2jNr.js → briefs-DNFzFFVx.js} +5 -5
- package/dist/{catalog-runtime-BD9GSpc6.js → catalog-runtime-Bdym_wKy.js} +1 -1
- package/dist/cli/chat.js +16 -26
- package/dist/cli/check.js +3 -3
- package/dist/cli/main.js +5 -3
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/snapshot-create.js +4 -4
- package/dist/cli/upgrade.d.ts +1 -1
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-C1yDYjoS.js → client-B2MjENki.js} +31 -16
- package/dist/{config-DrTeF80T.js → config-Dm3pWRqY.js} +3 -3
- package/dist/{connections-D46f8vvi.js → connections-0BBhiD-p.js} +10 -10
- package/dist/{conversation-DSo310Ps.js → conversation-AhOVzN_k.js} +11 -86
- package/dist/{conversation-privacy-CxlOCiUo.js → conversation-privacy-C1pJ89rs.js} +1 -1
- package/dist/{credential-sync-sm9fegLY.js → credential-sync-BRiWYs6_.js} +15 -14
- package/dist/{credentialed-lukuD-cc.js → credentialed-BeBaJMZ_.js} +565 -83
- package/dist/{store-xMuuXRE0.d.ts → db-BYOuu6v2.d.ts} +4 -0
- package/dist/{executor-4vhvsMZJ.js → executor-BB6eJkD4.js} +6 -6
- package/dist/instrumentation.js +2 -2
- package/dist/{interruption-marker-CkncDT8d.js → interruption-marker-CdblrIQK.js} +1 -1
- package/dist/{job-queue-In9QKz-I.js → job-queue-DiaXoSDg.js} +2 -2
- package/dist/{list-BkgnXEEB.js → list-BTDRxkOg.js} +331 -662
- package/dist/locks-D-0h111u.js +48 -0
- package/dist/{logging-DLSpkSSr.js → logging-C6EdT6PP.js} +1 -1
- package/dist/{history-C84mlAwO.js → media-U6Tuz6Mi.js} +84 -2
- package/dist/{message-BObyg59s.js → message-DsjiWRVg.js} +4 -4
- package/dist/{model-profile-Bg8EoCKD.js → model-profile-B998sjVd.js} +9 -2
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +5 -3
- package/dist/{oauth-callback-server-B-WbSdoH.js → oauth-callback-server-CjFFVGqa.js} +2 -2
- package/dist/{prompt-BW9jFsqZ.js → prompt-BPSdXYXV.js} +1 -1
- package/dist/{registry-BgecKva0.js → registry-CndmkP2P.js} +1 -1
- package/dist/reporting-schema-CZREhYc-.js +104 -0
- package/dist/{web-message-id-DlHNANtd.js → reporting-window-DRCi1Ls_.js} +139 -7
- package/dist/{resolve-Cpa4Bu1O.js → resolve-BkAA1wH7.js} +3 -3
- package/dist/{result-D3TSmZZ8.js → result-9TJTpuYP.js} +30 -11
- package/dist/{runner-DS5OKhEp.js → runner-BmKLSyJm.js} +12 -11
- package/dist/{sandbox-egress-signals-CnDjY5rx.js → sandbox-egress-signals-CjLaogMo.js} +1 -1
- package/dist/{schema-0050HFnB.js → schema-DNLJ_MzW.js} +36 -20
- package/dist/{schema-ZngqcOWM.d.ts → schema-WayPSHFu.d.ts} +149 -12
- package/dist/{service-account-CKV2WTY7.js → service-account-D6e9cVmH.js} +4 -3
- package/dist/{service-account-DGjnDw9y.js → service-account-DlhYl-Mu.js} +3 -3
- package/dist/{session-stop-Dam1EI7B.js → session-stop-MZRgYqYH.js} +24 -16
- package/dist/{skills-CfAnfImD.js → skills-BggTR_ue.js} +2 -2
- package/dist/{spawn-cmq9p4Ly.js → spawn-DmtlcAT_.js} +2 -2
- package/dist/{store-zPEoJPBB.js → store-BeWGgaMA.js} +3 -3
- package/dist/{store-C5PN02QO.js → store-Byarin7b.js} +4 -4
- package/dist/{task-queue-_mf28dSu.js → task-queue-Dt1_ZoI_.js} +3 -3
- package/dist/task-runner-DyKqAPpm.js +269 -0
- package/dist/{thread-state-BbbPh0Sn.js → thread-state-BHBLMvgb.js} +5 -5
- package/dist/{turn-failure-response-BKKmnMB5.js → turn-failure-response-DWXy_0oJ.js} +9 -9
- package/dist/{turn-lifecycle-oP0kVeGz.js → turn-lifecycle-AiXkVqCB.js} +1 -1
- package/dist/{tasks-SB9z-VTU.js → turn-session-routing-B9KLBhaT.js} +939 -120
- package/dist/{validation--JsAGLwc.js → validation-BW60Vtx_.js} +1 -1
- package/dist/{version-jHwJp4hC.js → version-CivPYmkH.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-CpXTGlep.js → viewer-BTgAfdUg.js} +216 -27
- package/dist/{work-3N-GHY9m.js → work-Bjm52fHo.js} +17 -36
- package/dist/{worker-CCdbcItd.js → worker-CMbgtHLu.js} +15 -7
- package/migrations/0069_system_identity.sql +25 -0
- package/migrations/0070_review_item_foreign_keys.sql +5 -0
- package/migrations/AGENTS.md +17 -0
- package/migrations/meta/0069_snapshot.json +3979 -0
- package/migrations/meta/0070_snapshot.json +3997 -0
- package/migrations/meta/_journal.json +15 -1
- package/package.json +5 -4
- package/dist/db-BU4LQD8s.d.ts +0 -6
- package/dist/history-BgI33Kie.d.ts +0 -78
- package/dist/reporting-window-DVCD_10C.js +0 -227
- package/dist/task-runner-DMI2prDO.js +0 -1073
- package/migrations/README.md +0 -30
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-nwUcYXjP.js";
|
|
2
|
-
import {
|
|
3
|
-
import { i as getChatConfig, n as botConfig, o as getSlackBotToken, v as normalizeSlackEmojiName } from "./config-
|
|
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-
|
|
2
|
+
import { S as toOptionalTrimmed, i as completeObject, o as embedTexts, t as GEN_AI_PROVIDER_NAME } from "./client-B2MjENki.js";
|
|
3
|
+
import { i as getChatConfig, n as botConfig, o as getSlackBotToken, v as normalizeSlackEmojiName } from "./config-Dm3pWRqY.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-C6EdT6PP.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-
|
|
7
|
-
import {
|
|
6
|
+
import { t as canExposeConversationPayload } from "./conversation-privacy-C1pJ89rs.js";
|
|
7
|
+
import { i as defaultModelId } from "./model-profile-B998sjVd.js";
|
|
8
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 {
|
|
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-BTgAfdUg.js";
|
|
10
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, g as juniorConversationBriefs, i as juniorWorkspaces, m as juniorConversationParticipants, n as juniorSnapshots, r as juniorWorkspaceRepos, w as juniorAttachments, x as juniorConversationAnnotations, y as juniorConversationEvents } from "./executor-
|
|
12
|
-
import {
|
|
13
|
-
import { _ as CredentialUnavailableError, a as resolveAuthTokenPlaceholder, h as credentialSubjectSchema, m as credentialLogContext, o as resolvePluginCommandEnv, p as credentialContextSchema } from "./registry-
|
|
14
|
-
import { n as pluginCatalogRuntime } from "./catalog-runtime-
|
|
15
|
-
import {
|
|
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-BB6eJkD4.js";
|
|
12
|
+
import { l as decodeStoredConversationEvent } from "./media-U6Tuz6Mi.js";
|
|
13
|
+
import { _ as CredentialUnavailableError, a as resolveAuthTokenPlaceholder, h as credentialSubjectSchema, m as credentialLogContext, o as resolvePluginCommandEnv, p as credentialContextSchema } from "./registry-CndmkP2P.js";
|
|
14
|
+
import { n as pluginCatalogRuntime } from "./catalog-runtime-Bdym_wKy.js";
|
|
15
|
+
import { r as acquireActiveLock } from "./locks-D-0h111u.js";
|
|
16
|
+
import { n as getDefaultRedisStateAdapterFor, r as getStateAdapter } from "./adapter-BQNTKVTF.js";
|
|
16
17
|
import { i as isValidTimeZone } from "./cadence-R7Em4RjE.js";
|
|
17
|
-
import { C as conversationReportEventSchema,
|
|
18
|
-
import { n as briefEntriesFromReportEvents, s as generateBrief, t as BRIEF_PROMPT } from "./prompt-
|
|
18
|
+
import { C as conversationReportEventSchema, J as conversationBriefSchema, W as readMessageAttachments, Y as parseBriefInput, w as conversationReportMessageEventSchema, z as readMessageCardRefs } from "./conversation-AhOVzN_k.js";
|
|
19
|
+
import { n as briefEntriesFromReportEvents, s as generateBrief, t as BRIEF_PROMPT } from "./prompt-BPSdXYXV.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
|
|
263
|
-
fullName
|
|
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);
|
|
@@ -1784,17 +1758,22 @@ function createPluginModel(pluginName, options = {}, runtime = {}) {
|
|
|
1784
1758
|
}
|
|
1785
1759
|
/** Create the host-owned embedding capability exposed to prompt hooks. */
|
|
1786
1760
|
function createPluginEmbedder(pluginName, runtime = {}) {
|
|
1787
|
-
return {
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1761
|
+
return {
|
|
1762
|
+
model: botConfig.embeddingModelId,
|
|
1763
|
+
provider: GEN_AI_PROVIDER_NAME,
|
|
1764
|
+
async embedTexts(input) {
|
|
1765
|
+
return await embedTexts({
|
|
1766
|
+
modelId: botConfig.embeddingModelId,
|
|
1767
|
+
texts: input.texts,
|
|
1768
|
+
...input.inputType !== void 0 ? { inputType: input.inputType } : void 0,
|
|
1769
|
+
signal: runtime.signal,
|
|
1770
|
+
metadata: {
|
|
1771
|
+
pluginName,
|
|
1772
|
+
pluginModelRole: "embedding"
|
|
1773
|
+
}
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1798
1777
|
}
|
|
1799
1778
|
// src/chat/plugins/state.ts
|
|
1800
1779
|
const MAX_PLUGIN_STATE_KEY_LENGTH = 512;
|
|
@@ -3085,19 +3064,6 @@ function getSlackToolContext(context) {
|
|
|
3085
3064
|
threadTs: parseSlackMessageTs(location.threadTs)
|
|
3086
3065
|
};
|
|
3087
3066
|
}
|
|
3088
|
-
/**
|
|
3089
|
-
* Return whether a person started this Turn with a Slack message.
|
|
3090
|
-
*
|
|
3091
|
-
* Only these Turns act with that person's Slack authority. Event, automation,
|
|
3092
|
-
* Agent invocation, and dashboard Turns in the same Conversation do not.
|
|
3093
|
-
*/
|
|
3094
|
-
function isSlackMessageTurn(context) {
|
|
3095
|
-
return context.source.kind === "slack" && context.destination.platform === "slack" && context.actor?.platform === "slack" && context.actor.teamId === context.source.teamId;
|
|
3096
|
-
}
|
|
3097
|
-
/** Disable reason for tools that need a Slack message Turn. */
|
|
3098
|
-
function slackMessageTurnDisabledReason(context) {
|
|
3099
|
-
return isSlackMessageTurn(context) ? void 0 : "It needs a Turn that a person started with a Slack message.";
|
|
3100
|
-
}
|
|
3101
3067
|
// src/chat/slack/tool-support/channel-capabilities.ts
|
|
3102
3068
|
/** Resolve channel capabilities from a Slack channel ID. */
|
|
3103
3069
|
function resolveChannelCapabilities(channelId) {
|
|
@@ -5565,6 +5531,24 @@ async function deleteWorkspaceSnapshotBuilders$1(names) {
|
|
|
5565
5531
|
}
|
|
5566
5532
|
if (failures.length > 0) throw failures[0];
|
|
5567
5533
|
}
|
|
5534
|
+
const DOCKER_SNAPSHOT_PACKAGES = /* @__PURE__ */ new Set([
|
|
5535
|
+
"spal-release",
|
|
5536
|
+
"ripgrep",
|
|
5537
|
+
"gh",
|
|
5538
|
+
"jq"
|
|
5539
|
+
]);
|
|
5540
|
+
/**
|
|
5541
|
+
* Keep only the small Docker base from the declared runtime dependencies.
|
|
5542
|
+
*
|
|
5543
|
+
* Postinstall commands are skipped, because they often download large tools.
|
|
5544
|
+
* A base package that no plugin declares is not installed.
|
|
5545
|
+
*/
|
|
5546
|
+
function selectSnapshotRuntime(runtime) {
|
|
5547
|
+
return {
|
|
5548
|
+
dependencies: runtime.dependencies.filter((dependency) => "package" in dependency && DOCKER_SNAPSHOT_PACKAGES.has(dependency.package)),
|
|
5549
|
+
postinstall: []
|
|
5550
|
+
};
|
|
5551
|
+
}
|
|
5568
5552
|
/** Local Docker implementation of the sandbox provider contract. */
|
|
5569
5553
|
const dockerSandboxProvider = {
|
|
5570
5554
|
errors: dockerSandboxErrors,
|
|
@@ -5572,7 +5556,8 @@ const dockerSandboxProvider = {
|
|
|
5572
5556
|
getSession,
|
|
5573
5557
|
createWorkspaceSnapshotBuilder: createWorkspaceSnapshotBuilder$1,
|
|
5574
5558
|
getWorkspaceSnapshotBuilder: getWorkspaceSnapshotBuilder$1,
|
|
5575
|
-
deleteWorkspaceSnapshotBuilders: deleteWorkspaceSnapshotBuilders$1
|
|
5559
|
+
deleteWorkspaceSnapshotBuilders: deleteWorkspaceSnapshotBuilders$1,
|
|
5560
|
+
selectSnapshotRuntime
|
|
5576
5561
|
};
|
|
5577
5562
|
// src/chat/sandbox/vercel/credentials.ts
|
|
5578
5563
|
/**
|
|
@@ -7092,9 +7077,12 @@ function workspaceRecipe(workspace) {
|
|
|
7092
7077
|
}
|
|
7093
7078
|
/** Build the complete profile that selects one reusable sandbox snapshot. */
|
|
7094
7079
|
function create(runtime, workspace) {
|
|
7095
|
-
const
|
|
7096
|
-
|
|
7097
|
-
|
|
7080
|
+
const declared = {
|
|
7081
|
+
dependencies: mergeDependencies([...GLOBAL_RUNTIME_DEPENDENCIES, ...pluginCatalogRuntime.getRuntimeDependencies()]),
|
|
7082
|
+
postinstall: [...GLOBAL_RUNTIME_POSTINSTALL, ...pluginCatalogRuntime.getRuntimePostinstall()]
|
|
7083
|
+
};
|
|
7084
|
+
const { dependencies, postinstall } = getSandboxProvider().selectSnapshotRuntime?.(declared) ?? declared;
|
|
7085
|
+
const pluginPostinstall = postinstall.filter((command) => !GLOBAL_RUNTIME_POSTINSTALL.includes(command));
|
|
7098
7086
|
if (dependencies.length === 0 && postinstall.length === 0 && !workspace) return null;
|
|
7099
7087
|
const rebuildEpoch = process.env.SANDBOX_SNAPSHOT_REBUILD_EPOCH?.trim() ?? "";
|
|
7100
7088
|
const floating = dependencies.some((dependency) => isFloating(dependency)) || pluginPostinstall.length > 0 || Boolean(workspace);
|
|
@@ -7552,6 +7540,488 @@ function createCodeChangePublisher(provider) {
|
|
|
7552
7540
|
}
|
|
7553
7541
|
};
|
|
7554
7542
|
}
|
|
7543
|
+
// src/chat/review-items/assignees.ts
|
|
7544
|
+
/** Text that the agent writes to assign Everyone. */
|
|
7545
|
+
const EVERYONE_ASSIGNEE = "everyone";
|
|
7546
|
+
/**
|
|
7547
|
+
* Resolve one assignee: Everyone, an email, or a mention. A mention must be
|
|
7548
|
+
* in the tenant of the item.
|
|
7549
|
+
*/
|
|
7550
|
+
async function resolveAssignee(db, value, tenant, findMentionedUserId) {
|
|
7551
|
+
const text = value.trim();
|
|
7552
|
+
if (text.toLowerCase() === "everyone") return { everyone: true };
|
|
7553
|
+
const mentionedUserId = await findMentionedUserId(text, tenant);
|
|
7554
|
+
if (mentionedUserId) return { userId: mentionedUserId };
|
|
7555
|
+
if (text.includes("@")) {
|
|
7556
|
+
const user = await findUserByEmailFromSql(db, text);
|
|
7557
|
+
if (!user) throw new ToolInputError(`No Junior user has the email ${text}.`);
|
|
7558
|
+
return { userId: user.id };
|
|
7559
|
+
}
|
|
7560
|
+
throw new ToolInputError(`Unknown assignee ${JSON.stringify(text)}. Use ${EVERYONE_ASSIGNEE}, an email, or a mention such as <@U123>.`);
|
|
7561
|
+
}
|
|
7562
|
+
/** Resolve the assignees of a Review item. One is required. */
|
|
7563
|
+
async function resolveAssignees(db, values, tenant, findMentionedUserId) {
|
|
7564
|
+
if (values.length === 0) throw new ToolInputError("A review item needs at least one assignee.");
|
|
7565
|
+
const resolved = await Promise.all(values.map((value) => resolveAssignee(db, value, tenant, findMentionedUserId)));
|
|
7566
|
+
return {
|
|
7567
|
+
everyone: resolved.some((assignee) => "everyone" in assignee),
|
|
7568
|
+
userIds: [...new Set(resolved.flatMap((assignee) => "userId" in assignee ? [assignee.userId] : []))]
|
|
7569
|
+
};
|
|
7570
|
+
}
|
|
7571
|
+
/** Return which of the Users take part in one root Conversation. */
|
|
7572
|
+
async function conversationParticipantUserIds(db, rootConversationId, userIds) {
|
|
7573
|
+
if (userIds.length === 0) return /* @__PURE__ */ new Set();
|
|
7574
|
+
const rows = await db.select({ userId: juniorConversationParticipants.userId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.rootConversationId, rootConversationId), inArray(juniorConversationParticipants.userId, [...userIds])));
|
|
7575
|
+
return new Set(rows.map((row) => row.userId));
|
|
7576
|
+
}
|
|
7577
|
+
/** Return the User who created an Automation, if Junior knows them. */
|
|
7578
|
+
async function automationCreatorUserId(db, automationId) {
|
|
7579
|
+
const [row] = await db.select({ userId: juniorIdentities.userId }).from(juniorAutomations).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorAutomations.creatorIdentityId)).where(eq(juniorAutomations.id, automationId)).limit(1);
|
|
7580
|
+
return row?.userId ?? void 0;
|
|
7581
|
+
}
|
|
7582
|
+
// src/chat/review-items/access.ts
|
|
7583
|
+
/** Return whether two tenants are the same. Two missing tenants match. */
|
|
7584
|
+
function isSameTenant(a, b) {
|
|
7585
|
+
return a?.provider === b?.provider && a?.providerTenantId === b?.providerTenantId;
|
|
7586
|
+
}
|
|
7587
|
+
/**
|
|
7588
|
+
* The reader for one person: items assigned to them, and public items in the
|
|
7589
|
+
* provider tenants of their identities.
|
|
7590
|
+
*/
|
|
7591
|
+
function reviewItemReaderForUser(user) {
|
|
7592
|
+
return {
|
|
7593
|
+
tenants: user.identities.flatMap((identity) => identity.providerTenantId ? [{
|
|
7594
|
+
provider: identity.provider,
|
|
7595
|
+
providerTenantId: identity.providerTenantId
|
|
7596
|
+
}] : []),
|
|
7597
|
+
userId: user.id
|
|
7598
|
+
};
|
|
7599
|
+
}
|
|
7600
|
+
/** SQL: the Review item has this assigned User. */
|
|
7601
|
+
function reviewItemAssignedTo(userId) {
|
|
7602
|
+
return sql`exists (select 1 from ${juniorReviewItemAssignees} where ${juniorReviewItemAssignees.itemId} = ${juniorReviewItems.id} and ${juniorReviewItemAssignees.userId} = ${userId})`;
|
|
7603
|
+
}
|
|
7604
|
+
/**
|
|
7605
|
+
* SQL: the Review item needs this User. Only an open `action` item that is
|
|
7606
|
+
* assigned to the User needs them. A notice, an item for Everyone, and an
|
|
7607
|
+
* item without assignees need no one. An assigned User can always see the
|
|
7608
|
+
* item, so this rule needs no visibility check. The nav count and the
|
|
7609
|
+
* "Waiting on you" marker use this rule.
|
|
7610
|
+
*/
|
|
7611
|
+
function reviewItemNeedsUser(userId) {
|
|
7612
|
+
return and(eq(juniorReviewItems.status, "open"), eq(juniorReviewItems.kind, "action"), reviewItemAssignedTo(userId));
|
|
7613
|
+
}
|
|
7614
|
+
/** SQL: the Review item links this Conversation. */
|
|
7615
|
+
function reviewItemLinkedTo(conversationId) {
|
|
7616
|
+
return sql`exists (select 1 from ${juniorReviewItemConversations} where ${juniorReviewItemConversations.itemId} = ${juniorReviewItems.id} and ${juniorReviewItemConversations.conversationId} = ${conversationId})`;
|
|
7617
|
+
}
|
|
7618
|
+
function publicIn(tenants) {
|
|
7619
|
+
return tenants.map((tenant) => and(eq(juniorReviewItems.visibility, "public"), eq(juniorReviewItems.provider, tenant.provider), eq(juniorReviewItems.providerTenantId, tenant.providerTenantId)));
|
|
7620
|
+
}
|
|
7621
|
+
/**
|
|
7622
|
+
* SQL form of `canReadReviewItem`, for search. Keep the two the same; the
|
|
7623
|
+
* Review item access test checks that they agree.
|
|
7624
|
+
*/
|
|
7625
|
+
function reviewItemVisibleTo(reader) {
|
|
7626
|
+
return or(sql`false`, ...publicIn(reader.tenants ?? []), ...reader.userId ? [reviewItemAssignedTo(reader.userId)] : [], ...reader.conversationId ? [reviewItemLinkedTo(reader.conversationId)] : [], ...reader.automationId ? [eq(juniorReviewItems.automationId, reader.automationId)] : []);
|
|
7627
|
+
}
|
|
7628
|
+
/** Return whether a reader can see one Review item. */
|
|
7629
|
+
function canReadReviewItem(item, reader) {
|
|
7630
|
+
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;
|
|
7631
|
+
}
|
|
7632
|
+
/**
|
|
7633
|
+
* Read the scope of an agent. The agent reads as its Conversation and
|
|
7634
|
+
* Automation, not as its Actor: an assignment lets a person see the item on
|
|
7635
|
+
* the review desk, not let an agent read it into another Conversation.
|
|
7636
|
+
*/
|
|
7637
|
+
async function readReviewItemAgentScope(context) {
|
|
7638
|
+
const user = (await context.resolveActorIdentity?.())?.user;
|
|
7639
|
+
const automationId = context.source.kind === "scheduled_automation" || context.source.kind === "event_automation" ? context.source.automationId : void 0;
|
|
7640
|
+
const current = (await readConversationAccessFromSql(getDb(), [context.conversationId], user)).get(context.conversationId);
|
|
7641
|
+
const tenant = current?.tenant;
|
|
7642
|
+
return {
|
|
7643
|
+
inPrivateConversation: current?.visibility !== "public",
|
|
7644
|
+
reader: {
|
|
7645
|
+
conversationId: context.conversationId,
|
|
7646
|
+
...automationId ? { automationId } : void 0,
|
|
7647
|
+
...tenant ? { tenants: [tenant] } : void 0
|
|
7648
|
+
},
|
|
7649
|
+
...tenant ? { tenant } : void 0,
|
|
7650
|
+
...user ? { user } : void 0
|
|
7651
|
+
};
|
|
7652
|
+
}
|
|
7653
|
+
/**
|
|
7654
|
+
* Dedupe keys with this prefix belong to plugins. Only the plugin that added
|
|
7655
|
+
* the item, or a person, can change it. An agent cannot use the prefix.
|
|
7656
|
+
*/
|
|
7657
|
+
const PLUGIN_REVIEW_ITEM_KEY_PREFIX = "plugin:";
|
|
7658
|
+
/**
|
|
7659
|
+
* Whether an agent can change an item. Only an agent in the tenant of the
|
|
7660
|
+
* item can change it. A public item changes only from a public
|
|
7661
|
+
* Conversation, so private content never reaches its readers. A private
|
|
7662
|
+
* item changes only from the Conversation or Automation that added it. An
|
|
7663
|
+
* agent never changes an item that a plugin added.
|
|
7664
|
+
*/
|
|
7665
|
+
function canAgentChangeReviewItem(item, scope) {
|
|
7666
|
+
if (item.dedupeKey.startsWith("plugin:")) return false;
|
|
7667
|
+
if (!isSameTenant(item.tenant, scope.tenant)) return false;
|
|
7668
|
+
if (item.visibility === "public") return !scope.inPrivateConversation;
|
|
7669
|
+
return item.addedByConversationId === scope.reader.conversationId || scope.reader.automationId !== void 0 && item.automationId === scope.reader.automationId;
|
|
7670
|
+
}
|
|
7671
|
+
/**
|
|
7672
|
+
* Check the Conversations that an agent links. Every link must be in the
|
|
7673
|
+
* tenant of the item. `allowedIds` (the current Conversation and the links
|
|
7674
|
+
* that the item already has) always pass. A private item gets no other
|
|
7675
|
+
* links. A public item can get other links only to public Conversations.
|
|
7676
|
+
*/
|
|
7677
|
+
async function requireReviewItemLinks(args) {
|
|
7678
|
+
const access = await readConversationAccessFromSql(getDb(), args.conversationIds, args.scope.user);
|
|
7679
|
+
for (const conversationId of args.conversationIds) {
|
|
7680
|
+
const conversation = access.get(conversationId);
|
|
7681
|
+
if (!conversation || !isSameTenant(conversation.tenant, args.tenant)) throw new ToolInputError(`Conversation ${conversationId} is not found.`);
|
|
7682
|
+
if (args.allowedIds.has(conversationId)) continue;
|
|
7683
|
+
if (args.visibility === "private") throw new ToolInputError("A private item links only to the Conversation that added it.");
|
|
7684
|
+
if (conversation.visibility !== "public") throw new ToolInputError(conversation.canViewPrivateContent ? `Conversation ${conversationId} is private. Only public Conversations can be linked.` : `Conversation ${conversationId} is not found.`);
|
|
7685
|
+
}
|
|
7686
|
+
}
|
|
7687
|
+
/**
|
|
7688
|
+
* Check who an agent assigns a private item to. It cannot go to Everyone.
|
|
7689
|
+
* Each User must be the current Actor, a person in one of the given
|
|
7690
|
+
* Conversations (the current one and, for an existing item, the one that
|
|
7691
|
+
* added it), or the creator of the Automation that added the item. An
|
|
7692
|
+
* Automation run often has no Actor and no people yet, and its creator wrote
|
|
7693
|
+
* the work. A public item can go to anyone.
|
|
7694
|
+
*/
|
|
7695
|
+
async function requireReviewItemAssignees(args) {
|
|
7696
|
+
if (args.visibility === "public") return;
|
|
7697
|
+
if (args.assignees.everyone) throw new ToolInputError("A private item cannot be assigned to Everyone. Assign Users instead.");
|
|
7698
|
+
const creatorId = args.automationId ? await automationCreatorUserId(getDb(), args.automationId) : void 0;
|
|
7699
|
+
const others = args.assignees.userIds.filter((id) => id !== args.scope.user?.id && id !== creatorId);
|
|
7700
|
+
const inConversation = /* @__PURE__ */ new Set();
|
|
7701
|
+
for (const conversationId of new Set(args.conversationIds)) for (const id of await conversationParticipantUserIds(getDb(), conversationId, others)) inConversation.add(id);
|
|
7702
|
+
if (others.some((id) => !inConversation.has(id))) throw new ToolInputError("A private item can be assigned only to people in its Conversation.");
|
|
7703
|
+
}
|
|
7704
|
+
/**
|
|
7705
|
+
* Return whether a User can act on an open Review item: an assigned User,
|
|
7706
|
+
* anyone for an item assigned to Everyone, and anyone for an item with no
|
|
7707
|
+
* assignees. The caller checks that the User can see the item.
|
|
7708
|
+
*/
|
|
7709
|
+
function canActOnReviewItem(item, userId) {
|
|
7710
|
+
if (item.status !== "open") return false;
|
|
7711
|
+
if (item.assignedToEveryone) return true;
|
|
7712
|
+
if (item.assignees.length === 0) return true;
|
|
7713
|
+
return item.assignees.some((assignee) => assignee.userId === userId);
|
|
7714
|
+
}
|
|
7715
|
+
/** Return whether a User is already the only assignee, so take does nothing. */
|
|
7716
|
+
function isOnlyAssignee(item, userId) {
|
|
7717
|
+
return !item.assignedToEveryone && item.assignees.length === 1 && item.assignees[0]?.userId === userId;
|
|
7718
|
+
}
|
|
7719
|
+
// src/chat/review-items/store.ts
|
|
7720
|
+
/**
|
|
7721
|
+
* Close reason of an action item that a person answered with a reply in
|
|
7722
|
+
* its thread instead of on the card or the review desk.
|
|
7723
|
+
*/
|
|
7724
|
+
const REVIEW_ITEM_REPLIED_REASON = "Replied in thread";
|
|
7725
|
+
async function readUsers(db, userIds) {
|
|
7726
|
+
if (userIds.length === 0) return /* @__PURE__ */ new Map();
|
|
7727
|
+
const rows = await db.select({
|
|
7728
|
+
displayName: juniorUsers.displayName,
|
|
7729
|
+
email: juniorUsers.primaryEmail,
|
|
7730
|
+
userId: juniorUsers.id
|
|
7731
|
+
}).from(juniorUsers).where(inArray(juniorUsers.id, [...new Set(userIds)]));
|
|
7732
|
+
return new Map(rows.map((row) => [row.userId, {
|
|
7733
|
+
email: row.email,
|
|
7734
|
+
userId: row.userId,
|
|
7735
|
+
...row.displayName ? { displayName: row.displayName } : void 0
|
|
7736
|
+
}]));
|
|
7737
|
+
}
|
|
7738
|
+
async function hydrate(db, rows) {
|
|
7739
|
+
if (rows.length === 0) return [];
|
|
7740
|
+
const ids = rows.map((row) => row.id);
|
|
7741
|
+
const [assigneeRows, conversationRows] = await Promise.all([db.select().from(juniorReviewItemAssignees).where(inArray(juniorReviewItemAssignees.itemId, ids)), db.select().from(juniorReviewItemConversations).where(inArray(juniorReviewItemConversations.itemId, ids))]);
|
|
7742
|
+
const users = await readUsers(db, [...assigneeRows.map((row) => row.userId), ...rows.flatMap((row) => row.closedByUserId ? [row.closedByUserId] : [])]);
|
|
7743
|
+
return rows.map((row) => {
|
|
7744
|
+
const closedBy = row.closedByUserId ? users.get(row.closedByUserId) : void 0;
|
|
7745
|
+
return {
|
|
7746
|
+
addedByConversationId: row.addedByConversationId,
|
|
7747
|
+
assignedToEveryone: row.assignedToEveryone,
|
|
7748
|
+
assignees: assigneeRows.filter((assignee) => assignee.itemId === row.id).flatMap((assignee) => users.get(assignee.userId) ?? []).sort((a, b) => a.email.localeCompare(b.email)),
|
|
7749
|
+
body: row.body,
|
|
7750
|
+
conversationIds: conversationRows.filter((link) => link.itemId === row.id).map((link) => link.conversationId).sort(),
|
|
7751
|
+
createdAt: row.createdAt,
|
|
7752
|
+
dedupeKey: row.dedupeKey,
|
|
7753
|
+
id: row.id,
|
|
7754
|
+
kind: row.kind,
|
|
7755
|
+
status: row.status,
|
|
7756
|
+
title: row.title,
|
|
7757
|
+
updatedAt: row.updatedAt,
|
|
7758
|
+
visibility: row.visibility,
|
|
7759
|
+
...row.answer ? { answer: row.answer } : void 0,
|
|
7760
|
+
...row.automationId ? { automationId: row.automationId } : void 0,
|
|
7761
|
+
...row.closeReason ? { closeReason: row.closeReason } : void 0,
|
|
7762
|
+
...row.closedAt ? { closedAt: row.closedAt } : void 0,
|
|
7763
|
+
...closedBy ? { closedBy } : void 0,
|
|
7764
|
+
...row.request ? { request: row.request } : void 0,
|
|
7765
|
+
...row.provider && row.providerTenantId ? { tenant: {
|
|
7766
|
+
provider: row.provider,
|
|
7767
|
+
providerTenantId: row.providerTenantId
|
|
7768
|
+
} } : void 0,
|
|
7769
|
+
...row.subjectId ? { subject: {
|
|
7770
|
+
id: row.subjectId,
|
|
7771
|
+
...row.subjectTitle ? { title: row.subjectTitle } : void 0,
|
|
7772
|
+
...row.subjectUrl ? { url: row.subjectUrl } : void 0
|
|
7773
|
+
} } : void 0
|
|
7774
|
+
};
|
|
7775
|
+
});
|
|
7776
|
+
}
|
|
7777
|
+
function contentColumns(content) {
|
|
7778
|
+
return {
|
|
7779
|
+
assignedToEveryone: content.assignedToEveryone,
|
|
7780
|
+
body: content.body,
|
|
7781
|
+
kind: content.kind,
|
|
7782
|
+
request: content.request ?? null,
|
|
7783
|
+
subjectId: content.subject?.id ?? null,
|
|
7784
|
+
subjectTitle: content.subject?.title ?? null,
|
|
7785
|
+
subjectUrl: content.subject?.url ?? null,
|
|
7786
|
+
title: content.title,
|
|
7787
|
+
visibility: content.visibility
|
|
7788
|
+
};
|
|
7789
|
+
}
|
|
7790
|
+
async function replaceLinks(db, itemId, content) {
|
|
7791
|
+
await db.delete(juniorReviewItemAssignees).where(eq(juniorReviewItemAssignees.itemId, itemId));
|
|
7792
|
+
await db.delete(juniorReviewItemConversations).where(eq(juniorReviewItemConversations.itemId, itemId));
|
|
7793
|
+
const userIds = [...new Set(content.assigneeUserIds)];
|
|
7794
|
+
if (userIds.length > 0) await db.insert(juniorReviewItemAssignees).values(userIds.map((userId) => ({
|
|
7795
|
+
itemId,
|
|
7796
|
+
userId
|
|
7797
|
+
})));
|
|
7798
|
+
const conversationIds = [...new Set(content.conversationIds)];
|
|
7799
|
+
if (conversationIds.length > 0) await db.insert(juniorReviewItemConversations).values(conversationIds.map((conversationId) => ({
|
|
7800
|
+
itemId,
|
|
7801
|
+
conversationId
|
|
7802
|
+
})));
|
|
7803
|
+
}
|
|
7804
|
+
/** Read one Review item by id. */
|
|
7805
|
+
async function getReviewItem(db, id) {
|
|
7806
|
+
return (await hydrate(db, await db.select().from(juniorReviewItems).where(eq(juniorReviewItems.id, id))))[0];
|
|
7807
|
+
}
|
|
7808
|
+
/**
|
|
7809
|
+
* Read the action items that the agent added in one Conversation and that a
|
|
7810
|
+
* reply can answer: the open items, and the items that a reply closed.
|
|
7811
|
+
*/
|
|
7812
|
+
async function listReplyActionItems(db, conversationId) {
|
|
7813
|
+
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)))));
|
|
7814
|
+
}
|
|
7815
|
+
/**
|
|
7816
|
+
* Read the open Review item with one dedupe key in one provider tenant, if
|
|
7817
|
+
* there is one. Dedupe keys are unique only inside a tenant.
|
|
7818
|
+
*/
|
|
7819
|
+
async function getOpenReviewItemByDedupeKey(db, dedupeKey, tenant) {
|
|
7820
|
+
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];
|
|
7821
|
+
}
|
|
7822
|
+
/**
|
|
7823
|
+
* Add one open Review item. Return undefined when another open item in the
|
|
7824
|
+
* same tenant already uses its dedupe key.
|
|
7825
|
+
*/
|
|
7826
|
+
async function insertReviewItem(db, input) {
|
|
7827
|
+
const now = new Date(input.nowMs);
|
|
7828
|
+
const id = await db.transaction(async (tx) => {
|
|
7829
|
+
const inserted = await tx.insert(juniorReviewItems).values({
|
|
7830
|
+
...contentColumns(input),
|
|
7831
|
+
addedByConversationId: input.addedByConversationId,
|
|
7832
|
+
automationId: input.automationId ?? null,
|
|
7833
|
+
createdAt: now,
|
|
7834
|
+
dedupeKey: input.dedupeKey,
|
|
7835
|
+
id: randomUUID(),
|
|
7836
|
+
provider: input.tenant?.provider ?? null,
|
|
7837
|
+
providerTenantId: input.tenant?.providerTenantId ?? null,
|
|
7838
|
+
status: "open",
|
|
7839
|
+
updatedAt: now
|
|
7840
|
+
}).onConflictDoNothing().returning({ id: juniorReviewItems.id });
|
|
7841
|
+
if (!inserted[0]) return void 0;
|
|
7842
|
+
await replaceLinks(tx, inserted[0].id, input);
|
|
7843
|
+
return inserted[0].id;
|
|
7844
|
+
});
|
|
7845
|
+
if (!id) return void 0;
|
|
7846
|
+
const item = await getReviewItem(db, id);
|
|
7847
|
+
if (!item) throw new Error(`Review item ${id} is missing after insert`);
|
|
7848
|
+
return item;
|
|
7849
|
+
}
|
|
7850
|
+
/** Replace the content of one open Review item. Return false when closed. */
|
|
7851
|
+
async function updateReviewItemContent(db, input) {
|
|
7852
|
+
return await db.transaction(async (tx) => {
|
|
7853
|
+
if (!(await tx.update(juniorReviewItems).set({
|
|
7854
|
+
...contentColumns(input),
|
|
7855
|
+
updatedAt: new Date(input.nowMs)
|
|
7856
|
+
}).where(and(eq(juniorReviewItems.id, input.id), eq(juniorReviewItems.status, "open"))).returning({ id: juniorReviewItems.id }))[0]) return false;
|
|
7857
|
+
await replaceLinks(tx, input.id, input);
|
|
7858
|
+
return true;
|
|
7859
|
+
});
|
|
7860
|
+
}
|
|
7861
|
+
/**
|
|
7862
|
+
* Close one open Review item. `closedByUserId` is the person who closed it,
|
|
7863
|
+
* and it is unset when an agent closes it. Return false when the item was
|
|
7864
|
+
* already closed, so only the first close wins.
|
|
7865
|
+
*/
|
|
7866
|
+
async function closeReviewItem(db, input) {
|
|
7867
|
+
const now = new Date(input.nowMs);
|
|
7868
|
+
return (await db.update(juniorReviewItems).set({
|
|
7869
|
+
answer: input.answer ?? null,
|
|
7870
|
+
closeReason: input.reason ?? null,
|
|
7871
|
+
closedAt: now,
|
|
7872
|
+
closedByUserId: input.closedByUserId ?? null,
|
|
7873
|
+
status: input.status,
|
|
7874
|
+
updatedAt: now
|
|
7875
|
+
}).where(and(eq(juniorReviewItems.id, input.id), eq(juniorReviewItems.status, "open"))).returning({ id: juniorReviewItems.id })).length > 0;
|
|
7876
|
+
}
|
|
7877
|
+
/**
|
|
7878
|
+
* Dismiss the open Review items that an Automation or a set of
|
|
7879
|
+
* Conversations added, when no agent can act on them any more. No person
|
|
7880
|
+
* closes them. It sends no message and does not change the Slack cards. A
|
|
7881
|
+
* close by Conversations skips the items of an Automation, because a later
|
|
7882
|
+
* run of the Automation can still change them. An Automation delete closes
|
|
7883
|
+
* those.
|
|
7884
|
+
*/
|
|
7885
|
+
async function dismissOpenReviewItems(db, input) {
|
|
7886
|
+
const now = new Date(input.nowMs);
|
|
7887
|
+
await db.update(juniorReviewItems).set({
|
|
7888
|
+
closeReason: input.reason,
|
|
7889
|
+
closedAt: now,
|
|
7890
|
+
status: "dismissed",
|
|
7891
|
+
updatedAt: now
|
|
7892
|
+
}).where(and(eq(juniorReviewItems.status, "open"), "automationId" in input ? eq(juniorReviewItems.automationId, input.automationId) : and(inArray(juniorReviewItems.addedByConversationId, [...input.addedByConversationIds]), isNull(juniorReviewItems.automationId))));
|
|
7893
|
+
}
|
|
7894
|
+
/**
|
|
7895
|
+
* Make one User the only assignee of an open Review item. Return false when
|
|
7896
|
+
* the item is closed.
|
|
7897
|
+
*/
|
|
7898
|
+
async function takeReviewItem(db, input) {
|
|
7899
|
+
return await db.transaction(async (tx) => {
|
|
7900
|
+
if (!(await tx.update(juniorReviewItems).set({
|
|
7901
|
+
assignedToEveryone: false,
|
|
7902
|
+
updatedAt: new Date(input.nowMs)
|
|
7903
|
+
}).where(and(eq(juniorReviewItems.id, input.id), eq(juniorReviewItems.status, "open"))).returning({ id: juniorReviewItems.id }))[0]) return false;
|
|
7904
|
+
await tx.delete(juniorReviewItemAssignees).where(eq(juniorReviewItemAssignees.itemId, input.id));
|
|
7905
|
+
await tx.insert(juniorReviewItemAssignees).values({
|
|
7906
|
+
itemId: input.id,
|
|
7907
|
+
userId: input.userId
|
|
7908
|
+
});
|
|
7909
|
+
return true;
|
|
7910
|
+
});
|
|
7911
|
+
}
|
|
7912
|
+
function searchConditions(reader, filters) {
|
|
7913
|
+
const conditions = [reviewItemVisibleTo(reader), inArray(juniorReviewItems.status, [...filters.statuses])];
|
|
7914
|
+
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))));
|
|
7915
|
+
if (filters.subjectId) conditions.push(eq(juniorReviewItems.subjectId, filters.subjectId));
|
|
7916
|
+
if (filters.automationId) conditions.push(eq(juniorReviewItems.automationId, filters.automationId));
|
|
7917
|
+
if (filters.conversationId) conditions.push(reviewItemLinkedTo(filters.conversationId));
|
|
7918
|
+
if (filters.assignee) conditions.push("everyone" in filters.assignee ? eq(juniorReviewItems.assignedToEveryone, true) : reviewItemAssignedTo(filters.assignee.userId));
|
|
7919
|
+
if (filters.query) {
|
|
7920
|
+
const pattern = `%${filters.query.replace(/[\\%_]/g, "\\$&")}%`;
|
|
7921
|
+
conditions.push(or(ilike(juniorReviewItems.title, pattern), ilike(juniorReviewItems.body, pattern)));
|
|
7922
|
+
}
|
|
7923
|
+
return conditions;
|
|
7924
|
+
}
|
|
7925
|
+
/** Count the open Review items that need a User (`reviewItemNeedsUser`). */
|
|
7926
|
+
async function countReviewItemsNeedingUser(db, userId) {
|
|
7927
|
+
return (await db.select({ count: count() }).from(juniorReviewItems).where(reviewItemNeedsUser(userId)))[0]?.count ?? 0;
|
|
7928
|
+
}
|
|
7929
|
+
/**
|
|
7930
|
+
* Find the Review items that a reader can see. Open items come first, so
|
|
7931
|
+
* the limit never drops an open item for a closed one. Then the newest
|
|
7932
|
+
* change comes first: `updated_at`, then `id`, both descending. `after`
|
|
7933
|
+
* pages through that order.
|
|
7934
|
+
*/
|
|
7935
|
+
async function searchReviewItems(db, reader, filters) {
|
|
7936
|
+
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));
|
|
7937
|
+
}
|
|
7938
|
+
/**
|
|
7939
|
+
* List the Conversations that wait on a User: each is linked to an open item
|
|
7940
|
+
* that needs the User (`reviewItemNeedsUser`).
|
|
7941
|
+
*/
|
|
7942
|
+
async function listWaitingConversationIds(db, userId) {
|
|
7943
|
+
return (await db.selectDistinct({ conversationId: juniorReviewItemConversations.conversationId }).from(juniorReviewItemConversations).innerJoin(juniorReviewItems, eq(juniorReviewItems.id, juniorReviewItemConversations.itemId)).where(reviewItemNeedsUser(userId))).map((row) => row.conversationId).sort();
|
|
7944
|
+
}
|
|
7945
|
+
// src/chat/review-items/plugin.ts
|
|
7946
|
+
/**
|
|
7947
|
+
* Resolve the Users to assign. An Identity without a User has no one to
|
|
7948
|
+
* assign. A private item goes only to people in its Conversation, the same
|
|
7949
|
+
* rule that `requireReviewItemAssignees` applies to agents.
|
|
7950
|
+
*/
|
|
7951
|
+
async function assigneeUserIds(db, identityIds, conversationId, isPrivate) {
|
|
7952
|
+
const userIds = [];
|
|
7953
|
+
for (const identityId of new Set(identityIds)) {
|
|
7954
|
+
const user = (await readIdentityByIdFromSql(db, identityId))?.user;
|
|
7955
|
+
if (!user) continue;
|
|
7956
|
+
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");
|
|
7957
|
+
userIds.push(user.id);
|
|
7958
|
+
}
|
|
7959
|
+
return userIds;
|
|
7960
|
+
}
|
|
7961
|
+
/**
|
|
7962
|
+
* Connect a plugin to the review desk for one Conversation. Dedupe keys are
|
|
7963
|
+
* `plugin:<name>:<key>`, so a plugin changes only its own items, and agents
|
|
7964
|
+
* cannot change them. A change sends no message to the Conversation. It
|
|
7965
|
+
* only shows the new state through `show`. An item is private when the
|
|
7966
|
+
* Conversation is not public.
|
|
7967
|
+
*/
|
|
7968
|
+
function createPluginReviewItems(args) {
|
|
7969
|
+
const { conversationId, show } = args;
|
|
7970
|
+
const scopedKey = (dedupeKey) => `${PLUGIN_REVIEW_ITEM_KEY_PREFIX}${args.plugin}:${dedupeKey}`;
|
|
7971
|
+
const readAccess = async (db) => {
|
|
7972
|
+
const access = (await readConversationAccessFromSql(db, [conversationId])).get(conversationId);
|
|
7973
|
+
if (!access) throw new Error(`Conversation ${conversationId} is not found`);
|
|
7974
|
+
return access;
|
|
7975
|
+
};
|
|
7976
|
+
return {
|
|
7977
|
+
async upsert(notice) {
|
|
7978
|
+
const db = getDb();
|
|
7979
|
+
const access = await readAccess(db);
|
|
7980
|
+
const existing = await getOpenReviewItemByDedupeKey(db, scopedKey(notice.dedupeKey), access.tenant);
|
|
7981
|
+
const isPrivate = access.visibility !== "public" || existing?.visibility === "private";
|
|
7982
|
+
if (existing && isPrivate && existing.addedByConversationId !== conversationId) throw new Error("A private review item links only to the Conversation that added it");
|
|
7983
|
+
const content = {
|
|
7984
|
+
assignedToEveryone: false,
|
|
7985
|
+
assigneeUserIds: await assigneeUserIds(db, notice.assigneeIdentityIds, conversationId, isPrivate),
|
|
7986
|
+
body: notice.body,
|
|
7987
|
+
conversationIds: [.../* @__PURE__ */ new Set([conversationId, ...existing?.conversationIds ?? []])],
|
|
7988
|
+
kind: "notice",
|
|
7989
|
+
title: notice.title,
|
|
7990
|
+
visibility: isPrivate ? "private" : "public",
|
|
7991
|
+
...notice.subject ? { subject: notice.subject } : void 0
|
|
7992
|
+
};
|
|
7993
|
+
if (existing) {
|
|
7994
|
+
await updateReviewItemContent(db, {
|
|
7995
|
+
...content,
|
|
7996
|
+
id: existing.id,
|
|
7997
|
+
nowMs: Date.now()
|
|
7998
|
+
});
|
|
7999
|
+
await show(existing.id);
|
|
8000
|
+
return;
|
|
8001
|
+
}
|
|
8002
|
+
const item = await insertReviewItem(db, {
|
|
8003
|
+
...content,
|
|
8004
|
+
addedByConversationId: conversationId,
|
|
8005
|
+
dedupeKey: scopedKey(notice.dedupeKey),
|
|
8006
|
+
nowMs: Date.now(),
|
|
8007
|
+
...access.tenant ? { tenant: access.tenant } : void 0
|
|
8008
|
+
});
|
|
8009
|
+
if (item) await show(item.id);
|
|
8010
|
+
},
|
|
8011
|
+
async close(input) {
|
|
8012
|
+
const db = getDb();
|
|
8013
|
+
const access = await readAccess(db);
|
|
8014
|
+
const item = await getOpenReviewItemByDedupeKey(db, scopedKey(input.dedupeKey), access.tenant);
|
|
8015
|
+
if (!item) return;
|
|
8016
|
+
if (await closeReviewItem(db, {
|
|
8017
|
+
id: item.id,
|
|
8018
|
+
nowMs: Date.now(),
|
|
8019
|
+
reason: input.reason,
|
|
8020
|
+
status: input.status
|
|
8021
|
+
})) await show(item.id);
|
|
8022
|
+
}
|
|
8023
|
+
};
|
|
8024
|
+
}
|
|
7555
8025
|
// src/chat/briefs/registration.ts
|
|
7556
8026
|
var registration_exports = /* @__PURE__ */ __exportAll({
|
|
7557
8027
|
coreTaskRegistrations: () => coreTaskRegistrations,
|
|
@@ -7712,6 +8182,13 @@ function pluginMcpContext(plugin, context) {
|
|
|
7712
8182
|
}
|
|
7713
8183
|
};
|
|
7714
8184
|
}
|
|
8185
|
+
/** Time one plugin hook call so slow plugins show up in turn traces. */
|
|
8186
|
+
async function withPluginHookSpan(pluginName, hookName, callback) {
|
|
8187
|
+
return await withSpan(`plugin.hook ${hookName}`, "plugin.hook", {}, callback, {
|
|
8188
|
+
"app.plugin.name": pluginName,
|
|
8189
|
+
"app.plugin.hook": hookName
|
|
8190
|
+
});
|
|
8191
|
+
}
|
|
7715
8192
|
function safeErrorMessage(error) {
|
|
7716
8193
|
return error instanceof Error ? error.message : String(error);
|
|
7717
8194
|
}
|
|
@@ -7859,7 +8336,7 @@ async function getPluginUserPromptContributions(args) {
|
|
|
7859
8336
|
const hook = plugin.hooks?.userPrompt;
|
|
7860
8337
|
if (!hook) continue;
|
|
7861
8338
|
try {
|
|
7862
|
-
const rawResult = await hook({ ...invocationPluginContext(plugin, args.context, args.turnId) });
|
|
8339
|
+
const rawResult = await withPluginHookSpan(pluginName, "userPrompt", async () => await hook({ ...invocationPluginContext(plugin, args.context, args.turnId) }));
|
|
7863
8340
|
if (rawResult === void 0) continue;
|
|
7864
8341
|
if (!Array.isArray(rawResult)) {
|
|
7865
8342
|
logInvalidPromptContributions({
|
|
@@ -8096,6 +8573,11 @@ function getPluginRoutes(options) {
|
|
|
8096
8573
|
plugin: pluginName
|
|
8097
8574
|
}) },
|
|
8098
8575
|
codeChanges: createCodeChangePublisher(pluginName),
|
|
8576
|
+
reviewItems: { forConversation: (conversationId) => createPluginReviewItems({
|
|
8577
|
+
conversationId,
|
|
8578
|
+
plugin: pluginName,
|
|
8579
|
+
show: options.showReviewItem ?? (async () => {})
|
|
8580
|
+
}) },
|
|
8099
8581
|
users: { readIdentity: readIdentityById },
|
|
8100
8582
|
events: {
|
|
8101
8583
|
async hasMatch(event) {
|
|
@@ -8502,11 +8984,11 @@ function createPluginHookRunner(input = {}) {
|
|
|
8502
8984
|
const hook = plugin.hooks?.sandboxPrepare;
|
|
8503
8985
|
if (!hook) continue;
|
|
8504
8986
|
logInfo("agent.plugin.sandbox_preparation.started", { "app.plugin.name": pluginName });
|
|
8505
|
-
await hook({
|
|
8987
|
+
await withPluginHookSpan(pluginName, "sandboxPrepare", async () => await hook({
|
|
8506
8988
|
...basePluginContext$1(plugin),
|
|
8507
8989
|
actor: input.actor,
|
|
8508
8990
|
sandbox: sandboxCapability
|
|
8509
|
-
});
|
|
8991
|
+
}));
|
|
8510
8992
|
}
|
|
8511
8993
|
},
|
|
8512
8994
|
async beforeToolExecute(tool) {
|
|
@@ -8519,7 +9001,7 @@ function createPluginHookRunner(input = {}) {
|
|
|
8519
9001
|
if (!hook) continue;
|
|
8520
9002
|
let replacement;
|
|
8521
9003
|
let blocked;
|
|
8522
|
-
await hook({
|
|
9004
|
+
await withPluginHookSpan(pluginName, "beforeToolExecute", async () => await hook({
|
|
8523
9005
|
...basePluginContext$1(plugin),
|
|
8524
9006
|
actor: input.actor,
|
|
8525
9007
|
actors,
|
|
@@ -8548,7 +9030,7 @@ function createPluginHookRunner(input = {}) {
|
|
|
8548
9030
|
replacement = input;
|
|
8549
9031
|
}
|
|
8550
9032
|
}
|
|
8551
|
-
});
|
|
9033
|
+
}));
|
|
8552
9034
|
if (blocked) throw blocked;
|
|
8553
9035
|
if (replacement !== void 0) {
|
|
8554
9036
|
if (!isRecord(replacement)) throw new Error(`Plugin "${pluginName}" replaced tool input with a non-object value`);
|
|
@@ -9483,4 +9965,4 @@ async function executeCredentialedEgressRequest(input) {
|
|
|
9483
9965
|
});
|
|
9484
9966
|
}
|
|
9485
9967
|
}
|
|
9486
|
-
export {
|
|
9968
|
+
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 };
|