@sentry/junior 0.259.0 → 0.261.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/dist/{adapter-Bn0gFVSF.js → adapter-Bx_1B2De.js} +1 -1
- package/dist/{admin-C8xz2f10.js → admin-BAbbVgVc.js} +1 -1
- package/dist/{agent-hooks-B00FaKdP.js → agent-hooks-ghyZZQqB.js} +721 -704
- package/dist/{agent-runner-CPSd7rKm.js → agent-runner-g110-xgg.js} +688 -2223
- package/dist/api/schema.d.ts +210 -54
- package/dist/api/schema.js +3 -3
- package/dist/api.js +161 -73
- package/dist/{app-Bbxrehjf.d.ts → app-VCiPQ65Q.d.ts} +6 -1
- package/dist/app.d.ts +1 -1
- package/dist/app.js +36 -35
- package/dist/{assistant-reply-CqthTerd.js → assistant-reply-CQcMaWNL.js} +1 -1
- package/dist/{automation-DWBFBenG.js → automation-DrpcA-SX.js} +3 -0
- package/dist/{briefs-DNYbQ9cD.js → briefs-CKcfrIQL.js} +4 -4
- package/dist/{catalog-runtime-B8SuFPGb.js → catalog-runtime-cd60vzAB.js} +1 -1
- package/dist/cli/chat.js +16 -16
- package/dist/cli/check.js +3 -3
- package/dist/cli/init.js +1 -1
- package/dist/cli/main.js +3 -3
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/snapshot-create.js +4 -4
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-BZdxgKyL.js → client-2i0JJpoC.js} +2 -2
- package/dist/{config-Deqpfpg9.js → config-C9qM41O0.js} +2 -2
- package/dist/{connections-9gJkfS85.js → connections-DrtTrwkp.js} +10 -10
- package/dist/{conversation-DjN3mlog.js → conversation-CvuKJ3tA.js} +108 -155
- package/dist/{conversation-privacy-CFhNKxp-.js → conversation-privacy-1iTiFX1O.js} +1 -1
- package/dist/{credential-sync-BABGymK3.js → credential-sync-wKNQ1dYc.js} +14 -15
- package/dist/{executor-Bb3eQb8q.js → executor-Rimyj8HZ.js} +3 -2
- package/dist/{images-BbSdVHC6.js → images-D1JggcVY.js} +3 -3
- package/dist/instrumentation.js +2 -2
- package/dist/{job-queue-vdGU-5ge.js → job-queue-BjQdSqA3.js} +1 -1
- package/dist/{logging-BStXN56b.js → logging-BLTa7R7f.js} +1 -1
- package/dist/{message-DRf6m4uO.js → message-DAeP5WWW.js} +3 -3
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +3 -3
- package/dist/{oauth-callback-server-JmOgLcGl.js → oauth-callback-server-BAy1hhF1.js} +2 -2
- package/dist/{profile-qBM6B_-V.js → profile-CthM2qFM.js} +24 -24
- package/dist/{prompt-xgpyMtg7.js → prompt-DeVax2qO.js} +1 -1
- package/dist/{registry-B_M_x_iS.js → registry-BnSaArNb.js} +1 -1
- package/dist/{resolve-BzpuLh8c.js → resolve-B5eLRl0v.js} +9 -5
- package/dist/{result-DEZH_AnZ.js → result-Cvm0GZCv.js} +9 -14
- package/dist/{runner-BqQlG10t.js → runner-D-pUzHbU.js} +11 -11
- package/dist/{sandbox-egress-signals-DCq3WLs5.js → sandbox-egress-signals-mvwGV7rl.js} +1 -1
- package/dist/{schema-CF9HqNRY.js → schema-OzL8Zw4H.js} +68 -3
- package/dist/{service-account-BzhW7kXu.js → service-account-Ch1uIpnR.js} +3 -3
- package/dist/{service-account-BqTBkyD7.js → service-account-DWxvpSfd.js} +3 -3
- package/dist/{session-stop-CV65ZlU3.js → session-stop-Boj_zpTa.js} +180 -22
- package/dist/{skills-CiWS5_Ub.js → skills-BpdYCfFt.js} +2 -2
- package/dist/{spawn-BSAQ7UQZ.js → spawn-h1DJMQTf.js} +2 -2
- package/dist/{store-B1GES4gC.js → store-B6KqcdCQ.js} +6 -6
- package/dist/{task-queue-BAeGEMjJ.js → task-queue-BaDwKSYv.js} +3 -3
- package/dist/{task-runner-BlGhcQPi.js → task-runner-BAX5li9a.js} +10 -10
- package/dist/{tasks-DieKX3d4.js → tasks-uCfmlWga.js} +4170 -1606
- package/dist/thread-state-BJHVIz8G.js +184 -0
- package/dist/{turn-failure-response-DQljogGJ.js → turn-failure-response-CCC76P6-.js} +8 -9
- package/dist/{turn-lifecycle-BLgrkV_L.js → turn-lifecycle-pHSESVR9.js} +1 -1
- package/dist/{turn-session-routing-BlajheOU.js → turn-session-routing-BIKJZaiw.js} +1 -1
- package/dist/{validation-23d7K9tX.js → validation-B9KL_5Wc.js} +1 -1
- package/dist/{version-B_XumRx-.js → version-CN9BXMW_.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-B2_ABsYi.js → viewer-C5xFaYVi.js} +19 -10
- package/dist/{web-input-BZDyxpzR.js → web-input-D6eqfXYd.js} +38 -34
- package/dist/{work-D3QKiV0O.js → work-C-ecZAme.js} +9 -10
- package/dist/{worker-C8mUjYf9.js → worker-BpYDqiOf.js} +5 -5
- package/migrations/0061_workspace_vcpus.sql +1 -0
- package/migrations/meta/0061_snapshot.json +3471 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +10 -10
- package/dist/experimental-BNK1l1jv.js +0 -49
- package/dist/oauth-authorization-djQn5Nhy.js +0 -97
- package/dist/pending-auth-rG5U_QS1.js +0 -442
- package/dist/thread-state-DpSWS4Rr.js +0 -426
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-nwUcYXjP.js";
|
|
2
|
-
import { i as completeObject, o as embedTexts } from "./client-
|
|
3
|
-
import { T as withSpan, a as extractGenAiUsageSummary, b as setSpanStatus, f as logException, m as logWarn, p as logInfo, y as setSpanAttributes, z as isRecord$1 } from "./logging-
|
|
2
|
+
import { i as completeObject, o as embedTexts } from "./client-2i0JJpoC.js";
|
|
3
|
+
import { T as withSpan, a as extractGenAiUsageSummary, b as setSpanStatus, f as logException, m as logWarn, p as logInfo, y as setSpanAttributes, z as isRecord$1 } from "./logging-BLTa7R7f.js";
|
|
4
4
|
import { a as getClient$1 } from "./sentry-BBUlHih9.js";
|
|
5
|
-
import { t as canExposeConversationPayload } from "./conversation-privacy-
|
|
6
|
-
import { i as getChatConfig, n as botConfig, o as getSlackBotToken } from "./config-
|
|
5
|
+
import { t as canExposeConversationPayload } from "./conversation-privacy-1iTiFX1O.js";
|
|
6
|
+
import { i as getChatConfig, n as botConfig, o as getSlackBotToken } from "./config-C9qM41O0.js";
|
|
7
7
|
import { r as defaultModelId } from "./model-profile-Bg8EoCKD.js";
|
|
8
8
|
import { d as parseSlackTeamId, g as parseSlackMessageTs, i as isActorUserId, o as parseActorUserId, u as parseSlackChannelReferenceId } from "./actor-tlxzqaRe.js";
|
|
9
|
-
import { D as hasAgentTurnUsage, M as readUserTimezone, P as saveUserTimezone, c as getConversationEventStore, d as getDb, f as getSqlExecutor, g as createSqlStore, i as resolveViewerUser, r as readActorIdentity } from "./viewer-
|
|
9
|
+
import { D as hasAgentTurnUsage, M as readUserTimezone, P as saveUserTimezone, c as getConversationEventStore, d as getDb, f as getSqlExecutor, g as createSqlStore, i as resolveViewerUser, r as readActorIdentity } from "./viewer-C5xFaYVi.js";
|
|
10
10
|
import { c as juniorIdentities, d as juniorDestinations, l as juniorUsers, s as juniorConversations } from "./agent-invocations-Qfz7dWgU.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-
|
|
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-Rimyj8HZ.js";
|
|
12
12
|
import { o as decodeStoredConversationEvent } from "./history-AY8rs-FR.js";
|
|
13
|
-
import { h as credentialSubjectSchema } from "./registry-
|
|
14
|
-
import { C as conversationReportEventSchema, F as readMessageAttachments, H as conversationBriefSchema, U as parseBriefInput, q as readMessageCardRefs } from "./conversation-DjN3mlog.js";
|
|
15
|
-
import { E as SANDBOX_WORKSPACE_ROOT, S as sleep$1, l as isSandboxMissingError, n as hash, v as getVercelSandboxCredentials, x as runNonInteractiveCommand } from "./profile-qBM6B_-V.js";
|
|
16
|
-
import { n as getDefaultRedisStateAdapterFor, r as getStateAdapter } from "./adapter-Bn0gFVSF.js";
|
|
13
|
+
import { h as credentialSubjectSchema } from "./registry-BnSaArNb.js";
|
|
17
14
|
import { i as isValidTimeZone } from "./cadence-R7Em4RjE.js";
|
|
18
|
-
import {
|
|
15
|
+
import { B as readMessageAttachments, C as conversationReportEventSchema, F as readMessageCardRefs, J as parseBriefInput, q as conversationBriefSchema, w as conversationReportMessageEventSchema } from "./conversation-CvuKJ3tA.js";
|
|
16
|
+
import { n as briefEntriesFromReportEvents, s as generateBrief, t as BRIEF_PROMPT } from "./prompt-DeVax2qO.js";
|
|
17
|
+
import { n as getDefaultRedisStateAdapterFor, r as getStateAdapter } from "./adapter-Bx_1B2De.js";
|
|
18
|
+
import { E as SANDBOX_WORKSPACE_ROOT, S as runNonInteractiveCommand, l as isSandboxMissingError, n as hash, v as getVercelSandboxCredentials, y as sleep$1 } from "./profile-CthM2qFM.js";
|
|
19
19
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
22
22
|
import { and, arrayContains, asc, desc, eq, exists, getTableColumns, gte, inArray, isNotNull, isNull, lt, lte, max, ne, notExists, or, sql } from "drizzle-orm";
|
|
23
23
|
import { codeChangeInputSchema, conversationAnnotationInputSchema, conversationEventPresentationSchema, defineConversationEvent, eventInputSchema, eventMatchSchema, eventMatches, eventTypeSchema, missingToolAnnotationKeys, normalizeEventIdentifier, objectAnnotationSchema, objectIconPath, objectTypeSchema, pluginEventsSchema, promptContextSchema, promptMessageSchema, stableEventMatchKey } from "@sentry/junior-plugin-api";
|
|
24
|
-
import { alias } from "drizzle-orm/pg-core";
|
|
24
|
+
import { alias, union } from "drizzle-orm/pg-core";
|
|
25
25
|
import { WebClient } from "@slack/web-api";
|
|
26
26
|
import { Sandbox } from "@vercel/sandbox";
|
|
27
27
|
// src/chat/conversations/structured-events.ts
|
|
@@ -157,731 +157,382 @@ function renderJuniorNativeConversationEvent(args) {
|
|
|
157
157
|
const presentation = definition.renderEvent(definition.parse(args.content));
|
|
158
158
|
return presentation === void 0 ? void 0 : conversationEventPresentationSchema.parse(presentation);
|
|
159
159
|
}
|
|
160
|
-
// src/chat/slack/
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
160
|
+
// src/chat/slack/user.ts
|
|
161
|
+
const slackUserInfoSchema = z.object({
|
|
162
|
+
ok: z.literal(true),
|
|
163
|
+
user: z.object({
|
|
164
|
+
name: z.string().nullish(),
|
|
165
|
+
tz: z.string().nullish(),
|
|
166
|
+
profile: z.object({
|
|
167
|
+
email: z.string().nullish(),
|
|
168
|
+
real_name: z.string().nullish()
|
|
169
|
+
}).optional()
|
|
170
|
+
})
|
|
171
|
+
});
|
|
172
|
+
const USER_CACHE_TTL_MS = 3e5;
|
|
173
|
+
const userCache = /* @__PURE__ */ new Map();
|
|
174
|
+
function userCacheKey(teamId, userId) {
|
|
175
|
+
return `${teamId}:${userId}`;
|
|
170
176
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
statusCode;
|
|
178
|
-
requestId;
|
|
179
|
-
errorData;
|
|
180
|
-
retryAfterSeconds;
|
|
181
|
-
detail;
|
|
182
|
-
detailLine;
|
|
183
|
-
detailRule;
|
|
184
|
-
constructor(message, code, options = {}) {
|
|
185
|
-
super(message);
|
|
186
|
-
this.name = "SlackActionError";
|
|
187
|
-
this.code = code;
|
|
188
|
-
this.apiError = options.apiError;
|
|
189
|
-
this.needed = options.needed;
|
|
190
|
-
this.provided = options.provided;
|
|
191
|
-
this.statusCode = options.statusCode;
|
|
192
|
-
this.requestId = options.requestId;
|
|
193
|
-
this.errorData = options.errorData;
|
|
194
|
-
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
195
|
-
this.detail = options.detail;
|
|
196
|
-
this.detailLine = options.detailLine;
|
|
197
|
-
this.detailRule = options.detailRule;
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
function serializeSlackErrorData(data) {
|
|
201
|
-
if (!data || typeof data !== "object") return;
|
|
202
|
-
const filtered = Object.fromEntries(Object.entries(data).filter(([key]) => key !== "error"));
|
|
203
|
-
if (Object.keys(filtered).length === 0) return;
|
|
204
|
-
try {
|
|
205
|
-
const serialized = JSON.stringify(filtered);
|
|
206
|
-
return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
|
|
207
|
-
} catch {
|
|
208
|
-
return;
|
|
177
|
+
function readFromCache(teamId, userId) {
|
|
178
|
+
const hit = userCache.get(userCacheKey(teamId, userId));
|
|
179
|
+
if (!hit) return null;
|
|
180
|
+
if (hit.expiresAt < Date.now()) {
|
|
181
|
+
userCache.delete(userCacheKey(teamId, userId));
|
|
182
|
+
return null;
|
|
209
183
|
}
|
|
184
|
+
return hit.value;
|
|
210
185
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
186
|
+
function writeToCache(teamId, userId, value) {
|
|
187
|
+
userCache.set(userCacheKey(teamId, userId), {
|
|
188
|
+
value,
|
|
189
|
+
expiresAt: Date.now() + USER_CACHE_TTL_MS
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/** Fetch Slack user profile info with in-memory TTL cache to avoid repeated API calls. */
|
|
193
|
+
async function lookupSlackUser(teamId, userId) {
|
|
194
|
+
if (!teamId || !userId) return null;
|
|
195
|
+
const cached = readFromCache(teamId, userId);
|
|
196
|
+
if (cached) return cached;
|
|
197
|
+
const token = getSlackBotToken();
|
|
198
|
+
if (!token) return null;
|
|
199
|
+
try {
|
|
200
|
+
const response = await fetch(`https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`, { headers: { authorization: `Bearer ${token}` } });
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
logWarn("slack.user.lookup.failed", {
|
|
203
|
+
"app.slack.user_id": userId,
|
|
204
|
+
"app.slack.team_id": teamId,
|
|
205
|
+
"http.response.status_code": response.status
|
|
206
|
+
});
|
|
207
|
+
return null;
|
|
222
208
|
}
|
|
209
|
+
const parsed = slackUserInfoSchema.safeParse(await response.json());
|
|
210
|
+
if (!parsed.success) return null;
|
|
211
|
+
const payload = parsed.data;
|
|
212
|
+
const result = {
|
|
213
|
+
userName: payload.user.name?.trim() || void 0,
|
|
214
|
+
fullName: payload.user.profile?.real_name?.trim() || void 0,
|
|
215
|
+
email: payload.user.profile?.email?.trim() || void 0,
|
|
216
|
+
timezone: payload.user.tz?.trim() || void 0
|
|
217
|
+
};
|
|
218
|
+
writeToCache(teamId, userId, result);
|
|
219
|
+
return result;
|
|
220
|
+
} catch (error) {
|
|
221
|
+
logWarn("slack.user.lookup.failed", {
|
|
222
|
+
"app.slack.user_id": userId,
|
|
223
|
+
"app.slack.team_id": teamId,
|
|
224
|
+
"exception.message": error instanceof Error ? error.message : String(error)
|
|
225
|
+
});
|
|
226
|
+
return null;
|
|
223
227
|
}
|
|
224
228
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const lineMatch = trimmed.match(/line\s+(\d+):/i);
|
|
231
|
-
if (lineMatch) {
|
|
232
|
-
const line = Number.parseInt(lineMatch[1] ?? "", 10);
|
|
233
|
-
if (Number.isFinite(line)) parsed.detailLine = line;
|
|
234
|
-
}
|
|
235
|
-
if (/unsupported heading depth/i.test(trimmed)) parsed.detailRule = "unsupported_heading_depth";
|
|
236
|
-
return parsed;
|
|
229
|
+
// src/chat/identities/timezone.ts
|
|
230
|
+
const DEFAULT_TIMEZONE = "America/Los_Angeles";
|
|
231
|
+
/** The install timezone, used when Junior does not know a person's timezone. */
|
|
232
|
+
function defaultTimezone() {
|
|
233
|
+
return process.env.JUNIOR_TIMEZONE?.trim() || DEFAULT_TIMEZONE;
|
|
237
234
|
}
|
|
238
235
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*/
|
|
243
|
-
const installationTokenStorage = new AsyncLocalStorage();
|
|
244
|
-
const clientsByToken = /* @__PURE__ */ new Map();
|
|
245
|
-
/**
|
|
246
|
-
* Bind a workspace installation token for all Slack Web API calls inside
|
|
247
|
-
* `fn`, so multi-workspace outbound writes use the destination workspace's
|
|
248
|
-
* credentials rather than the env fallback token.
|
|
236
|
+
* Pick a person's timezone: their Slack profile, then the saved user
|
|
237
|
+
* timezone, then the install default. A valid Slack timezone is saved on the
|
|
238
|
+
* linked user, so later lookups without Slack still find it.
|
|
249
239
|
*/
|
|
250
|
-
function
|
|
251
|
-
const
|
|
252
|
-
if (
|
|
253
|
-
|
|
240
|
+
async function resolveUserTimezone(args) {
|
|
241
|
+
const timezone = (args.slack ? await lookupSlackUser(args.slack.teamId, args.slack.userId) : void 0)?.timezone;
|
|
242
|
+
if (timezone && isValidTimeZone(timezone)) {
|
|
243
|
+
if (args.userId) await saveUserTimezone(args.db, args.userId, timezone, args.nowMs);
|
|
244
|
+
return timezone;
|
|
245
|
+
}
|
|
246
|
+
return (args.userId ? await readUserTimezone(args.db, args.userId) : void 0) ?? defaultTimezone();
|
|
254
247
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const teamId = getWorkspaceTeamId();
|
|
266
|
-
if (teamId) throw new SlackActionError(`Slack call is scoped to workspace ${teamId} but no installation token is bound and no default bot token is configured`, "missing_token");
|
|
267
|
-
throw new SlackActionError("SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack Web API actions in this service", "missing_token");
|
|
248
|
+
// src/chat/plugins/annotations.ts
|
|
249
|
+
function annotationFromRow(row) {
|
|
250
|
+
const parsed = conversationAnnotationInputSchema.safeParse(row.annotation);
|
|
251
|
+
if (!parsed.success) return void 0;
|
|
252
|
+
return {
|
|
253
|
+
...parsed.data,
|
|
254
|
+
plugin: row.plugin,
|
|
255
|
+
createdAt: row.createdAt.toISOString(),
|
|
256
|
+
updatedAt: row.updatedAt.toISOString()
|
|
257
|
+
};
|
|
268
258
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
259
|
+
function createPluginAnnotations(args) {
|
|
260
|
+
return {
|
|
261
|
+
async upsert(input) {
|
|
262
|
+
const annotation = conversationAnnotationInputSchema.parse(input);
|
|
263
|
+
const now = /* @__PURE__ */ new Date();
|
|
264
|
+
await args.db.insert(juniorConversationAnnotations).values({
|
|
265
|
+
annotation,
|
|
266
|
+
conversationId: args.conversationId,
|
|
267
|
+
createdAt: now,
|
|
268
|
+
key: annotation.key,
|
|
269
|
+
kind: annotation.kind,
|
|
270
|
+
plugin: args.plugin,
|
|
271
|
+
updatedAt: now
|
|
272
|
+
}).onConflictDoUpdate({
|
|
273
|
+
target: [
|
|
274
|
+
juniorConversationAnnotations.conversationId,
|
|
275
|
+
juniorConversationAnnotations.plugin,
|
|
276
|
+
juniorConversationAnnotations.kind,
|
|
277
|
+
juniorConversationAnnotations.key
|
|
278
|
+
],
|
|
279
|
+
set: {
|
|
280
|
+
annotation,
|
|
281
|
+
updatedAt: now
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
},
|
|
285
|
+
async remove(kind, key) {
|
|
286
|
+
await args.db.delete(juniorConversationAnnotations).where(and(eq(juniorConversationAnnotations.conversationId, args.conversationId), eq(juniorConversationAnnotations.plugin, args.plugin), eq(juniorConversationAnnotations.kind, kind), eq(juniorConversationAnnotations.key, key)));
|
|
287
|
+
},
|
|
288
|
+
async list() {
|
|
289
|
+
return (await listConversationAnnotations(args.db, args.conversationId)).filter((a) => a.plugin === args.plugin);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
272
292
|
}
|
|
273
|
-
/**
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
*/
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
clientsByToken.set(token, cached);
|
|
293
|
+
/** Load stored annotations for one conversation, newest update first. */
|
|
294
|
+
async function listConversationAnnotations(db, conversationId) {
|
|
295
|
+
return (await listConversationAnnotationsById(db, [conversationId])).get(conversationId) ?? [];
|
|
296
|
+
}
|
|
297
|
+
/** Load stored annotations for a bounded set of conversations. */
|
|
298
|
+
async function listConversationAnnotationsById(db, conversationIds) {
|
|
299
|
+
const byConversation = /* @__PURE__ */ new Map();
|
|
300
|
+
if (conversationIds.length === 0) return byConversation;
|
|
301
|
+
const rows = await db.select().from(juniorConversationAnnotations).where(inArray(juniorConversationAnnotations.conversationId, [...conversationIds])).orderBy(desc(juniorConversationAnnotations.updatedAt));
|
|
302
|
+
for (const row of rows) {
|
|
303
|
+
const annotation = annotationFromRow(row);
|
|
304
|
+
if (!annotation) continue;
|
|
305
|
+
const current = byConversation.get(row.conversationId);
|
|
306
|
+
if (current) current.push(annotation);
|
|
307
|
+
else byConversation.set(row.conversationId, [annotation]);
|
|
289
308
|
}
|
|
290
|
-
return
|
|
309
|
+
return byConversation;
|
|
291
310
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
needed: candidate.data?.needed,
|
|
307
|
-
provided: candidate.data?.provided
|
|
308
|
-
});
|
|
309
|
-
if (apiError === "not_in_channel") return new SlackActionError(message, "not_in_channel", baseOptions);
|
|
310
|
-
if (apiError === "restricted_action_read_only_channel") return new SlackActionError(message, "read_only_channel", baseOptions);
|
|
311
|
-
if (apiError === "already_reacted") return new SlackActionError(message, "already_reacted", baseOptions);
|
|
312
|
-
if (apiError === "no_reaction") return new SlackActionError(message, "no_reaction", baseOptions);
|
|
313
|
-
if (apiError === "invalid_arguments") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
314
|
-
if (apiError === "invalid_cursor") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
315
|
-
if (apiError === "invalid_name") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
316
|
-
if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") return new SlackActionError(message, "not_found", baseOptions);
|
|
317
|
-
if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") return new SlackActionError(message, "feature_unavailable", baseOptions);
|
|
318
|
-
if (apiError === "canvas_creation_failed") return new SlackActionError(message, "canvas_creation_failed", baseOptions);
|
|
319
|
-
if (apiError === "canvas_editing_failed") return new SlackActionError(message, "canvas_editing_failed", baseOptions);
|
|
320
|
-
if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) return new SlackActionError(message, "rate_limited", {
|
|
321
|
-
...baseOptions,
|
|
322
|
-
retryAfterSeconds: candidate.retryAfter
|
|
311
|
+
// src/chat/conversations/annotation-results.ts
|
|
312
|
+
/** Save object annotations and retain returned facts in the tool result. */
|
|
313
|
+
async function saveObjectAnnotations(conversationId, plugin, value) {
|
|
314
|
+
const annotations = objectAnnotationSchema.array().parse(value);
|
|
315
|
+
await getDb().transaction(async (db) => {
|
|
316
|
+
const store = createPluginAnnotations({
|
|
317
|
+
conversationId,
|
|
318
|
+
plugin,
|
|
319
|
+
db
|
|
320
|
+
});
|
|
321
|
+
for (const annotation of annotations) {
|
|
322
|
+
await store.upsert(annotation);
|
|
323
|
+
await store.remove("resource_link", annotation.key);
|
|
324
|
+
}
|
|
323
325
|
});
|
|
324
|
-
return
|
|
326
|
+
return annotations.map((annotation) => ({
|
|
327
|
+
...annotation,
|
|
328
|
+
plugin
|
|
329
|
+
}));
|
|
325
330
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (typeof candidate.code === "string" && (CONNECTION_ERROR_CODES.has(candidate.code) || TIMEOUT_ERROR_CODES.has(candidate.code))) return candidate.code;
|
|
343
|
-
return findNetworkErrorCode(candidate.original, depth + 1) ?? findNetworkErrorCode(candidate.cause, depth + 1);
|
|
331
|
+
/** Save annotations from a successful plugin tool result and attach their cards. */
|
|
332
|
+
async function annotateToolResult(conversationId, plugin, result) {
|
|
333
|
+
if (!isRecord$1(result) || result.isError === true) return result;
|
|
334
|
+
const details = isRecord$1(result.details) ? result.details : result;
|
|
335
|
+
if (details.timed_out === true || details.isError === true || !Array.isArray(details.objectAnnotations)) return result;
|
|
336
|
+
const cards = await saveObjectAnnotations(conversationId, plugin, details.objectAnnotations);
|
|
337
|
+
const { objectAnnotations: _annotations, ...rest } = details;
|
|
338
|
+
const annotated = {
|
|
339
|
+
...rest,
|
|
340
|
+
objectCards: cards
|
|
341
|
+
};
|
|
342
|
+
if (isRecord$1(result.details)) return {
|
|
343
|
+
...result,
|
|
344
|
+
details: annotated
|
|
345
|
+
};
|
|
346
|
+
return annotated;
|
|
344
347
|
}
|
|
345
|
-
|
|
346
|
-
|
|
348
|
+
// src/chat/conversations/sql/privacy.ts
|
|
349
|
+
const privacyRoot = alias(juniorConversations, "privacy_root");
|
|
350
|
+
/** Accept only a structurally valid persisted root as privacy authority. */
|
|
351
|
+
async function readRootCandidate(executor, conversationId) {
|
|
352
|
+
const row = (await executor.db().select({
|
|
353
|
+
requestedConversationId: juniorConversations.conversationId,
|
|
354
|
+
requestedParentConversationId: juniorConversations.parentConversationId,
|
|
355
|
+
requestedRootConversationId: juniorConversations.rootConversationId,
|
|
356
|
+
rootConversationId: privacyRoot.conversationId,
|
|
357
|
+
rootDestinationId: privacyRoot.destinationId,
|
|
358
|
+
rootParentConversationId: privacyRoot.parentConversationId,
|
|
359
|
+
rootRootConversationId: privacyRoot.rootConversationId
|
|
360
|
+
}).from(juniorConversations).leftJoin(privacyRoot, eq(privacyRoot.conversationId, juniorConversations.rootConversationId)).where(eq(juniorConversations.conversationId, conversationId)))[0];
|
|
361
|
+
return row !== void 0 && row.requestedRootConversationId !== null && (row.requestedParentConversationId !== null || row.requestedRootConversationId === row.requestedConversationId) && row.rootConversationId === row.requestedRootConversationId && row.rootRootConversationId === row.rootConversationId && row.rootParentConversationId === null && row.rootDestinationId !== null ? {
|
|
362
|
+
destinationId: row.rootDestinationId,
|
|
363
|
+
rootConversationId: row.rootConversationId
|
|
364
|
+
} : void 0;
|
|
347
365
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
function classifySlackRetry(raw, mapped) {
|
|
355
|
-
if (mapped.code === "rate_limited") return "rate_limited";
|
|
356
|
-
if (mapped.statusCode !== void 0 && mapped.statusCode >= 500) return "server_error";
|
|
357
|
-
const networkCode = findNetworkErrorCode(raw);
|
|
358
|
-
if (networkCode && CONNECTION_ERROR_CODES.has(networkCode)) return "connection";
|
|
359
|
-
if (networkCode && TIMEOUT_ERROR_CODES.has(networkCode)) return "timeout";
|
|
360
|
-
if (hasSocketHangUpMessage(raw)) return "connection";
|
|
361
|
-
return "none";
|
|
366
|
+
async function readCandidateVisibility(executor, candidate) {
|
|
367
|
+
const destinations = await executor.db().select({ visibility: juniorDestinations.visibility }).from(juniorDestinations).where(eq(juniorDestinations.id, candidate.destinationId)).for("share");
|
|
368
|
+
return {
|
|
369
|
+
rootConversationId: candidate.rootConversationId,
|
|
370
|
+
visibility: destinations[0]?.visibility ?? null
|
|
371
|
+
};
|
|
362
372
|
}
|
|
363
373
|
/**
|
|
364
|
-
*
|
|
365
|
-
* failures do not surface as turn failures, while non-idempotent posts are
|
|
366
|
-
* never re-sent when Slack may already have accepted them.
|
|
374
|
+
* Resolve a conversation's privacy authority from its persisted root.
|
|
367
375
|
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
376
|
+
* Missing or structurally invalid root metadata fails closed. The root
|
|
377
|
+
* destination is locked so callers that keep the transaction open receive a
|
|
378
|
+
* stable visibility decision.
|
|
371
379
|
*/
|
|
372
|
-
async function
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
"url.scheme": "https",
|
|
399
|
-
"url.path": `/api/${action}`,
|
|
400
|
-
"app.slack.method": action,
|
|
401
|
-
"app.retry.max_attempts": maxAttempts,
|
|
402
|
-
...attemptNumber > 1 ? { "http.request.resend_count": attemptNumber - 1 } : void 0,
|
|
403
|
-
...context.attributes ?? {},
|
|
404
|
-
...context.spanAttributes ?? {}
|
|
405
|
-
});
|
|
406
|
-
} catch (error) {
|
|
407
|
-
const mapped = mapSlackError(error);
|
|
408
|
-
const retryClass = classifySlackRetry(error, mapped);
|
|
409
|
-
const isRetryable = retryClass === "rate_limited" || retryClass === "connection" || retryClass === "server_error" || retryClass === "timeout" && context.idempotent === true;
|
|
410
|
-
const remainingDelayBudgetMs = MAX_TOTAL_RETRY_DELAY_MS - totalDelayMs;
|
|
411
|
-
const baseLogAttributes = {
|
|
412
|
-
"app.slack.action": action,
|
|
413
|
-
"app.slack.error_code": mapped.code,
|
|
414
|
-
...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : void 0,
|
|
415
|
-
...mapped.detail ? { "app.slack.detail": mapped.detail } : void 0,
|
|
416
|
-
...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : void 0,
|
|
417
|
-
...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : void 0,
|
|
418
|
-
...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : void 0,
|
|
419
|
-
...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : void 0,
|
|
420
|
-
...context.attributes ?? {}
|
|
421
|
-
};
|
|
422
|
-
if (!isRetryable || attempt >= maxAttempts || remainingDelayBudgetMs <= 0) {
|
|
423
|
-
logWarn("slack.action.failed", {
|
|
424
|
-
...baseLogAttributes,
|
|
425
|
-
...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : void 0
|
|
426
|
-
});
|
|
427
|
-
throw mapped;
|
|
428
|
-
}
|
|
429
|
-
logWarn("slack.action.retrying", {
|
|
430
|
-
...baseLogAttributes,
|
|
431
|
-
"app.slack.retry_attempt": attempt,
|
|
432
|
-
"app.slack.retry_class": retryClass
|
|
433
|
-
});
|
|
434
|
-
const retryAfterMs = retryClass === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
|
|
435
|
-
const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
|
|
436
|
-
const delayMs = Math.min(retryAfterMs ?? backoffMs, MAX_RETRY_DELAY_MS, remainingDelayBudgetMs);
|
|
437
|
-
totalDelayMs += delayMs;
|
|
438
|
-
await sleep$1(delayMs);
|
|
439
|
-
}
|
|
380
|
+
async function resolveRootVisibility(executor, conversationId) {
|
|
381
|
+
const candidate = await readRootCandidate(executor, conversationId);
|
|
382
|
+
if (!candidate) return {
|
|
383
|
+
rootConversationId: conversationId,
|
|
384
|
+
visibility: null
|
|
385
|
+
};
|
|
386
|
+
return readCandidateVisibility(executor, candidate);
|
|
387
|
+
}
|
|
388
|
+
// src/chat/briefs/events.ts
|
|
389
|
+
/** Durable cost and size record for one stored Brief version. */
|
|
390
|
+
const briefUpdatedEvent = defineConversationEvent({
|
|
391
|
+
name: "brief_updated",
|
|
392
|
+
version: 1,
|
|
393
|
+
schema: z.object({
|
|
394
|
+
version: z.number().int().positive(),
|
|
395
|
+
modelId: z.string().min(1),
|
|
396
|
+
costUsd: z.number().finite().nonnegative().optional(),
|
|
397
|
+
decisions: z.number().int().nonnegative(),
|
|
398
|
+
openDecisions: z.number().int().nonnegative(),
|
|
399
|
+
links: z.number().int().nonnegative()
|
|
400
|
+
}).strict(),
|
|
401
|
+
renderEvent(event) {
|
|
402
|
+
return {
|
|
403
|
+
icon: "activity",
|
|
404
|
+
title: `Brief updated (v${event.version})`
|
|
405
|
+
};
|
|
440
406
|
}
|
|
441
|
-
|
|
407
|
+
});
|
|
408
|
+
// src/chat/slack/attachments.ts
|
|
409
|
+
/** Save the vision summary for a stored Slack attachment. */
|
|
410
|
+
async function recordSlackAttachment(args) {
|
|
411
|
+
await args.db.db().update(juniorAttachments).set({ visionSummary: args.visionSummary }).where(and(eq(juniorAttachments.id, args.attachmentId), eq(juniorAttachments.conversationId, args.conversationId), isNull(juniorAttachments.deleteRequestedAt)));
|
|
442
412
|
}
|
|
443
|
-
/**
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
* is workspace-scoped and no installation token can be resolved, so a write
|
|
448
|
-
* never goes out with another workspace's credentials.
|
|
449
|
-
*/
|
|
450
|
-
function getSlackClient(options) {
|
|
451
|
-
if (options) return new WebClient(resolveSlackToken(), {
|
|
452
|
-
retryConfig: { retries: 0 },
|
|
453
|
-
rejectRateLimitedCalls: true,
|
|
454
|
-
timeout: options.timeoutMs
|
|
455
|
-
});
|
|
456
|
-
return getClient();
|
|
413
|
+
/** Read a saved vision summary for one live attachment. */
|
|
414
|
+
async function readAttachmentVisionSummary(args) {
|
|
415
|
+
const [row] = await args.db.db().select({ visionSummary: juniorAttachments.visionSummary }).from(juniorAttachments).where(and(eq(juniorAttachments.id, args.attachmentId), eq(juniorAttachments.conversationId, args.conversationId), isNull(juniorAttachments.deleteRequestedAt)));
|
|
416
|
+
return row?.visionSummary;
|
|
457
417
|
}
|
|
458
|
-
/**
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
418
|
+
/** Match live conversation attachments to Slack file ids. */
|
|
419
|
+
async function matchSlackAttachments(args) {
|
|
420
|
+
if (args.providerIds.length === 0) return [];
|
|
421
|
+
return await args.db.db().select({
|
|
422
|
+
id: juniorAttachments.id,
|
|
423
|
+
providerId: juniorAttachments.providerId,
|
|
424
|
+
bytes: juniorAttachments.bytes,
|
|
425
|
+
contentType: juniorAttachments.contentType,
|
|
426
|
+
filename: juniorAttachments.filename
|
|
427
|
+
}).from(juniorAttachments).where(and(eq(juniorAttachments.conversationId, args.conversationId), eq(juniorAttachments.provider, "slack"), inArray(juniorAttachments.providerId, args.providerIds), isNull(juniorAttachments.deleteRequestedAt)));
|
|
468
428
|
}
|
|
469
|
-
/**
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const normalized = normalizeSlackConversationId(channelId);
|
|
475
|
-
if (!normalized) return false;
|
|
476
|
-
return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
|
|
429
|
+
/** Read the Slack file identity retained by the Chat SDK. */
|
|
430
|
+
function getSlackFileId(attachment) {
|
|
431
|
+
const metadataId = attachment.fetchMetadata?.fileId;
|
|
432
|
+
if (metadataId) return metadataId;
|
|
433
|
+
return (attachment.url ?? attachment.fetchMetadata?.url ?? "").match(/(?:^|[-/])(F[A-Z0-9]+)(?:[-/]|$)/i)?.[1];
|
|
477
434
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
435
|
+
// src/api/conversations/message-attachments.ts
|
|
436
|
+
/** Resolve saved Slack file references without exposing private storage details. */
|
|
437
|
+
async function hydrateMessageAttachments(db, conversationId, events) {
|
|
438
|
+
const messages = events.flatMap((event) => event.data.type === "message" ? [event.data] : []);
|
|
439
|
+
const ids = [...new Set(messages.flatMap((message) => slackFileIds(message.meta)))];
|
|
440
|
+
if (!ids.length) return;
|
|
441
|
+
const attachments = await matchSlackAttachments({
|
|
442
|
+
db,
|
|
443
|
+
conversationId,
|
|
444
|
+
providerIds: ids
|
|
445
|
+
});
|
|
446
|
+
const byFileId = new Map(attachments.map(({ providerId, ...attachment }) => [providerId, attachment]));
|
|
447
|
+
for (const message of messages) {
|
|
448
|
+
const stored = readMessageAttachments(message.meta?.attachments);
|
|
449
|
+
const matched = slackFileIds(message.meta).flatMap((id) => {
|
|
450
|
+
const attachment = byFileId.get(id);
|
|
451
|
+
return attachment ? [attachment] : [];
|
|
452
|
+
});
|
|
453
|
+
if (matched.length) message.meta = {
|
|
454
|
+
...message.meta,
|
|
455
|
+
attachments: [...stored, ...matched]
|
|
456
|
+
};
|
|
457
|
+
}
|
|
482
458
|
}
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
return (
|
|
486
|
-
action: "files.info",
|
|
487
|
-
idempotent: true,
|
|
488
|
-
spanAttributes: { "app.slack.file_id": fileId }
|
|
489
|
-
})).file?.permalink;
|
|
459
|
+
function slackFileIds(meta) {
|
|
460
|
+
const ids = meta?.slackFileIds ?? meta?.imageFileIds;
|
|
461
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
490
462
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return
|
|
494
|
-
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
495
|
-
setSpanAttributes({ "http.response.status_code": response.status });
|
|
496
|
-
if (!response.ok) {
|
|
497
|
-
setSpanAttributes({ "error.type": String(response.status) });
|
|
498
|
-
setSpanStatus("error");
|
|
499
|
-
throw new Error(`Slack file download failed: ${response.status}`);
|
|
500
|
-
}
|
|
501
|
-
return Buffer.from(await response.arrayBuffer());
|
|
502
|
-
}, {
|
|
503
|
-
"http.request.method": "GET",
|
|
504
|
-
"server.address": "files.slack.com",
|
|
505
|
-
"url.scheme": "https",
|
|
506
|
-
"app.slack.method": "files.download"
|
|
507
|
-
});
|
|
463
|
+
// src/chat/sentry-links.ts
|
|
464
|
+
function getSentryOrgSlug() {
|
|
465
|
+
return process.env.SENTRY_ORG_SLUG?.trim() || void 0;
|
|
508
466
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
ok: z.literal(true),
|
|
512
|
-
user: z.object({
|
|
513
|
-
name: z.string().nullish(),
|
|
514
|
-
tz: z.string().nullish(),
|
|
515
|
-
profile: z.object({
|
|
516
|
-
email: z.string().nullish(),
|
|
517
|
-
real_name: z.string().nullish()
|
|
518
|
-
}).optional()
|
|
519
|
-
})
|
|
520
|
-
});
|
|
521
|
-
const USER_CACHE_TTL_MS = 3e5;
|
|
522
|
-
const userCache = /* @__PURE__ */ new Map();
|
|
523
|
-
function userCacheKey(teamId, userId) {
|
|
524
|
-
return `${teamId}:${userId}`;
|
|
467
|
+
function isSentrySaasDsnHost(host) {
|
|
468
|
+
return host === "sentry.io" || host.endsWith(".sentry.io");
|
|
525
469
|
}
|
|
526
|
-
function
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
return null;
|
|
532
|
-
}
|
|
533
|
-
return hit.value;
|
|
470
|
+
function buildSentryWebBaseUrl(dsn) {
|
|
471
|
+
if (isSentrySaasDsnHost(dsn.host)) return "https://sentry.io";
|
|
472
|
+
const port = dsn.port ? `:${dsn.port}` : "";
|
|
473
|
+
const path = dsn.path ? `/${dsn.path}` : "";
|
|
474
|
+
return `${dsn.protocol}://${dsn.host}${port}${path}`;
|
|
534
475
|
}
|
|
535
|
-
function
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
476
|
+
function buildSentryOrgPath(args) {
|
|
477
|
+
const dsn = getClient$1()?.getDsn();
|
|
478
|
+
if (!dsn?.host || !dsn.projectId) return;
|
|
479
|
+
const orgSlug = getSentryOrgSlug();
|
|
480
|
+
if (!orgSlug) return;
|
|
481
|
+
const params = new URLSearchParams();
|
|
482
|
+
params.set("project", dsn.projectId);
|
|
483
|
+
if (args.query) for (const [key, value] of Object.entries(args.query)) params.set(key, value);
|
|
484
|
+
const path = `${args.path}?${params.toString()}`;
|
|
485
|
+
if (isSentrySaasDsnHost(dsn.host)) return `https://${orgSlug}.sentry.io/${path}`;
|
|
486
|
+
return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path}`;
|
|
487
|
+
}
|
|
488
|
+
/** Build a Sentry conversation URL only when the runtime has enough Sentry config. */
|
|
489
|
+
function buildSentryConversationUrl(conversationId) {
|
|
490
|
+
return buildSentryOrgPath({ path: `explore/conversations/${encodeURIComponent(conversationId)}/` });
|
|
491
|
+
}
|
|
492
|
+
/** Build a Sentry event URL only when the runtime has enough Sentry config. */
|
|
493
|
+
function buildSentryEventUrl(eventId) {
|
|
494
|
+
return buildSentryOrgPath({
|
|
495
|
+
path: "issues/",
|
|
496
|
+
query: { query: eventId }
|
|
539
497
|
});
|
|
540
498
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
if (
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
499
|
+
// src/api/conversations/event-model.ts
|
|
500
|
+
/** Apply recorded model choices without carrying a previous turn's settings forward. */
|
|
501
|
+
function conversationEventModel(data, current) {
|
|
502
|
+
if (data.type === "turn_started") return void 0;
|
|
503
|
+
if (data.type === "turn_routed" || data.type === "handoff") return {
|
|
504
|
+
modelId: data.modelId,
|
|
505
|
+
modelProfile: data.modelProfile,
|
|
506
|
+
...data.reasoningLevel ? { reasoningLevel: data.reasoningLevel } : void 0
|
|
507
|
+
};
|
|
508
|
+
if (data.type === "assistant_message") {
|
|
509
|
+
const model = text$1(data.model);
|
|
510
|
+
const provider = text$1(data.provider);
|
|
511
|
+
if (model) {
|
|
512
|
+
const modelId = model.includes("/") || !provider ? model : `${provider}/${model}`;
|
|
513
|
+
return {
|
|
514
|
+
...current,
|
|
515
|
+
modelId
|
|
516
|
+
};
|
|
557
517
|
}
|
|
558
|
-
const parsed = slackUserInfoSchema.safeParse(await response.json());
|
|
559
|
-
if (!parsed.success) return null;
|
|
560
|
-
const payload = parsed.data;
|
|
561
|
-
const result = {
|
|
562
|
-
userName: payload.user.name?.trim() || void 0,
|
|
563
|
-
fullName: payload.user.profile?.real_name?.trim() || void 0,
|
|
564
|
-
email: payload.user.profile?.email?.trim() || void 0,
|
|
565
|
-
timezone: payload.user.tz?.trim() || void 0
|
|
566
|
-
};
|
|
567
|
-
writeToCache(teamId, userId, result);
|
|
568
|
-
return result;
|
|
569
|
-
} catch (error) {
|
|
570
|
-
logWarn("slack.user.lookup.failed", {
|
|
571
|
-
"app.slack.user_id": userId,
|
|
572
|
-
"app.slack.team_id": teamId,
|
|
573
|
-
"exception.message": error instanceof Error ? error.message : String(error)
|
|
574
|
-
});
|
|
575
|
-
return null;
|
|
576
518
|
}
|
|
519
|
+
return current;
|
|
577
520
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
/** The install timezone, used when Junior does not know a person's timezone. */
|
|
581
|
-
function defaultTimezone() {
|
|
582
|
-
return process.env.JUNIOR_TIMEZONE?.trim() || DEFAULT_TIMEZONE;
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Pick a person's timezone: their Slack profile, then the saved user
|
|
586
|
-
* timezone, then the install default. A valid Slack timezone is saved on the
|
|
587
|
-
* linked user, so later lookups without Slack still find it.
|
|
588
|
-
*/
|
|
589
|
-
async function resolveUserTimezone(args) {
|
|
590
|
-
const timezone = (args.slack ? await lookupSlackUser(args.slack.teamId, args.slack.userId) : void 0)?.timezone;
|
|
591
|
-
if (timezone && isValidTimeZone(timezone)) {
|
|
592
|
-
if (args.userId) await saveUserTimezone(args.db, args.userId, timezone, args.nowMs);
|
|
593
|
-
return timezone;
|
|
594
|
-
}
|
|
595
|
-
return (args.userId ? await readUserTimezone(args.db, args.userId) : void 0) ?? defaultTimezone();
|
|
521
|
+
function text$1(value) {
|
|
522
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
596
523
|
}
|
|
597
|
-
|
|
598
|
-
function
|
|
599
|
-
|
|
600
|
-
|
|
524
|
+
/** Allowlist model-call diagnostics; never expose opaque provider metadata. */
|
|
525
|
+
function conversationEventModelCall(data) {
|
|
526
|
+
if (data.type !== "assistant_message") return void 0;
|
|
527
|
+
const usage = extractGenAiUsageSummary(data);
|
|
528
|
+
const provider = text$1(data.provider);
|
|
529
|
+
const api = text$1(data.api);
|
|
530
|
+
const stopReason = text$1(data.stopReason);
|
|
601
531
|
return {
|
|
602
|
-
...
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
};
|
|
607
|
-
}
|
|
608
|
-
function createPluginAnnotations(args) {
|
|
609
|
-
return {
|
|
610
|
-
async upsert(input) {
|
|
611
|
-
const annotation = conversationAnnotationInputSchema.parse(input);
|
|
612
|
-
const now = /* @__PURE__ */ new Date();
|
|
613
|
-
await args.db.insert(juniorConversationAnnotations).values({
|
|
614
|
-
annotation,
|
|
615
|
-
conversationId: args.conversationId,
|
|
616
|
-
createdAt: now,
|
|
617
|
-
key: annotation.key,
|
|
618
|
-
kind: annotation.kind,
|
|
619
|
-
plugin: args.plugin,
|
|
620
|
-
updatedAt: now
|
|
621
|
-
}).onConflictDoUpdate({
|
|
622
|
-
target: [
|
|
623
|
-
juniorConversationAnnotations.conversationId,
|
|
624
|
-
juniorConversationAnnotations.plugin,
|
|
625
|
-
juniorConversationAnnotations.kind,
|
|
626
|
-
juniorConversationAnnotations.key
|
|
627
|
-
],
|
|
628
|
-
set: {
|
|
629
|
-
annotation,
|
|
630
|
-
updatedAt: now
|
|
631
|
-
}
|
|
632
|
-
});
|
|
633
|
-
},
|
|
634
|
-
async remove(kind, key) {
|
|
635
|
-
await args.db.delete(juniorConversationAnnotations).where(and(eq(juniorConversationAnnotations.conversationId, args.conversationId), eq(juniorConversationAnnotations.plugin, args.plugin), eq(juniorConversationAnnotations.kind, kind), eq(juniorConversationAnnotations.key, key)));
|
|
636
|
-
},
|
|
637
|
-
async list() {
|
|
638
|
-
return (await listConversationAnnotations(args.db, args.conversationId)).filter((a) => a.plugin === args.plugin);
|
|
639
|
-
}
|
|
640
|
-
};
|
|
641
|
-
}
|
|
642
|
-
/** Load stored annotations for one conversation, newest update first. */
|
|
643
|
-
async function listConversationAnnotations(db, conversationId) {
|
|
644
|
-
return (await listConversationAnnotationsById(db, [conversationId])).get(conversationId) ?? [];
|
|
645
|
-
}
|
|
646
|
-
/** Load stored annotations for a bounded set of conversations. */
|
|
647
|
-
async function listConversationAnnotationsById(db, conversationIds) {
|
|
648
|
-
const byConversation = /* @__PURE__ */ new Map();
|
|
649
|
-
if (conversationIds.length === 0) return byConversation;
|
|
650
|
-
const rows = await db.select().from(juniorConversationAnnotations).where(inArray(juniorConversationAnnotations.conversationId, [...conversationIds])).orderBy(desc(juniorConversationAnnotations.updatedAt));
|
|
651
|
-
for (const row of rows) {
|
|
652
|
-
const annotation = annotationFromRow(row);
|
|
653
|
-
if (!annotation) continue;
|
|
654
|
-
const current = byConversation.get(row.conversationId);
|
|
655
|
-
if (current) current.push(annotation);
|
|
656
|
-
else byConversation.set(row.conversationId, [annotation]);
|
|
657
|
-
}
|
|
658
|
-
return byConversation;
|
|
659
|
-
}
|
|
660
|
-
// src/chat/conversations/annotation-results.ts
|
|
661
|
-
/** Save object annotations and retain returned facts in the tool result. */
|
|
662
|
-
async function saveObjectAnnotations(conversationId, plugin, value) {
|
|
663
|
-
const annotations = objectAnnotationSchema.array().parse(value);
|
|
664
|
-
await getDb().transaction(async (db) => {
|
|
665
|
-
const store = createPluginAnnotations({
|
|
666
|
-
conversationId,
|
|
667
|
-
plugin,
|
|
668
|
-
db
|
|
669
|
-
});
|
|
670
|
-
for (const annotation of annotations) {
|
|
671
|
-
await store.upsert(annotation);
|
|
672
|
-
await store.remove("resource_link", annotation.key);
|
|
673
|
-
}
|
|
674
|
-
});
|
|
675
|
-
return annotations.map((annotation) => ({
|
|
676
|
-
...annotation,
|
|
677
|
-
plugin
|
|
678
|
-
}));
|
|
679
|
-
}
|
|
680
|
-
/** Save annotations from a successful plugin tool result and attach their cards. */
|
|
681
|
-
async function annotateToolResult(conversationId, plugin, result) {
|
|
682
|
-
if (!isRecord$1(result) || result.isError === true) return result;
|
|
683
|
-
const details = isRecord$1(result.details) ? result.details : result;
|
|
684
|
-
if (details.timed_out === true || details.isError === true || !Array.isArray(details.objectAnnotations)) return result;
|
|
685
|
-
const cards = await saveObjectAnnotations(conversationId, plugin, details.objectAnnotations);
|
|
686
|
-
const { objectAnnotations: _annotations, ...rest } = details;
|
|
687
|
-
const annotated = {
|
|
688
|
-
...rest,
|
|
689
|
-
objectCards: cards
|
|
690
|
-
};
|
|
691
|
-
if (isRecord$1(result.details)) return {
|
|
692
|
-
...result,
|
|
693
|
-
details: annotated
|
|
694
|
-
};
|
|
695
|
-
return annotated;
|
|
696
|
-
}
|
|
697
|
-
// src/chat/conversations/sql/privacy.ts
|
|
698
|
-
const privacyRoot = alias(juniorConversations, "privacy_root");
|
|
699
|
-
/** Accept only a structurally valid persisted root as privacy authority. */
|
|
700
|
-
async function readRootCandidate(executor, conversationId) {
|
|
701
|
-
const row = (await executor.db().select({
|
|
702
|
-
requestedConversationId: juniorConversations.conversationId,
|
|
703
|
-
requestedParentConversationId: juniorConversations.parentConversationId,
|
|
704
|
-
requestedRootConversationId: juniorConversations.rootConversationId,
|
|
705
|
-
rootConversationId: privacyRoot.conversationId,
|
|
706
|
-
rootDestinationId: privacyRoot.destinationId,
|
|
707
|
-
rootParentConversationId: privacyRoot.parentConversationId,
|
|
708
|
-
rootRootConversationId: privacyRoot.rootConversationId
|
|
709
|
-
}).from(juniorConversations).leftJoin(privacyRoot, eq(privacyRoot.conversationId, juniorConversations.rootConversationId)).where(eq(juniorConversations.conversationId, conversationId)))[0];
|
|
710
|
-
return row !== void 0 && row.requestedRootConversationId !== null && (row.requestedParentConversationId !== null || row.requestedRootConversationId === row.requestedConversationId) && row.rootConversationId === row.requestedRootConversationId && row.rootRootConversationId === row.rootConversationId && row.rootParentConversationId === null && row.rootDestinationId !== null ? {
|
|
711
|
-
destinationId: row.rootDestinationId,
|
|
712
|
-
rootConversationId: row.rootConversationId
|
|
713
|
-
} : void 0;
|
|
714
|
-
}
|
|
715
|
-
async function readCandidateVisibility(executor, candidate) {
|
|
716
|
-
const destinations = await executor.db().select({ visibility: juniorDestinations.visibility }).from(juniorDestinations).where(eq(juniorDestinations.id, candidate.destinationId)).for("share");
|
|
717
|
-
return {
|
|
718
|
-
rootConversationId: candidate.rootConversationId,
|
|
719
|
-
visibility: destinations[0]?.visibility ?? null
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
/**
|
|
723
|
-
* Resolve a conversation's privacy authority from its persisted root.
|
|
724
|
-
*
|
|
725
|
-
* Missing or structurally invalid root metadata fails closed. The root
|
|
726
|
-
* destination is locked so callers that keep the transaction open receive a
|
|
727
|
-
* stable visibility decision.
|
|
728
|
-
*/
|
|
729
|
-
async function resolveRootVisibility(executor, conversationId) {
|
|
730
|
-
const candidate = await readRootCandidate(executor, conversationId);
|
|
731
|
-
if (!candidate) return {
|
|
732
|
-
rootConversationId: conversationId,
|
|
733
|
-
visibility: null
|
|
734
|
-
};
|
|
735
|
-
return readCandidateVisibility(executor, candidate);
|
|
736
|
-
}
|
|
737
|
-
// src/chat/briefs/events.ts
|
|
738
|
-
/** Durable cost and size record for one stored Brief version. */
|
|
739
|
-
const briefUpdatedEvent = defineConversationEvent({
|
|
740
|
-
name: "brief_updated",
|
|
741
|
-
version: 1,
|
|
742
|
-
schema: z.object({
|
|
743
|
-
version: z.number().int().positive(),
|
|
744
|
-
modelId: z.string().min(1),
|
|
745
|
-
costUsd: z.number().finite().nonnegative().optional(),
|
|
746
|
-
decisions: z.number().int().nonnegative(),
|
|
747
|
-
openDecisions: z.number().int().nonnegative(),
|
|
748
|
-
links: z.number().int().nonnegative()
|
|
749
|
-
}).strict(),
|
|
750
|
-
renderEvent(event) {
|
|
751
|
-
return {
|
|
752
|
-
icon: "activity",
|
|
753
|
-
title: `Brief updated (v${event.version})`
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
});
|
|
757
|
-
// src/chat/slack/attachments.ts
|
|
758
|
-
/** Save the vision summary for a stored Slack attachment. */
|
|
759
|
-
async function recordSlackAttachment(args) {
|
|
760
|
-
await args.db.db().update(juniorAttachments).set({ visionSummary: args.visionSummary }).where(and(eq(juniorAttachments.id, args.attachmentId), eq(juniorAttachments.conversationId, args.conversationId), isNull(juniorAttachments.deleteRequestedAt)));
|
|
761
|
-
}
|
|
762
|
-
/** Read a saved vision summary for one live attachment. */
|
|
763
|
-
async function readAttachmentVisionSummary(args) {
|
|
764
|
-
const [row] = await args.db.db().select({ visionSummary: juniorAttachments.visionSummary }).from(juniorAttachments).where(and(eq(juniorAttachments.id, args.attachmentId), eq(juniorAttachments.conversationId, args.conversationId), isNull(juniorAttachments.deleteRequestedAt)));
|
|
765
|
-
return row?.visionSummary;
|
|
766
|
-
}
|
|
767
|
-
/** Match live conversation attachments to Slack file ids. */
|
|
768
|
-
async function matchSlackAttachments(args) {
|
|
769
|
-
if (args.providerIds.length === 0) return [];
|
|
770
|
-
return await args.db.db().select({
|
|
771
|
-
id: juniorAttachments.id,
|
|
772
|
-
providerId: juniorAttachments.providerId,
|
|
773
|
-
bytes: juniorAttachments.bytes,
|
|
774
|
-
contentType: juniorAttachments.contentType,
|
|
775
|
-
filename: juniorAttachments.filename
|
|
776
|
-
}).from(juniorAttachments).where(and(eq(juniorAttachments.conversationId, args.conversationId), eq(juniorAttachments.provider, "slack"), inArray(juniorAttachments.providerId, args.providerIds), isNull(juniorAttachments.deleteRequestedAt)));
|
|
777
|
-
}
|
|
778
|
-
/** Read the Slack file identity retained by the Chat SDK. */
|
|
779
|
-
function getSlackFileId(attachment) {
|
|
780
|
-
const metadataId = attachment.fetchMetadata?.fileId;
|
|
781
|
-
if (metadataId) return metadataId;
|
|
782
|
-
return (attachment.url ?? attachment.fetchMetadata?.url ?? "").match(/(?:^|[-/])(F[A-Z0-9]+)(?:[-/]|$)/i)?.[1];
|
|
783
|
-
}
|
|
784
|
-
// src/api/conversations/message-attachments.ts
|
|
785
|
-
/** Resolve saved Slack file references without exposing private storage details. */
|
|
786
|
-
async function hydrateMessageAttachments(db, conversationId, events) {
|
|
787
|
-
const messages = events.flatMap((event) => event.data.type === "message" ? [event.data] : []);
|
|
788
|
-
const ids = [...new Set(messages.flatMap((message) => slackFileIds(message.meta)))];
|
|
789
|
-
if (!ids.length) return;
|
|
790
|
-
const attachments = await matchSlackAttachments({
|
|
791
|
-
db,
|
|
792
|
-
conversationId,
|
|
793
|
-
providerIds: ids
|
|
794
|
-
});
|
|
795
|
-
const byFileId = new Map(attachments.map(({ providerId, ...attachment }) => [providerId, attachment]));
|
|
796
|
-
for (const message of messages) {
|
|
797
|
-
const stored = readMessageAttachments(message.meta?.attachments);
|
|
798
|
-
const matched = slackFileIds(message.meta).flatMap((id) => {
|
|
799
|
-
const attachment = byFileId.get(id);
|
|
800
|
-
return attachment ? [attachment] : [];
|
|
801
|
-
});
|
|
802
|
-
if (matched.length) message.meta = {
|
|
803
|
-
...message.meta,
|
|
804
|
-
attachments: [...stored, ...matched]
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
function slackFileIds(meta) {
|
|
809
|
-
const ids = meta?.slackFileIds ?? meta?.imageFileIds;
|
|
810
|
-
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
811
|
-
}
|
|
812
|
-
// src/chat/sentry-links.ts
|
|
813
|
-
function getSentryOrgSlug() {
|
|
814
|
-
return process.env.SENTRY_ORG_SLUG?.trim() || void 0;
|
|
815
|
-
}
|
|
816
|
-
function isSentrySaasDsnHost(host) {
|
|
817
|
-
return host === "sentry.io" || host.endsWith(".sentry.io");
|
|
818
|
-
}
|
|
819
|
-
function buildSentryWebBaseUrl(dsn) {
|
|
820
|
-
if (isSentrySaasDsnHost(dsn.host)) return "https://sentry.io";
|
|
821
|
-
const port = dsn.port ? `:${dsn.port}` : "";
|
|
822
|
-
const path = dsn.path ? `/${dsn.path}` : "";
|
|
823
|
-
return `${dsn.protocol}://${dsn.host}${port}${path}`;
|
|
824
|
-
}
|
|
825
|
-
function buildSentryOrgPath(args) {
|
|
826
|
-
const dsn = getClient$1()?.getDsn();
|
|
827
|
-
if (!dsn?.host || !dsn.projectId) return;
|
|
828
|
-
const orgSlug = getSentryOrgSlug();
|
|
829
|
-
if (!orgSlug) return;
|
|
830
|
-
const params = new URLSearchParams();
|
|
831
|
-
params.set("project", dsn.projectId);
|
|
832
|
-
if (args.query) for (const [key, value] of Object.entries(args.query)) params.set(key, value);
|
|
833
|
-
const path = `${args.path}?${params.toString()}`;
|
|
834
|
-
if (isSentrySaasDsnHost(dsn.host)) return `https://${orgSlug}.sentry.io/${path}`;
|
|
835
|
-
return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path}`;
|
|
836
|
-
}
|
|
837
|
-
/** Build a Sentry conversation URL only when the runtime has enough Sentry config. */
|
|
838
|
-
function buildSentryConversationUrl(conversationId) {
|
|
839
|
-
return buildSentryOrgPath({ path: `explore/conversations/${encodeURIComponent(conversationId)}/` });
|
|
840
|
-
}
|
|
841
|
-
/** Build a Sentry event URL only when the runtime has enough Sentry config. */
|
|
842
|
-
function buildSentryEventUrl(eventId) {
|
|
843
|
-
return buildSentryOrgPath({
|
|
844
|
-
path: "issues/",
|
|
845
|
-
query: { query: eventId }
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
// src/api/conversations/event-model.ts
|
|
849
|
-
/** Apply recorded model choices without carrying a previous turn's settings forward. */
|
|
850
|
-
function conversationEventModel(data, current) {
|
|
851
|
-
if (data.type === "turn_started") return void 0;
|
|
852
|
-
if (data.type === "turn_routed" || data.type === "handoff") return {
|
|
853
|
-
modelId: data.modelId,
|
|
854
|
-
modelProfile: data.modelProfile,
|
|
855
|
-
...data.reasoningLevel ? { reasoningLevel: data.reasoningLevel } : void 0
|
|
856
|
-
};
|
|
857
|
-
if (data.type === "assistant_message") {
|
|
858
|
-
const model = text$1(data.model);
|
|
859
|
-
const provider = text$1(data.provider);
|
|
860
|
-
if (model) {
|
|
861
|
-
const modelId = model.includes("/") || !provider ? model : `${provider}/${model}`;
|
|
862
|
-
return {
|
|
863
|
-
...current,
|
|
864
|
-
modelId
|
|
865
|
-
};
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
return current;
|
|
869
|
-
}
|
|
870
|
-
function text$1(value) {
|
|
871
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
872
|
-
}
|
|
873
|
-
/** Allowlist model-call diagnostics; never expose opaque provider metadata. */
|
|
874
|
-
function conversationEventModelCall(data) {
|
|
875
|
-
if (data.type !== "assistant_message") return void 0;
|
|
876
|
-
const usage = extractGenAiUsageSummary(data);
|
|
877
|
-
const provider = text$1(data.provider);
|
|
878
|
-
const api = text$1(data.api);
|
|
879
|
-
const stopReason = text$1(data.stopReason);
|
|
880
|
-
return {
|
|
881
|
-
...provider ? { provider } : void 0,
|
|
882
|
-
...api ? { api } : void 0,
|
|
883
|
-
...stopReason ? { stopReason } : void 0,
|
|
884
|
-
...hasAgentTurnUsage(usage) ? { usage } : void 0
|
|
532
|
+
...provider ? { provider } : void 0,
|
|
533
|
+
...api ? { api } : void 0,
|
|
534
|
+
...stopReason ? { stopReason } : void 0,
|
|
535
|
+
...hasAgentTurnUsage(usage) ? { usage } : void 0
|
|
885
536
|
};
|
|
886
537
|
}
|
|
887
538
|
// src/api/conversations/events.ts
|
|
@@ -1072,6 +723,12 @@ function conversationReportToolResultIds(events) {
|
|
|
1072
723
|
return result.success ? [result.data.toolCallId] : [];
|
|
1073
724
|
}))];
|
|
1074
725
|
}
|
|
726
|
+
const storedMessageEventsSchema = z.array(conversationReportMessageEventSchema);
|
|
727
|
+
/** Read the Events listed on a message that holds several Events. */
|
|
728
|
+
function reportMessageEvents(meta) {
|
|
729
|
+
const parsed = storedMessageEventsSchema.safeParse(meta?.events);
|
|
730
|
+
return parsed.success ? { events: parsed.data } : void 0;
|
|
731
|
+
}
|
|
1075
732
|
function reportEventData(args) {
|
|
1076
733
|
const { data } = args;
|
|
1077
734
|
if (data.type === "structured_event") {
|
|
@@ -1100,6 +757,7 @@ function reportEventData(args) {
|
|
|
1100
757
|
...typeof data.meta?.eventType === "string" ? { eventType: data.meta.eventType } : void 0,
|
|
1101
758
|
...typeof data.meta?.explicitMention === "boolean" ? { explicitMention: data.meta.explicitMention } : void 0,
|
|
1102
759
|
...args.canExposePayload && typeof data.meta?.trustedSummary === "string" ? { trustedSummary: data.meta.trustedSummary } : void 0,
|
|
760
|
+
...args.canExposePayload ? reportMessageEvents(data.meta) : void 0,
|
|
1103
761
|
...args.canExposePayload && (data.meta?.cards || data.meta?.objectCards) ? { cards: readMessageCardRefs(data.meta) } : void 0,
|
|
1104
762
|
...args.canExposePayload ? {
|
|
1105
763
|
text: data.text,
|
|
@@ -1793,12 +1451,16 @@ function conversationHasParticipantUser(userId) {
|
|
|
1793
1451
|
return exists(participant);
|
|
1794
1452
|
}
|
|
1795
1453
|
/**
|
|
1796
|
-
* True when
|
|
1797
|
-
*
|
|
1454
|
+
* True when the conversation is one the person started or participates in.
|
|
1455
|
+
* Feeds filter with this id set instead of a per-row membership check, so
|
|
1456
|
+
* Postgres reads only that person's conversations, not every root.
|
|
1798
1457
|
*/
|
|
1799
|
-
function
|
|
1800
|
-
const
|
|
1801
|
-
|
|
1458
|
+
function conversationIsMemberRoot(member) {
|
|
1459
|
+
const db = getDb();
|
|
1460
|
+
const started = db.select({ conversationId: juniorConversations.conversationId }).from(juniorConversations).innerJoin(juniorIdentities, eq(juniorIdentities.id, juniorConversations.actorIdentityId));
|
|
1461
|
+
const participating = db.select({ conversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants);
|
|
1462
|
+
const memberRoots = "userId" in member ? union(started.where(eq(juniorIdentities.userId, member.userId)), participating.where(eq(juniorConversationParticipants.userId, member.userId))) : union(started.innerJoin(juniorUsers, eq(juniorUsers.id, juniorIdentities.userId)).where(eq(juniorUsers.primaryEmailNormalized, member.email)), participating.innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(eq(juniorUsers.primaryEmailNormalized, member.email)));
|
|
1463
|
+
return inArray(juniorConversations.conversationId, memberRoots);
|
|
1802
1464
|
}
|
|
1803
1465
|
/** True when one user's personal feed has archived this root after the cutoff. */
|
|
1804
1466
|
function conversationArchivedForUser(userId, archivedAfter) {
|
|
@@ -2229,6 +1891,355 @@ async function updateTimerIndex(state, operation) {
|
|
|
2229
1891
|
await state.releaseLock(lock);
|
|
2230
1892
|
}
|
|
2231
1893
|
}
|
|
1894
|
+
// src/chat/slack/workspace-context.ts
|
|
1895
|
+
const workspaceTeamIdStorage = new AsyncLocalStorage();
|
|
1896
|
+
/** Run a callback with the Slack workspace team ID for the inbound webhook. */
|
|
1897
|
+
function runWithWorkspaceTeamId(teamId, fn) {
|
|
1898
|
+
if (!teamId) return fn();
|
|
1899
|
+
return workspaceTeamIdStorage.run(teamId, fn);
|
|
1900
|
+
}
|
|
1901
|
+
/** Return the Slack workspace team ID for the current inbound webhook. */
|
|
1902
|
+
function getWorkspaceTeamId() {
|
|
1903
|
+
return workspaceTeamIdStorage.getStore();
|
|
1904
|
+
}
|
|
1905
|
+
// src/chat/slack/client.ts
|
|
1906
|
+
var SlackActionError = class extends Error {
|
|
1907
|
+
code;
|
|
1908
|
+
apiError;
|
|
1909
|
+
needed;
|
|
1910
|
+
provided;
|
|
1911
|
+
statusCode;
|
|
1912
|
+
requestId;
|
|
1913
|
+
errorData;
|
|
1914
|
+
retryAfterSeconds;
|
|
1915
|
+
detail;
|
|
1916
|
+
detailLine;
|
|
1917
|
+
detailRule;
|
|
1918
|
+
constructor(message, code, options = {}) {
|
|
1919
|
+
super(message);
|
|
1920
|
+
this.name = "SlackActionError";
|
|
1921
|
+
this.code = code;
|
|
1922
|
+
this.apiError = options.apiError;
|
|
1923
|
+
this.needed = options.needed;
|
|
1924
|
+
this.provided = options.provided;
|
|
1925
|
+
this.statusCode = options.statusCode;
|
|
1926
|
+
this.requestId = options.requestId;
|
|
1927
|
+
this.errorData = options.errorData;
|
|
1928
|
+
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
1929
|
+
this.detail = options.detail;
|
|
1930
|
+
this.detailLine = options.detailLine;
|
|
1931
|
+
this.detailRule = options.detailRule;
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
function serializeSlackErrorData(data) {
|
|
1935
|
+
if (!data || typeof data !== "object") return;
|
|
1936
|
+
const filtered = Object.fromEntries(Object.entries(data).filter(([key]) => key !== "error"));
|
|
1937
|
+
if (Object.keys(filtered).length === 0) return;
|
|
1938
|
+
try {
|
|
1939
|
+
const serialized = JSON.stringify(filtered);
|
|
1940
|
+
return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
|
|
1941
|
+
} catch {
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
/** Extract a header value by case-insensitive name from a raw headers object. */
|
|
1946
|
+
function getHeaderString(headers, name) {
|
|
1947
|
+
if (!headers || typeof headers !== "object") return;
|
|
1948
|
+
const key = name.toLowerCase();
|
|
1949
|
+
const entries = headers;
|
|
1950
|
+
for (const [entryKey, value] of Object.entries(entries)) {
|
|
1951
|
+
if (entryKey.toLowerCase() !== key) continue;
|
|
1952
|
+
if (typeof value === "string") return value;
|
|
1953
|
+
if (Array.isArray(value)) {
|
|
1954
|
+
const first = value.find((entry) => typeof entry === "string");
|
|
1955
|
+
return typeof first === "string" ? first : void 0;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
function parseSlackCanvasDetail(detail) {
|
|
1960
|
+
if (typeof detail !== "string") return {};
|
|
1961
|
+
const trimmed = detail.trim();
|
|
1962
|
+
if (!trimmed) return {};
|
|
1963
|
+
const parsed = { detail: trimmed };
|
|
1964
|
+
const lineMatch = trimmed.match(/line\s+(\d+):/i);
|
|
1965
|
+
if (lineMatch) {
|
|
1966
|
+
const line = Number.parseInt(lineMatch[1] ?? "", 10);
|
|
1967
|
+
if (Number.isFinite(line)) parsed.detailLine = line;
|
|
1968
|
+
}
|
|
1969
|
+
if (/unsupported heading depth/i.test(trimmed)) parsed.detailRule = "unsupported_heading_depth";
|
|
1970
|
+
return parsed;
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Ambient destination-installation token for Slack outbound calls, bound by
|
|
1974
|
+
* installation-scoped entry points so writes carry the destination
|
|
1975
|
+
* workspace's credentials instead of the process-global env token.
|
|
1976
|
+
*/
|
|
1977
|
+
const installationTokenStorage = new AsyncLocalStorage();
|
|
1978
|
+
const clientsByToken = /* @__PURE__ */ new Map();
|
|
1979
|
+
/**
|
|
1980
|
+
* Bind a workspace installation token for all Slack Web API calls inside
|
|
1981
|
+
* `fn`, so multi-workspace outbound writes use the destination workspace's
|
|
1982
|
+
* credentials rather than the env fallback token.
|
|
1983
|
+
*/
|
|
1984
|
+
function runWithSlackInstallationToken(token, fn) {
|
|
1985
|
+
const trimmed = token.trim();
|
|
1986
|
+
if (!trimmed) throw new SlackActionError("Slack installation token binding requires a non-empty token", "missing_token");
|
|
1987
|
+
return installationTokenStorage.run({ token: trimmed }, fn);
|
|
1988
|
+
}
|
|
1989
|
+
/**
|
|
1990
|
+
* Token precedence: ambient installation token, then env token, then a hard
|
|
1991
|
+
* failure for team-scoped calls — a call scoped to one workspace must never
|
|
1992
|
+
* silently fall through to another workspace's credentials.
|
|
1993
|
+
*/
|
|
1994
|
+
function resolveSlackToken() {
|
|
1995
|
+
const ambientToken = installationTokenStorage.getStore()?.token;
|
|
1996
|
+
if (ambientToken) return ambientToken;
|
|
1997
|
+
const envToken = getSlackBotToken();
|
|
1998
|
+
if (envToken) return envToken;
|
|
1999
|
+
const teamId = getWorkspaceTeamId();
|
|
2000
|
+
if (teamId) throw new SlackActionError(`Slack call is scoped to workspace ${teamId} but no installation token is bound and no default bot token is configured`, "missing_token");
|
|
2001
|
+
throw new SlackActionError("SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack Web API actions in this service", "missing_token");
|
|
2002
|
+
}
|
|
2003
|
+
/** Normalize Junior Slack references to native Slack conversation IDs. */
|
|
2004
|
+
function normalizeSlackConversationId(channelId) {
|
|
2005
|
+
return parseSlackChannelReferenceId(channelId);
|
|
2006
|
+
}
|
|
2007
|
+
/**
|
|
2008
|
+
* Return the per-token cached WebClient. `withSlackRetries` owns retry
|
|
2009
|
+
* classification for this boundary: the WebClient's built-in policy would
|
|
2010
|
+
* re-post timed-out writes (a duplicate-message hazard) and sleep for the
|
|
2011
|
+
* full unbounded Retry-After inside the request, so both internal behaviors
|
|
2012
|
+
* are disabled here.
|
|
2013
|
+
*/
|
|
2014
|
+
function getClient() {
|
|
2015
|
+
const token = resolveSlackToken();
|
|
2016
|
+
let cached = clientsByToken.get(token);
|
|
2017
|
+
if (!cached) {
|
|
2018
|
+
cached = new WebClient(token, {
|
|
2019
|
+
retryConfig: { retries: 0 },
|
|
2020
|
+
rejectRateLimitedCalls: true
|
|
2021
|
+
});
|
|
2022
|
+
clientsByToken.set(token, cached);
|
|
2023
|
+
}
|
|
2024
|
+
return cached;
|
|
2025
|
+
}
|
|
2026
|
+
function mapSlackError(error) {
|
|
2027
|
+
if (error instanceof SlackActionError) return error;
|
|
2028
|
+
const candidate = error;
|
|
2029
|
+
const apiError = candidate.data?.error;
|
|
2030
|
+
const message = candidate.message ?? "Slack action failed";
|
|
2031
|
+
const baseOptions = {
|
|
2032
|
+
apiError,
|
|
2033
|
+
statusCode: candidate.statusCode,
|
|
2034
|
+
requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
|
|
2035
|
+
errorData: serializeSlackErrorData(candidate.data),
|
|
2036
|
+
...parseSlackCanvasDetail(candidate.data?.detail)
|
|
2037
|
+
};
|
|
2038
|
+
if (apiError === "missing_scope") return new SlackActionError(message, "missing_scope", {
|
|
2039
|
+
...baseOptions,
|
|
2040
|
+
needed: candidate.data?.needed,
|
|
2041
|
+
provided: candidate.data?.provided
|
|
2042
|
+
});
|
|
2043
|
+
if (apiError === "not_in_channel") return new SlackActionError(message, "not_in_channel", baseOptions);
|
|
2044
|
+
if (apiError === "restricted_action_read_only_channel") return new SlackActionError(message, "read_only_channel", baseOptions);
|
|
2045
|
+
if (apiError === "already_reacted") return new SlackActionError(message, "already_reacted", baseOptions);
|
|
2046
|
+
if (apiError === "no_reaction") return new SlackActionError(message, "no_reaction", baseOptions);
|
|
2047
|
+
if (apiError === "invalid_arguments") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
2048
|
+
if (apiError === "invalid_cursor") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
2049
|
+
if (apiError === "invalid_name") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
2050
|
+
if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") return new SlackActionError(message, "not_found", baseOptions);
|
|
2051
|
+
if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") return new SlackActionError(message, "feature_unavailable", baseOptions);
|
|
2052
|
+
if (apiError === "canvas_creation_failed") return new SlackActionError(message, "canvas_creation_failed", baseOptions);
|
|
2053
|
+
if (apiError === "canvas_editing_failed") return new SlackActionError(message, "canvas_editing_failed", baseOptions);
|
|
2054
|
+
if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) return new SlackActionError(message, "rate_limited", {
|
|
2055
|
+
...baseOptions,
|
|
2056
|
+
retryAfterSeconds: candidate.retryAfter
|
|
2057
|
+
});
|
|
2058
|
+
return new SlackActionError(message, "internal_error", baseOptions);
|
|
2059
|
+
}
|
|
2060
|
+
const MAX_RETRY_DELAY_MS = 1e4;
|
|
2061
|
+
const MAX_TOTAL_RETRY_DELAY_MS = 2e4;
|
|
2062
|
+
const CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2063
|
+
"ECONNREFUSED",
|
|
2064
|
+
"ECONNRESET",
|
|
2065
|
+
"ENOTFOUND",
|
|
2066
|
+
"EAI_AGAIN"
|
|
2067
|
+
]);
|
|
2068
|
+
const TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2069
|
+
"ETIMEDOUT",
|
|
2070
|
+
"ECONNABORTED",
|
|
2071
|
+
"ESOCKETTIMEDOUT"
|
|
2072
|
+
]);
|
|
2073
|
+
function findNetworkErrorCode(error, depth = 0) {
|
|
2074
|
+
if (!error || typeof error !== "object" || depth > 4) return;
|
|
2075
|
+
const candidate = error;
|
|
2076
|
+
if (typeof candidate.code === "string" && (CONNECTION_ERROR_CODES.has(candidate.code) || TIMEOUT_ERROR_CODES.has(candidate.code))) return candidate.code;
|
|
2077
|
+
return findNetworkErrorCode(candidate.original, depth + 1) ?? findNetworkErrorCode(candidate.cause, depth + 1);
|
|
2078
|
+
}
|
|
2079
|
+
function hasSocketHangUpMessage(error) {
|
|
2080
|
+
return error instanceof Error && error.message.toLowerCase().includes("socket hang up");
|
|
2081
|
+
}
|
|
2082
|
+
/**
|
|
2083
|
+
* Classify a failed Slack call for retry: rate limits, Slack 5xx, and
|
|
2084
|
+
* connection-phase failures are always retryable; timeouts are retried only
|
|
2085
|
+
* for idempotent operations because Slack may already have accepted the
|
|
2086
|
+
* request.
|
|
2087
|
+
*/
|
|
2088
|
+
function classifySlackRetry(raw, mapped) {
|
|
2089
|
+
if (mapped.code === "rate_limited") return "rate_limited";
|
|
2090
|
+
if (mapped.statusCode !== void 0 && mapped.statusCode >= 500) return "server_error";
|
|
2091
|
+
const networkCode = findNetworkErrorCode(raw);
|
|
2092
|
+
if (networkCode && CONNECTION_ERROR_CODES.has(networkCode)) return "connection";
|
|
2093
|
+
if (networkCode && TIMEOUT_ERROR_CODES.has(networkCode)) return "timeout";
|
|
2094
|
+
if (hasSocketHangUpMessage(raw)) return "connection";
|
|
2095
|
+
return "none";
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Run a Slack Web API call with bounded retries so transient platform
|
|
2099
|
+
* failures do not surface as turn failures, while non-idempotent posts are
|
|
2100
|
+
* never re-sent when Slack may already have accepted them.
|
|
2101
|
+
*
|
|
2102
|
+
* Retry classes: rate limits (bounded Retry-After), connection-phase network
|
|
2103
|
+
* failures, and Slack 5xx responses always retry; request timeouts retry only
|
|
2104
|
+
* when the caller marks the operation `idempotent`.
|
|
2105
|
+
*/
|
|
2106
|
+
async function withSlackRetries(task, maxAttempts = 3, context = {}) {
|
|
2107
|
+
let attempt = 0;
|
|
2108
|
+
let totalDelayMs = 0;
|
|
2109
|
+
const action = context.action ?? "unknown";
|
|
2110
|
+
while (attempt < maxAttempts) {
|
|
2111
|
+
attempt += 1;
|
|
2112
|
+
const attemptNumber = attempt;
|
|
2113
|
+
try {
|
|
2114
|
+
return await withSpan(`POST slack.com/api/${action}`, "http.client", {}, async () => {
|
|
2115
|
+
try {
|
|
2116
|
+
return await task();
|
|
2117
|
+
} catch (error) {
|
|
2118
|
+
const mapped = mapSlackError(error);
|
|
2119
|
+
const errorAttrs = { "error.type": mapped.code };
|
|
2120
|
+
if (mapped.apiError) errorAttrs["app.slack.api_error_code"] = mapped.apiError;
|
|
2121
|
+
if (mapped.code === "rate_limited") {
|
|
2122
|
+
errorAttrs["http.response.status_code"] = 429;
|
|
2123
|
+
if (mapped.retryAfterSeconds) errorAttrs["app.slack.retry_after_ms"] = mapped.retryAfterSeconds * 1e3;
|
|
2124
|
+
} else if (mapped.statusCode != null) errorAttrs["http.response.status_code"] = mapped.statusCode;
|
|
2125
|
+
setSpanAttributes(errorAttrs);
|
|
2126
|
+
setSpanStatus("error");
|
|
2127
|
+
throw error;
|
|
2128
|
+
}
|
|
2129
|
+
}, {
|
|
2130
|
+
"http.request.method": "POST",
|
|
2131
|
+
"server.address": "slack.com",
|
|
2132
|
+
"url.scheme": "https",
|
|
2133
|
+
"url.path": `/api/${action}`,
|
|
2134
|
+
"app.slack.method": action,
|
|
2135
|
+
"app.retry.max_attempts": maxAttempts,
|
|
2136
|
+
...attemptNumber > 1 ? { "http.request.resend_count": attemptNumber - 1 } : void 0,
|
|
2137
|
+
...context.attributes ?? {},
|
|
2138
|
+
...context.spanAttributes ?? {}
|
|
2139
|
+
});
|
|
2140
|
+
} catch (error) {
|
|
2141
|
+
const mapped = mapSlackError(error);
|
|
2142
|
+
const retryClass = classifySlackRetry(error, mapped);
|
|
2143
|
+
const isRetryable = retryClass === "rate_limited" || retryClass === "connection" || retryClass === "server_error" || retryClass === "timeout" && context.idempotent === true;
|
|
2144
|
+
const remainingDelayBudgetMs = MAX_TOTAL_RETRY_DELAY_MS - totalDelayMs;
|
|
2145
|
+
const baseLogAttributes = {
|
|
2146
|
+
"app.slack.action": action,
|
|
2147
|
+
"app.slack.error_code": mapped.code,
|
|
2148
|
+
...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : void 0,
|
|
2149
|
+
...mapped.detail ? { "app.slack.detail": mapped.detail } : void 0,
|
|
2150
|
+
...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : void 0,
|
|
2151
|
+
...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : void 0,
|
|
2152
|
+
...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : void 0,
|
|
2153
|
+
...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : void 0,
|
|
2154
|
+
...context.attributes ?? {}
|
|
2155
|
+
};
|
|
2156
|
+
if (!isRetryable || attempt >= maxAttempts || remainingDelayBudgetMs <= 0) {
|
|
2157
|
+
logWarn("slack.action.failed", {
|
|
2158
|
+
...baseLogAttributes,
|
|
2159
|
+
...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : void 0
|
|
2160
|
+
});
|
|
2161
|
+
throw mapped;
|
|
2162
|
+
}
|
|
2163
|
+
logWarn("slack.action.retrying", {
|
|
2164
|
+
...baseLogAttributes,
|
|
2165
|
+
"app.slack.retry_attempt": attempt,
|
|
2166
|
+
"app.slack.retry_class": retryClass
|
|
2167
|
+
});
|
|
2168
|
+
const retryAfterMs = retryClass === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
|
|
2169
|
+
const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
|
|
2170
|
+
const delayMs = Math.min(retryAfterMs ?? backoffMs, MAX_RETRY_DELAY_MS, remainingDelayBudgetMs);
|
|
2171
|
+
totalDelayMs += delayMs;
|
|
2172
|
+
await sleep$1(delayMs);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
throw new SlackActionError("Slack action exhausted retries", "internal_error");
|
|
2176
|
+
}
|
|
2177
|
+
/**
|
|
2178
|
+
* Slack Web API client for the current destination workspace: the ambient
|
|
2179
|
+
* installation token when one is bound, otherwise the env bot token for
|
|
2180
|
+
* single-workspace deployments. Fails instead of falling back when the call
|
|
2181
|
+
* is workspace-scoped and no installation token can be resolved, so a write
|
|
2182
|
+
* never goes out with another workspace's credentials.
|
|
2183
|
+
*/
|
|
2184
|
+
function getSlackClient(options) {
|
|
2185
|
+
if (options) return new WebClient(resolveSlackToken(), {
|
|
2186
|
+
retryConfig: { retries: 0 },
|
|
2187
|
+
rejectRateLimitedCalls: true,
|
|
2188
|
+
timeout: options.timeoutMs
|
|
2189
|
+
});
|
|
2190
|
+
return getClient();
|
|
2191
|
+
}
|
|
2192
|
+
/**
|
|
2193
|
+
* Slack channel ID prefixes:
|
|
2194
|
+
* - C: channel — modern private channels also use C, so the prefix never
|
|
2195
|
+
* proves a channel is public
|
|
2196
|
+
* - G: legacy private channel / group DM
|
|
2197
|
+
* - D: direct message (1:1)
|
|
2198
|
+
*/
|
|
2199
|
+
function isDmChannel(channelId) {
|
|
2200
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
2201
|
+
return Boolean(normalized && normalized.startsWith("D"));
|
|
2202
|
+
}
|
|
2203
|
+
/**
|
|
2204
|
+
* Conversation-scoped Slack contexts backed by a concrete conversation ID.
|
|
2205
|
+
* Includes channels/groups/DMs (C/G/D).
|
|
2206
|
+
*/
|
|
2207
|
+
function isConversationScopedChannel(channelId) {
|
|
2208
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
2209
|
+
if (!normalized) return false;
|
|
2210
|
+
return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
|
|
2211
|
+
}
|
|
2212
|
+
function isConversationChannel(channelId) {
|
|
2213
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
2214
|
+
if (!normalized) return false;
|
|
2215
|
+
return normalized.startsWith("C") || normalized.startsWith("G");
|
|
2216
|
+
}
|
|
2217
|
+
async function getFilePermalink(fileId) {
|
|
2218
|
+
const client = getClient();
|
|
2219
|
+
return (await withSlackRetries(() => client.files.info({ file: fileId }), 3, {
|
|
2220
|
+
action: "files.info",
|
|
2221
|
+
idempotent: true,
|
|
2222
|
+
spanAttributes: { "app.slack.file_id": fileId }
|
|
2223
|
+
})).file?.permalink;
|
|
2224
|
+
}
|
|
2225
|
+
async function downloadPrivateSlackFile(url) {
|
|
2226
|
+
const token = resolveSlackToken();
|
|
2227
|
+
return withSpan("GET files.slack.com", "http.client", {}, async () => {
|
|
2228
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
2229
|
+
setSpanAttributes({ "http.response.status_code": response.status });
|
|
2230
|
+
if (!response.ok) {
|
|
2231
|
+
setSpanAttributes({ "error.type": String(response.status) });
|
|
2232
|
+
setSpanStatus("error");
|
|
2233
|
+
throw new Error(`Slack file download failed: ${response.status}`);
|
|
2234
|
+
}
|
|
2235
|
+
return Buffer.from(await response.arrayBuffer());
|
|
2236
|
+
}, {
|
|
2237
|
+
"http.request.method": "GET",
|
|
2238
|
+
"server.address": "files.slack.com",
|
|
2239
|
+
"url.scheme": "https",
|
|
2240
|
+
"app.slack.method": "files.download"
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2232
2243
|
// src/chat/credentials/subject.ts
|
|
2233
2244
|
const CREDENTIAL_SUBJECT_HMAC_CONTEXT = "junior.credential_subject.v1";
|
|
2234
2245
|
const CREDENTIAL_SUBJECT_SIGNATURE_VERSION = "v1";
|
|
@@ -3058,6 +3069,8 @@ function normalizeWorkspaceRecipe(input) {
|
|
|
3058
3069
|
const setupScript = input.setupScript ?? "";
|
|
3059
3070
|
if (typeof setupScript !== "string") throw new WorkspaceValidationError("Setup script must be a string.");
|
|
3060
3071
|
if (Buffer.byteLength(setupScript, "utf8") > MAX_SETUP_SCRIPT_BYTES) throw new WorkspaceValidationError(`Setup script must be at most ${MAX_SETUP_SCRIPT_BYTES} bytes.`);
|
|
3072
|
+
const vcpus = input.vcpus ?? null;
|
|
3073
|
+
if (vcpus !== null && (!Number.isSafeInteger(vcpus) || vcpus <= 0)) throw new WorkspaceValidationError("vCPUs must be a positive integer.");
|
|
3061
3074
|
if (!Array.isArray(input.repos)) throw new WorkspaceValidationError("Repositories must be an array.");
|
|
3062
3075
|
if (input.repos.length > MAX_REPOS) throw new WorkspaceValidationError(`A Workspace can include at most ${MAX_REPOS} repositories.`);
|
|
3063
3076
|
const repos = input.repos.map((entry, index) => {
|
|
@@ -3087,6 +3100,7 @@ function normalizeWorkspaceRecipe(input) {
|
|
|
3087
3100
|
return {
|
|
3088
3101
|
name,
|
|
3089
3102
|
setupScript,
|
|
3103
|
+
vcpus,
|
|
3090
3104
|
repos
|
|
3091
3105
|
};
|
|
3092
3106
|
}
|
|
@@ -3096,6 +3110,7 @@ function workspaceFromRows(row, repos, snapshot) {
|
|
|
3096
3110
|
id: row.id,
|
|
3097
3111
|
name: row.name,
|
|
3098
3112
|
setupScript: row.setupScript,
|
|
3113
|
+
vcpus: row.vcpus,
|
|
3099
3114
|
repos: repos.map((repo) => ({
|
|
3100
3115
|
provider: repo.provider,
|
|
3101
3116
|
repo: repo.repo
|
|
@@ -3194,6 +3209,7 @@ async function createWorkspace(input) {
|
|
|
3194
3209
|
id,
|
|
3195
3210
|
name: recipe.name,
|
|
3196
3211
|
setupScript: recipe.setupScript,
|
|
3212
|
+
vcpus: recipe.vcpus,
|
|
3197
3213
|
createdAt: now,
|
|
3198
3214
|
updatedAt: now
|
|
3199
3215
|
});
|
|
@@ -3220,6 +3236,7 @@ async function updateWorkspace(id, input) {
|
|
|
3220
3236
|
const rows = await db.update(juniorWorkspaces).set({
|
|
3221
3237
|
name: recipe.name,
|
|
3222
3238
|
setupScript: recipe.setupScript,
|
|
3239
|
+
vcpus: recipe.vcpus,
|
|
3223
3240
|
updatedAt: now
|
|
3224
3241
|
}).where(eq(juniorWorkspaces.id, id)).returning();
|
|
3225
3242
|
await replaceWorkspaceRepos(db, id, recipe.repos);
|
|
@@ -4332,4 +4349,4 @@ function createPluginHookRunner(input = {}) {
|
|
|
4332
4349
|
}
|
|
4333
4350
|
};
|
|
4334
4351
|
}
|
|
4335
|
-
export { bindSlackDirectCredentialSubject as $, authenticationUnlinkedEvent as $t, getWorkspaceSnapshotBuilder as A,
|
|
4352
|
+
export { bindSlackDirectCredentialSubject as $, authenticationUnlinkedEvent as $t, getWorkspaceSnapshotBuilder as A, conversationIsMemberRoot as At, STOP_WATCHING_TOOL_NAME as B, readAttachmentVisionSummary as Bt, WorkspaceValidationError as C, createPluginModel as Ct, setWorkspaceSnapshotBuild as D, conversationArchivedForEmail as Dt, setWorkspaceSnapshot as E, conversationArchiveTimes as Et, isSlackMessageTurn as F, readLatestConversationBrief as Ft, createWatch as G, listConversationAnnotations as Gt, cancelWatch as H, resolveRootVisibility as Ht, slackMessageTurnDisabledReason as I, readConversationEventPage as It, getWatch as J, resolveUserTimezone as Jt, deliverWatch as K, listConversationAnnotationsById as Kt, RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS as L, buildSentryConversationUrl as Lt, workspaceRepoCheckoutPath as M, conversationNotArchivedForUser as Mt, resolveChannelCapabilities as N, viewerConversationMembership as Nt, SANDBOX_RUNTIME as O, conversationArchivedForUser as Ot, getSlackToolContext as P, createPluginConversationEvents as Pt, bindScheduledAutomationCredentialSubject as Q, authenticationLinkedEvent as Qt, RESOURCE_SUBSCRIPTION_MAX_TTL_MS as R, getSlackFileId as Rt, updateWorkspace as S, createPluginEmbedder as St, loadSnapshotsForProfile as T, readConversationAccessFromSql as Tt, collectEventMatchKeys as U, saveObjectAnnotations as Ut, cancelSubscriptions as V, recordSlackAttachment as Vt, createTimerWatch as W, createPluginAnnotations as Wt, takeDueTimerWatches as X, JUNIOR_NATIVE_EVENT_NAMESPACE as Xt, listWatches as Y, lookupSlackUser as Yt, bindEventAutomationCredentialSubject as Z, agentsInstructionsUpdatedEvent as Zt, deleteWorkspace as _, getDashboardConversationLink as _t, getPluginRoutes as a, downloadPrivateSlackFile as at, listWorkspaces as b, setDashboardConversationLinkOptions as bt, getPluginTools as c, getSlackClient as ct, setPlugins as d, normalizeSlackConversationId as dt, conversationNoteEvent as en, verifyEventAutomationCredentialSubject as et, validatePlugins as f, runWithSlackInstallationToken as ft, createWorkspace as g, JUNIOR_THREAD_STATE_TTL_MS as gt, setBriefsConfig as h, runWithWorkspaceTeamId as ht, getPluginApiRoutes as i, SlackActionError as it, tryWorkspaceRepoCheckoutPath as j, conversationNotArchivedForEmail as jt, deleteWorkspaceSnapshotBuilders as k, conversationHasParticipantUser as kt, getPluginUserPromptContributions as l, isConversationScopedChannel as lt, registration_exports as m, getWorkspaceTeamId as mt, applyPluginFormatMarkdown as n, verifySlackDirectCredentialSubject as nt, getPluginSlackConversationLink as o, getFilePermalink as ot, coreTaskRegistrations as p, withSlackRetries as pt, findMatchingWatches as q, defaultTimezone as qt, createPluginHookRunner as r, verifyTimerWatchCredentialSubject as rt, getPluginSystemPromptContributions as s, getHeaderString as st, agent_hooks_exports as t, verifyScheduledAutomationCredentialSubject as tt, getPlugins as u, isDmChannel as ut, getWorkspace as v, getDashboardObjectIconLink as vt, invalidateReadySnapshot as w, createPluginLogger as wt, listWorkspacesByRepository as x, createPluginState as xt, getWorkspaceByName as y, getDashboardTaskLink as yt, RESOURCE_WATCH_TOOL_SOURCE as z, matchSlackAttachments as zt };
|