@sentry/junior 0.258.0 → 0.260.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-Dj5BTJRd.js +270 -0
- package/dist/{admin-My1SLGVO.js → admin-tzkZDqrH.js} +1 -1
- package/dist/{agent-hooks-BJ_5mlRH.js → agent-hooks-V4Hfc2tB.js} +711 -697
- package/dist/{agent-runner-DGsFQw72.js → agent-runner-DvTUyGTY.js} +751 -2508
- package/dist/api/schema.d.ts +239 -54
- package/dist/api/schema.js +4 -4
- package/dist/api.js +218 -57
- package/dist/{app-PX2H1PxU.d.ts → app-BsBUs3rF.d.ts} +6 -3
- package/dist/app.d.ts +2 -2
- package/dist/app.js +31 -31
- package/dist/{assistant-reply-H1M-91oF.js → assistant-reply-CEOMzsWy.js} +1 -1
- package/dist/{automation-DWBFBenG.js → automation-DrpcA-SX.js} +3 -0
- package/dist/{briefs-vYoK85rJ.js → briefs-BSCkyYqs.js} +4 -4
- package/dist/{catalog-runtime-BN4DVxco.js → catalog-runtime-Bpcs2b2L.js} +1 -1
- package/dist/cli/chat.d.ts +1 -1
- package/dist/cli/chat.js +16 -16
- package/dist/cli/check.js +3 -3
- package/dist/cli/main.js +6 -2
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.js +4 -4
- package/dist/cli/run.d.ts +2 -1
- package/dist/cli/run.js +2 -1
- package/dist/cli/snapshot-create.js +4 -3
- package/dist/cli/upgrade.d.ts +1 -1
- package/dist/cli/upgrade.js +3 -3
- package/dist/{client-DbyHuMFU.js → client-JeyzRwsT.js} +57 -4
- package/dist/{config-_WO_AjCB.js → config-ULs6Zl9b.js} +5 -2
- package/dist/{connections-ag_1JOhB.js → connections-CZ7HhEmN.js} +174 -10
- package/dist/{conversation-DjN3mlog.js → conversation-CvuKJ3tA.js} +108 -155
- package/dist/{conversation-privacy-DHP_xnVj.js → conversation-privacy-BQSlZXgV.js} +1 -1
- package/dist/{credential-sync-B-CPX_ag.js → credential-sync-BKcv5rBe.js} +14 -177
- package/dist/{executor-RvG6kVe1.js → executor-BoGNVEDn.js} +3 -2
- package/dist/{images-C2J_bj2g.js → images-BBmDXQCB.js} +3 -3
- package/dist/instrumentation.js +2 -2
- package/dist/{job-queue-DVe37it2.js → job-queue-CbdVyaCo.js} +1 -1
- package/dist/{logging-DsHONhVm.js → logging-DSMmkHXG.js} +1 -1
- package/dist/{message-DyitL8no.js → message-zaj5gBbS.js} +3 -3
- package/dist/nitro.d.ts +2 -2
- package/dist/nitro.js +3 -3
- package/dist/{oauth-callback-server-CDby-pCn.js → oauth-callback-server-yYFG8Own.js} +2 -2
- package/dist/{plugins-B11Mfkmv.d.ts → plugins-B_0Cr0MO.d.ts} +5 -1
- package/dist/{profile-DpYjfKke.js → profile-ByWkAY7L.js} +24 -293
- package/dist/{prompt-xgpyMtg7.js → prompt-DeVax2qO.js} +1 -1
- package/dist/{registry-Du566MtJ.js → registry-BL-osYWN.js} +41 -2
- package/dist/{resolve-ClCIgh8E.js → resolve-HuPG2VMB.js} +9 -4
- package/dist/{result-BBbKA61B.js → result-DS7V4a3-.js} +9 -14
- package/dist/{runner-CCuqFSqx.js → runner-Cba_OfGl.js} +11 -11
- package/dist/{sandbox-egress-signals-DY1mbKF4.js → sandbox-egress-signals-C7NcPzea.js} +1 -1
- package/dist/{schema-DrMh3NtR.js → schema-OzL8Zw4H.js} +82 -3
- package/dist/service-account-DqwHhwJA.js +120 -0
- package/dist/service-account-Ko1Xs--F.js +661 -0
- package/dist/{session-stop-CIoFiwwe.js → session-stop-HfKx-pky.js} +180 -22
- package/dist/{skills-DyQBLIrK.js → skills-D6_bcAuW.js} +2 -2
- package/dist/{spawn-Dqfeo7XP.js → spawn-BWqK6EwM.js} +2 -2
- package/dist/{store-DJLwq4x8.js → store-DzRbd5VT.js} +6 -6
- package/dist/{task-queue-BzW4MaWU.js → task-queue-9LICOzcL.js} +3 -3
- package/dist/{task-runner-D3qJh4fK.js → task-runner-DGnd_B5X.js} +10 -10
- package/dist/{tasks-CEXKTASS.js → tasks-CjrS098O.js} +4928 -2389
- package/dist/thread-state-CqkYowEz.js +184 -0
- package/dist/{turn-failure-response-CWx7aNIh.js → turn-failure-response-BkKmGqFt.js} +8 -9
- package/dist/{turn-lifecycle-C9JFst7-.js → turn-lifecycle-DYv8IV6w.js} +1 -1
- package/dist/{turn-session-routing-CUxtfpr7.js → turn-session-routing-TaVhKmdz.js} +1 -1
- package/dist/{validation-BHRxa5G4.js → validation-BrXMYPyo.js} +1 -1
- package/dist/{version-C0U6rHg0.js → version-Ct049vav.js} +1 -1
- package/dist/version.js +1 -1
- package/dist/{viewer-CGvvRqKp.js → viewer-C-J0ewlH.js} +19 -10
- package/dist/{web-input-Keb5GRWj.js → web-input-C_xB3RXp.js} +41 -37
- package/dist/{work-DRcsJ3hV.js → work-D829Ue7k.js} +9 -10
- package/dist/{worker-Cjo-QqPq.js → worker-BRTYzlpk.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 +6 -6
- package/dist/experimental-BNK1l1jv.js +0 -49
- package/dist/oauth-authorization-djQn5Nhy.js +0 -97
- package/dist/pending-auth-D8J1uJUS.js +0 -442
- package/dist/thread-state-W3f6kAtz.js +0 -426
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-nwUcYXjP.js";
|
|
2
|
-
import {
|
|
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-JeyzRwsT.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-DSMmkHXG.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-BQSlZXgV.js";
|
|
6
|
+
import { i as getChatConfig, n as botConfig, o as getSlackBotToken } from "./config-ULs6Zl9b.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-C-J0ewlH.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-BoGNVEDn.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 { C as getDefaultRedisStateAdapterFor, N as SANDBOX_WORKSPACE_ROOT, k as sleep$1, l as isSandboxMissingError, n as hash, v as getVercelSandboxCredentials, w as getStateAdapter, x as runNonInteractiveCommand } from "./profile-DpYjfKke.js";
|
|
13
|
+
import { h as credentialSubjectSchema } from "./registry-BL-osYWN.js";
|
|
16
14
|
import { i as isValidTimeZone } from "./cadence-R7Em4RjE.js";
|
|
17
|
-
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-Dj5BTJRd.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-ByWkAY7L.js";
|
|
18
19
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
19
20
|
import { z } from "zod";
|
|
20
21
|
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -156,731 +157,382 @@ function renderJuniorNativeConversationEvent(args) {
|
|
|
156
157
|
const presentation = definition.renderEvent(definition.parse(args.content));
|
|
157
158
|
return presentation === void 0 ? void 0 : conversationEventPresentationSchema.parse(presentation);
|
|
158
159
|
}
|
|
159
|
-
// src/chat/slack/
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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}`;
|
|
169
176
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
statusCode;
|
|
177
|
-
requestId;
|
|
178
|
-
errorData;
|
|
179
|
-
retryAfterSeconds;
|
|
180
|
-
detail;
|
|
181
|
-
detailLine;
|
|
182
|
-
detailRule;
|
|
183
|
-
constructor(message, code, options = {}) {
|
|
184
|
-
super(message);
|
|
185
|
-
this.name = "SlackActionError";
|
|
186
|
-
this.code = code;
|
|
187
|
-
this.apiError = options.apiError;
|
|
188
|
-
this.needed = options.needed;
|
|
189
|
-
this.provided = options.provided;
|
|
190
|
-
this.statusCode = options.statusCode;
|
|
191
|
-
this.requestId = options.requestId;
|
|
192
|
-
this.errorData = options.errorData;
|
|
193
|
-
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
194
|
-
this.detail = options.detail;
|
|
195
|
-
this.detailLine = options.detailLine;
|
|
196
|
-
this.detailRule = options.detailRule;
|
|
197
|
-
}
|
|
198
|
-
};
|
|
199
|
-
function serializeSlackErrorData(data) {
|
|
200
|
-
if (!data || typeof data !== "object") return;
|
|
201
|
-
const filtered = Object.fromEntries(Object.entries(data).filter(([key]) => key !== "error"));
|
|
202
|
-
if (Object.keys(filtered).length === 0) return;
|
|
203
|
-
try {
|
|
204
|
-
const serialized = JSON.stringify(filtered);
|
|
205
|
-
return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
|
|
206
|
-
} catch {
|
|
207
|
-
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;
|
|
208
183
|
}
|
|
184
|
+
return hit.value;
|
|
209
185
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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;
|
|
221
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;
|
|
222
227
|
}
|
|
223
228
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const lineMatch = trimmed.match(/line\s+(\d+):/i);
|
|
230
|
-
if (lineMatch) {
|
|
231
|
-
const line = Number.parseInt(lineMatch[1] ?? "", 10);
|
|
232
|
-
if (Number.isFinite(line)) parsed.detailLine = line;
|
|
233
|
-
}
|
|
234
|
-
if (/unsupported heading depth/i.test(trimmed)) parsed.detailRule = "unsupported_heading_depth";
|
|
235
|
-
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;
|
|
236
234
|
}
|
|
237
235
|
/**
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
*/
|
|
242
|
-
const installationTokenStorage = new AsyncLocalStorage();
|
|
243
|
-
const clientsByToken = /* @__PURE__ */ new Map();
|
|
244
|
-
/**
|
|
245
|
-
* Bind a workspace installation token for all Slack Web API calls inside
|
|
246
|
-
* `fn`, so multi-workspace outbound writes use the destination workspace's
|
|
247
|
-
* 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.
|
|
248
239
|
*/
|
|
249
|
-
function
|
|
250
|
-
const
|
|
251
|
-
if (
|
|
252
|
-
|
|
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();
|
|
253
247
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const teamId = getWorkspaceTeamId();
|
|
265
|
-
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");
|
|
266
|
-
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
|
+
};
|
|
267
258
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
+
};
|
|
271
292
|
}
|
|
272
|
-
/**
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
*/
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
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]);
|
|
288
308
|
}
|
|
289
|
-
return
|
|
309
|
+
return byConversation;
|
|
290
310
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
needed: candidate.data?.needed,
|
|
306
|
-
provided: candidate.data?.provided
|
|
307
|
-
});
|
|
308
|
-
if (apiError === "not_in_channel") return new SlackActionError(message, "not_in_channel", baseOptions);
|
|
309
|
-
if (apiError === "restricted_action_read_only_channel") return new SlackActionError(message, "read_only_channel", baseOptions);
|
|
310
|
-
if (apiError === "already_reacted") return new SlackActionError(message, "already_reacted", baseOptions);
|
|
311
|
-
if (apiError === "no_reaction") return new SlackActionError(message, "no_reaction", baseOptions);
|
|
312
|
-
if (apiError === "invalid_arguments") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
313
|
-
if (apiError === "invalid_cursor") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
314
|
-
if (apiError === "invalid_name") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
315
|
-
if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") return new SlackActionError(message, "not_found", baseOptions);
|
|
316
|
-
if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") return new SlackActionError(message, "feature_unavailable", baseOptions);
|
|
317
|
-
if (apiError === "canvas_creation_failed") return new SlackActionError(message, "canvas_creation_failed", baseOptions);
|
|
318
|
-
if (apiError === "canvas_editing_failed") return new SlackActionError(message, "canvas_editing_failed", baseOptions);
|
|
319
|
-
if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) return new SlackActionError(message, "rate_limited", {
|
|
320
|
-
...baseOptions,
|
|
321
|
-
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
|
+
}
|
|
322
325
|
});
|
|
323
|
-
return
|
|
326
|
+
return annotations.map((annotation) => ({
|
|
327
|
+
...annotation,
|
|
328
|
+
plugin
|
|
329
|
+
}));
|
|
324
330
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (typeof candidate.code === "string" && (CONNECTION_ERROR_CODES.has(candidate.code) || TIMEOUT_ERROR_CODES.has(candidate.code))) return candidate.code;
|
|
342
|
-
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;
|
|
343
347
|
}
|
|
344
|
-
|
|
345
|
-
|
|
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;
|
|
346
365
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
function classifySlackRetry(raw, mapped) {
|
|
354
|
-
if (mapped.code === "rate_limited") return "rate_limited";
|
|
355
|
-
if (mapped.statusCode !== void 0 && mapped.statusCode >= 500) return "server_error";
|
|
356
|
-
const networkCode = findNetworkErrorCode(raw);
|
|
357
|
-
if (networkCode && CONNECTION_ERROR_CODES.has(networkCode)) return "connection";
|
|
358
|
-
if (networkCode && TIMEOUT_ERROR_CODES.has(networkCode)) return "timeout";
|
|
359
|
-
if (hasSocketHangUpMessage(raw)) return "connection";
|
|
360
|
-
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
|
+
};
|
|
361
372
|
}
|
|
362
373
|
/**
|
|
363
|
-
*
|
|
364
|
-
* failures do not surface as turn failures, while non-idempotent posts are
|
|
365
|
-
* never re-sent when Slack may already have accepted them.
|
|
374
|
+
* Resolve a conversation's privacy authority from its persisted root.
|
|
366
375
|
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
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.
|
|
370
379
|
*/
|
|
371
|
-
async function
|
|
372
|
-
|
|
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
|
-
"url.scheme": "https",
|
|
398
|
-
"url.path": `/api/${action}`,
|
|
399
|
-
"app.slack.method": action,
|
|
400
|
-
"app.retry.max_attempts": maxAttempts,
|
|
401
|
-
...attemptNumber > 1 ? { "http.request.resend_count": attemptNumber - 1 } : void 0,
|
|
402
|
-
...context.attributes ?? {},
|
|
403
|
-
...context.spanAttributes ?? {}
|
|
404
|
-
});
|
|
405
|
-
} catch (error) {
|
|
406
|
-
const mapped = mapSlackError(error);
|
|
407
|
-
const retryClass = classifySlackRetry(error, mapped);
|
|
408
|
-
const isRetryable = retryClass === "rate_limited" || retryClass === "connection" || retryClass === "server_error" || retryClass === "timeout" && context.idempotent === true;
|
|
409
|
-
const remainingDelayBudgetMs = MAX_TOTAL_RETRY_DELAY_MS - totalDelayMs;
|
|
410
|
-
const baseLogAttributes = {
|
|
411
|
-
"app.slack.action": action,
|
|
412
|
-
"app.slack.error_code": mapped.code,
|
|
413
|
-
...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : void 0,
|
|
414
|
-
...mapped.detail ? { "app.slack.detail": mapped.detail } : void 0,
|
|
415
|
-
...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : void 0,
|
|
416
|
-
...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : void 0,
|
|
417
|
-
...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : void 0,
|
|
418
|
-
...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : void 0,
|
|
419
|
-
...context.attributes ?? {}
|
|
420
|
-
};
|
|
421
|
-
if (!isRetryable || attempt >= maxAttempts || remainingDelayBudgetMs <= 0) {
|
|
422
|
-
logWarn("slack.action.failed", {
|
|
423
|
-
...baseLogAttributes,
|
|
424
|
-
...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : void 0
|
|
425
|
-
});
|
|
426
|
-
throw mapped;
|
|
427
|
-
}
|
|
428
|
-
logWarn("slack.action.retrying", {
|
|
429
|
-
...baseLogAttributes,
|
|
430
|
-
"app.slack.retry_attempt": attempt,
|
|
431
|
-
"app.slack.retry_class": retryClass
|
|
432
|
-
});
|
|
433
|
-
const retryAfterMs = retryClass === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
|
|
434
|
-
const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
|
|
435
|
-
const delayMs = Math.min(retryAfterMs ?? backoffMs, MAX_RETRY_DELAY_MS, remainingDelayBudgetMs);
|
|
436
|
-
totalDelayMs += delayMs;
|
|
437
|
-
await sleep$1(delayMs);
|
|
438
|
-
}
|
|
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
|
+
};
|
|
439
406
|
}
|
|
440
|
-
|
|
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)));
|
|
441
412
|
}
|
|
442
|
-
/**
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
* is workspace-scoped and no installation token can be resolved, so a write
|
|
447
|
-
* never goes out with another workspace's credentials.
|
|
448
|
-
*/
|
|
449
|
-
function getSlackClient(options) {
|
|
450
|
-
if (options) return new WebClient(resolveSlackToken(), {
|
|
451
|
-
retryConfig: { retries: 0 },
|
|
452
|
-
rejectRateLimitedCalls: true,
|
|
453
|
-
timeout: options.timeoutMs
|
|
454
|
-
});
|
|
455
|
-
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;
|
|
456
417
|
}
|
|
457
|
-
/**
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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)));
|
|
467
428
|
}
|
|
468
|
-
/**
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const normalized = normalizeSlackConversationId(channelId);
|
|
474
|
-
if (!normalized) return false;
|
|
475
|
-
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];
|
|
476
434
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
+
}
|
|
481
458
|
}
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
return (
|
|
485
|
-
action: "files.info",
|
|
486
|
-
idempotent: true,
|
|
487
|
-
spanAttributes: { "app.slack.file_id": fileId }
|
|
488
|
-
})).file?.permalink;
|
|
459
|
+
function slackFileIds(meta) {
|
|
460
|
+
const ids = meta?.slackFileIds ?? meta?.imageFileIds;
|
|
461
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
489
462
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
return
|
|
493
|
-
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
494
|
-
setSpanAttributes({ "http.response.status_code": response.status });
|
|
495
|
-
if (!response.ok) {
|
|
496
|
-
setSpanAttributes({ "error.type": String(response.status) });
|
|
497
|
-
setSpanStatus("error");
|
|
498
|
-
throw new Error(`Slack file download failed: ${response.status}`);
|
|
499
|
-
}
|
|
500
|
-
return Buffer.from(await response.arrayBuffer());
|
|
501
|
-
}, {
|
|
502
|
-
"http.request.method": "GET",
|
|
503
|
-
"server.address": "files.slack.com",
|
|
504
|
-
"url.scheme": "https",
|
|
505
|
-
"app.slack.method": "files.download"
|
|
506
|
-
});
|
|
463
|
+
// src/chat/sentry-links.ts
|
|
464
|
+
function getSentryOrgSlug() {
|
|
465
|
+
return process.env.SENTRY_ORG_SLUG?.trim() || void 0;
|
|
507
466
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
ok: z.literal(true),
|
|
511
|
-
user: z.object({
|
|
512
|
-
name: z.string().nullish(),
|
|
513
|
-
tz: z.string().nullish(),
|
|
514
|
-
profile: z.object({
|
|
515
|
-
email: z.string().nullish(),
|
|
516
|
-
real_name: z.string().nullish()
|
|
517
|
-
}).optional()
|
|
518
|
-
})
|
|
519
|
-
});
|
|
520
|
-
const USER_CACHE_TTL_MS = 3e5;
|
|
521
|
-
const userCache = /* @__PURE__ */ new Map();
|
|
522
|
-
function userCacheKey(teamId, userId) {
|
|
523
|
-
return `${teamId}:${userId}`;
|
|
467
|
+
function isSentrySaasDsnHost(host) {
|
|
468
|
+
return host === "sentry.io" || host.endsWith(".sentry.io");
|
|
524
469
|
}
|
|
525
|
-
function
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
return null;
|
|
531
|
-
}
|
|
532
|
-
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}`;
|
|
533
475
|
}
|
|
534
|
-
function
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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 }
|
|
538
497
|
});
|
|
539
498
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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
|
+
};
|
|
556
517
|
}
|
|
557
|
-
const parsed = slackUserInfoSchema.safeParse(await response.json());
|
|
558
|
-
if (!parsed.success) return null;
|
|
559
|
-
const payload = parsed.data;
|
|
560
|
-
const result = {
|
|
561
|
-
userName: payload.user.name?.trim() || void 0,
|
|
562
|
-
fullName: payload.user.profile?.real_name?.trim() || void 0,
|
|
563
|
-
email: payload.user.profile?.email?.trim() || void 0,
|
|
564
|
-
timezone: payload.user.tz?.trim() || void 0
|
|
565
|
-
};
|
|
566
|
-
writeToCache(teamId, userId, result);
|
|
567
|
-
return result;
|
|
568
|
-
} catch (error) {
|
|
569
|
-
logWarn("slack.user.lookup.failed", {
|
|
570
|
-
"app.slack.user_id": userId,
|
|
571
|
-
"app.slack.team_id": teamId,
|
|
572
|
-
"exception.message": error instanceof Error ? error.message : String(error)
|
|
573
|
-
});
|
|
574
|
-
return null;
|
|
575
518
|
}
|
|
519
|
+
return current;
|
|
576
520
|
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
/** The install timezone, used when Junior does not know a person's timezone. */
|
|
580
|
-
function defaultTimezone() {
|
|
581
|
-
return process.env.JUNIOR_TIMEZONE?.trim() || DEFAULT_TIMEZONE;
|
|
582
|
-
}
|
|
583
|
-
/**
|
|
584
|
-
* Pick a person's timezone: their Slack profile, then the saved user
|
|
585
|
-
* timezone, then the install default. A valid Slack timezone is saved on the
|
|
586
|
-
* linked user, so later lookups without Slack still find it.
|
|
587
|
-
*/
|
|
588
|
-
async function resolveUserTimezone(args) {
|
|
589
|
-
const timezone = (args.slack ? await lookupSlackUser(args.slack.teamId, args.slack.userId) : void 0)?.timezone;
|
|
590
|
-
if (timezone && isValidTimeZone(timezone)) {
|
|
591
|
-
if (args.userId) await saveUserTimezone(args.db, args.userId, timezone, args.nowMs);
|
|
592
|
-
return timezone;
|
|
593
|
-
}
|
|
594
|
-
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;
|
|
595
523
|
}
|
|
596
|
-
|
|
597
|
-
function
|
|
598
|
-
|
|
599
|
-
|
|
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);
|
|
600
531
|
return {
|
|
601
|
-
...
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
function createPluginAnnotations(args) {
|
|
608
|
-
return {
|
|
609
|
-
async upsert(input) {
|
|
610
|
-
const annotation = conversationAnnotationInputSchema.parse(input);
|
|
611
|
-
const now = /* @__PURE__ */ new Date();
|
|
612
|
-
await args.db.insert(juniorConversationAnnotations).values({
|
|
613
|
-
annotation,
|
|
614
|
-
conversationId: args.conversationId,
|
|
615
|
-
createdAt: now,
|
|
616
|
-
key: annotation.key,
|
|
617
|
-
kind: annotation.kind,
|
|
618
|
-
plugin: args.plugin,
|
|
619
|
-
updatedAt: now
|
|
620
|
-
}).onConflictDoUpdate({
|
|
621
|
-
target: [
|
|
622
|
-
juniorConversationAnnotations.conversationId,
|
|
623
|
-
juniorConversationAnnotations.plugin,
|
|
624
|
-
juniorConversationAnnotations.kind,
|
|
625
|
-
juniorConversationAnnotations.key
|
|
626
|
-
],
|
|
627
|
-
set: {
|
|
628
|
-
annotation,
|
|
629
|
-
updatedAt: now
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
},
|
|
633
|
-
async remove(kind, key) {
|
|
634
|
-
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)));
|
|
635
|
-
},
|
|
636
|
-
async list() {
|
|
637
|
-
return (await listConversationAnnotations(args.db, args.conversationId)).filter((a) => a.plugin === args.plugin);
|
|
638
|
-
}
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
/** Load stored annotations for one conversation, newest update first. */
|
|
642
|
-
async function listConversationAnnotations(db, conversationId) {
|
|
643
|
-
return (await listConversationAnnotationsById(db, [conversationId])).get(conversationId) ?? [];
|
|
644
|
-
}
|
|
645
|
-
/** Load stored annotations for a bounded set of conversations. */
|
|
646
|
-
async function listConversationAnnotationsById(db, conversationIds) {
|
|
647
|
-
const byConversation = /* @__PURE__ */ new Map();
|
|
648
|
-
if (conversationIds.length === 0) return byConversation;
|
|
649
|
-
const rows = await db.select().from(juniorConversationAnnotations).where(inArray(juniorConversationAnnotations.conversationId, [...conversationIds])).orderBy(desc(juniorConversationAnnotations.updatedAt));
|
|
650
|
-
for (const row of rows) {
|
|
651
|
-
const annotation = annotationFromRow(row);
|
|
652
|
-
if (!annotation) continue;
|
|
653
|
-
const current = byConversation.get(row.conversationId);
|
|
654
|
-
if (current) current.push(annotation);
|
|
655
|
-
else byConversation.set(row.conversationId, [annotation]);
|
|
656
|
-
}
|
|
657
|
-
return byConversation;
|
|
658
|
-
}
|
|
659
|
-
// src/chat/conversations/annotation-results.ts
|
|
660
|
-
/** Save object annotations and retain returned facts in the tool result. */
|
|
661
|
-
async function saveObjectAnnotations(conversationId, plugin, value) {
|
|
662
|
-
const annotations = objectAnnotationSchema.array().parse(value);
|
|
663
|
-
await getDb().transaction(async (db) => {
|
|
664
|
-
const store = createPluginAnnotations({
|
|
665
|
-
conversationId,
|
|
666
|
-
plugin,
|
|
667
|
-
db
|
|
668
|
-
});
|
|
669
|
-
for (const annotation of annotations) {
|
|
670
|
-
await store.upsert(annotation);
|
|
671
|
-
await store.remove("resource_link", annotation.key);
|
|
672
|
-
}
|
|
673
|
-
});
|
|
674
|
-
return annotations.map((annotation) => ({
|
|
675
|
-
...annotation,
|
|
676
|
-
plugin
|
|
677
|
-
}));
|
|
678
|
-
}
|
|
679
|
-
/** Save annotations from a successful plugin tool result and attach their cards. */
|
|
680
|
-
async function annotateToolResult(conversationId, plugin, result) {
|
|
681
|
-
if (!isRecord$1(result) || result.isError === true) return result;
|
|
682
|
-
const details = isRecord$1(result.details) ? result.details : result;
|
|
683
|
-
if (details.timed_out === true || details.isError === true || !Array.isArray(details.objectAnnotations)) return result;
|
|
684
|
-
const cards = await saveObjectAnnotations(conversationId, plugin, details.objectAnnotations);
|
|
685
|
-
const { objectAnnotations: _annotations, ...rest } = details;
|
|
686
|
-
const annotated = {
|
|
687
|
-
...rest,
|
|
688
|
-
objectCards: cards
|
|
689
|
-
};
|
|
690
|
-
if (isRecord$1(result.details)) return {
|
|
691
|
-
...result,
|
|
692
|
-
details: annotated
|
|
693
|
-
};
|
|
694
|
-
return annotated;
|
|
695
|
-
}
|
|
696
|
-
// src/chat/conversations/sql/privacy.ts
|
|
697
|
-
const privacyRoot = alias(juniorConversations, "privacy_root");
|
|
698
|
-
/** Accept only a structurally valid persisted root as privacy authority. */
|
|
699
|
-
async function readRootCandidate(executor, conversationId) {
|
|
700
|
-
const row = (await executor.db().select({
|
|
701
|
-
requestedConversationId: juniorConversations.conversationId,
|
|
702
|
-
requestedParentConversationId: juniorConversations.parentConversationId,
|
|
703
|
-
requestedRootConversationId: juniorConversations.rootConversationId,
|
|
704
|
-
rootConversationId: privacyRoot.conversationId,
|
|
705
|
-
rootDestinationId: privacyRoot.destinationId,
|
|
706
|
-
rootParentConversationId: privacyRoot.parentConversationId,
|
|
707
|
-
rootRootConversationId: privacyRoot.rootConversationId
|
|
708
|
-
}).from(juniorConversations).leftJoin(privacyRoot, eq(privacyRoot.conversationId, juniorConversations.rootConversationId)).where(eq(juniorConversations.conversationId, conversationId)))[0];
|
|
709
|
-
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 ? {
|
|
710
|
-
destinationId: row.rootDestinationId,
|
|
711
|
-
rootConversationId: row.rootConversationId
|
|
712
|
-
} : void 0;
|
|
713
|
-
}
|
|
714
|
-
async function readCandidateVisibility(executor, candidate) {
|
|
715
|
-
const destinations = await executor.db().select({ visibility: juniorDestinations.visibility }).from(juniorDestinations).where(eq(juniorDestinations.id, candidate.destinationId)).for("share");
|
|
716
|
-
return {
|
|
717
|
-
rootConversationId: candidate.rootConversationId,
|
|
718
|
-
visibility: destinations[0]?.visibility ?? null
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Resolve a conversation's privacy authority from its persisted root.
|
|
723
|
-
*
|
|
724
|
-
* Missing or structurally invalid root metadata fails closed. The root
|
|
725
|
-
* destination is locked so callers that keep the transaction open receive a
|
|
726
|
-
* stable visibility decision.
|
|
727
|
-
*/
|
|
728
|
-
async function resolveRootVisibility(executor, conversationId) {
|
|
729
|
-
const candidate = await readRootCandidate(executor, conversationId);
|
|
730
|
-
if (!candidate) return {
|
|
731
|
-
rootConversationId: conversationId,
|
|
732
|
-
visibility: null
|
|
733
|
-
};
|
|
734
|
-
return readCandidateVisibility(executor, candidate);
|
|
735
|
-
}
|
|
736
|
-
// src/chat/briefs/events.ts
|
|
737
|
-
/** Durable cost and size record for one stored Brief version. */
|
|
738
|
-
const briefUpdatedEvent = defineConversationEvent({
|
|
739
|
-
name: "brief_updated",
|
|
740
|
-
version: 1,
|
|
741
|
-
schema: z.object({
|
|
742
|
-
version: z.number().int().positive(),
|
|
743
|
-
modelId: z.string().min(1),
|
|
744
|
-
costUsd: z.number().finite().nonnegative().optional(),
|
|
745
|
-
decisions: z.number().int().nonnegative(),
|
|
746
|
-
openDecisions: z.number().int().nonnegative(),
|
|
747
|
-
links: z.number().int().nonnegative()
|
|
748
|
-
}).strict(),
|
|
749
|
-
renderEvent(event) {
|
|
750
|
-
return {
|
|
751
|
-
icon: "activity",
|
|
752
|
-
title: `Brief updated (v${event.version})`
|
|
753
|
-
};
|
|
754
|
-
}
|
|
755
|
-
});
|
|
756
|
-
// src/chat/slack/attachments.ts
|
|
757
|
-
/** Save the vision summary for a stored Slack attachment. */
|
|
758
|
-
async function recordSlackAttachment(args) {
|
|
759
|
-
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)));
|
|
760
|
-
}
|
|
761
|
-
/** Read a saved vision summary for one live attachment. */
|
|
762
|
-
async function readAttachmentVisionSummary(args) {
|
|
763
|
-
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)));
|
|
764
|
-
return row?.visionSummary;
|
|
765
|
-
}
|
|
766
|
-
/** Match live conversation attachments to Slack file ids. */
|
|
767
|
-
async function matchSlackAttachments(args) {
|
|
768
|
-
if (args.providerIds.length === 0) return [];
|
|
769
|
-
return await args.db.db().select({
|
|
770
|
-
id: juniorAttachments.id,
|
|
771
|
-
providerId: juniorAttachments.providerId,
|
|
772
|
-
bytes: juniorAttachments.bytes,
|
|
773
|
-
contentType: juniorAttachments.contentType,
|
|
774
|
-
filename: juniorAttachments.filename
|
|
775
|
-
}).from(juniorAttachments).where(and(eq(juniorAttachments.conversationId, args.conversationId), eq(juniorAttachments.provider, "slack"), inArray(juniorAttachments.providerId, args.providerIds), isNull(juniorAttachments.deleteRequestedAt)));
|
|
776
|
-
}
|
|
777
|
-
/** Read the Slack file identity retained by the Chat SDK. */
|
|
778
|
-
function getSlackFileId(attachment) {
|
|
779
|
-
const metadataId = attachment.fetchMetadata?.fileId;
|
|
780
|
-
if (metadataId) return metadataId;
|
|
781
|
-
return (attachment.url ?? attachment.fetchMetadata?.url ?? "").match(/(?:^|[-/])(F[A-Z0-9]+)(?:[-/]|$)/i)?.[1];
|
|
782
|
-
}
|
|
783
|
-
// src/api/conversations/message-attachments.ts
|
|
784
|
-
/** Resolve saved Slack file references without exposing private storage details. */
|
|
785
|
-
async function hydrateMessageAttachments(db, conversationId, events) {
|
|
786
|
-
const messages = events.flatMap((event) => event.data.type === "message" ? [event.data] : []);
|
|
787
|
-
const ids = [...new Set(messages.flatMap((message) => slackFileIds(message.meta)))];
|
|
788
|
-
if (!ids.length) return;
|
|
789
|
-
const attachments = await matchSlackAttachments({
|
|
790
|
-
db,
|
|
791
|
-
conversationId,
|
|
792
|
-
providerIds: ids
|
|
793
|
-
});
|
|
794
|
-
const byFileId = new Map(attachments.map(({ providerId, ...attachment }) => [providerId, attachment]));
|
|
795
|
-
for (const message of messages) {
|
|
796
|
-
const stored = readMessageAttachments(message.meta?.attachments);
|
|
797
|
-
const matched = slackFileIds(message.meta).flatMap((id) => {
|
|
798
|
-
const attachment = byFileId.get(id);
|
|
799
|
-
return attachment ? [attachment] : [];
|
|
800
|
-
});
|
|
801
|
-
if (matched.length) message.meta = {
|
|
802
|
-
...message.meta,
|
|
803
|
-
attachments: [...stored, ...matched]
|
|
804
|
-
};
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
function slackFileIds(meta) {
|
|
808
|
-
const ids = meta?.slackFileIds ?? meta?.imageFileIds;
|
|
809
|
-
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
810
|
-
}
|
|
811
|
-
// src/chat/sentry-links.ts
|
|
812
|
-
function getSentryOrgSlug() {
|
|
813
|
-
return process.env.SENTRY_ORG_SLUG?.trim() || void 0;
|
|
814
|
-
}
|
|
815
|
-
function isSentrySaasDsnHost(host) {
|
|
816
|
-
return host === "sentry.io" || host.endsWith(".sentry.io");
|
|
817
|
-
}
|
|
818
|
-
function buildSentryWebBaseUrl(dsn) {
|
|
819
|
-
if (isSentrySaasDsnHost(dsn.host)) return "https://sentry.io";
|
|
820
|
-
const port = dsn.port ? `:${dsn.port}` : "";
|
|
821
|
-
const path = dsn.path ? `/${dsn.path}` : "";
|
|
822
|
-
return `${dsn.protocol}://${dsn.host}${port}${path}`;
|
|
823
|
-
}
|
|
824
|
-
function buildSentryOrgPath(args) {
|
|
825
|
-
const dsn = getClient$1()?.getDsn();
|
|
826
|
-
if (!dsn?.host || !dsn.projectId) return;
|
|
827
|
-
const orgSlug = getSentryOrgSlug();
|
|
828
|
-
if (!orgSlug) return;
|
|
829
|
-
const params = new URLSearchParams();
|
|
830
|
-
params.set("project", dsn.projectId);
|
|
831
|
-
if (args.query) for (const [key, value] of Object.entries(args.query)) params.set(key, value);
|
|
832
|
-
const path = `${args.path}?${params.toString()}`;
|
|
833
|
-
if (isSentrySaasDsnHost(dsn.host)) return `https://${orgSlug}.sentry.io/${path}`;
|
|
834
|
-
return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path}`;
|
|
835
|
-
}
|
|
836
|
-
/** Build a Sentry conversation URL only when the runtime has enough Sentry config. */
|
|
837
|
-
function buildSentryConversationUrl(conversationId) {
|
|
838
|
-
return buildSentryOrgPath({ path: `explore/conversations/${encodeURIComponent(conversationId)}/` });
|
|
839
|
-
}
|
|
840
|
-
/** Build a Sentry event URL only when the runtime has enough Sentry config. */
|
|
841
|
-
function buildSentryEventUrl(eventId) {
|
|
842
|
-
return buildSentryOrgPath({
|
|
843
|
-
path: "issues/",
|
|
844
|
-
query: { query: eventId }
|
|
845
|
-
});
|
|
846
|
-
}
|
|
847
|
-
// src/api/conversations/event-model.ts
|
|
848
|
-
/** Apply recorded model choices without carrying a previous turn's settings forward. */
|
|
849
|
-
function conversationEventModel(data, current) {
|
|
850
|
-
if (data.type === "turn_started") return void 0;
|
|
851
|
-
if (data.type === "turn_routed" || data.type === "handoff") return {
|
|
852
|
-
modelId: data.modelId,
|
|
853
|
-
modelProfile: data.modelProfile,
|
|
854
|
-
...data.reasoningLevel ? { reasoningLevel: data.reasoningLevel } : void 0
|
|
855
|
-
};
|
|
856
|
-
if (data.type === "assistant_message") {
|
|
857
|
-
const model = text$1(data.model);
|
|
858
|
-
const provider = text$1(data.provider);
|
|
859
|
-
if (model) {
|
|
860
|
-
const modelId = model.includes("/") || !provider ? model : `${provider}/${model}`;
|
|
861
|
-
return {
|
|
862
|
-
...current,
|
|
863
|
-
modelId
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
return current;
|
|
868
|
-
}
|
|
869
|
-
function text$1(value) {
|
|
870
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
871
|
-
}
|
|
872
|
-
/** Allowlist model-call diagnostics; never expose opaque provider metadata. */
|
|
873
|
-
function conversationEventModelCall(data) {
|
|
874
|
-
if (data.type !== "assistant_message") return void 0;
|
|
875
|
-
const usage = extractGenAiUsageSummary(data);
|
|
876
|
-
const provider = text$1(data.provider);
|
|
877
|
-
const api = text$1(data.api);
|
|
878
|
-
const stopReason = text$1(data.stopReason);
|
|
879
|
-
return {
|
|
880
|
-
...provider ? { provider } : void 0,
|
|
881
|
-
...api ? { api } : void 0,
|
|
882
|
-
...stopReason ? { stopReason } : void 0,
|
|
883
|
-
...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
|
|
884
536
|
};
|
|
885
537
|
}
|
|
886
538
|
// src/api/conversations/events.ts
|
|
@@ -1071,6 +723,12 @@ function conversationReportToolResultIds(events) {
|
|
|
1071
723
|
return result.success ? [result.data.toolCallId] : [];
|
|
1072
724
|
}))];
|
|
1073
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
|
+
}
|
|
1074
732
|
function reportEventData(args) {
|
|
1075
733
|
const { data } = args;
|
|
1076
734
|
if (data.type === "structured_event") {
|
|
@@ -1099,6 +757,7 @@ function reportEventData(args) {
|
|
|
1099
757
|
...typeof data.meta?.eventType === "string" ? { eventType: data.meta.eventType } : void 0,
|
|
1100
758
|
...typeof data.meta?.explicitMention === "boolean" ? { explicitMention: data.meta.explicitMention } : void 0,
|
|
1101
759
|
...args.canExposePayload && typeof data.meta?.trustedSummary === "string" ? { trustedSummary: data.meta.trustedSummary } : void 0,
|
|
760
|
+
...args.canExposePayload ? reportMessageEvents(data.meta) : void 0,
|
|
1102
761
|
...args.canExposePayload && (data.meta?.cards || data.meta?.objectCards) ? { cards: readMessageCardRefs(data.meta) } : void 0,
|
|
1103
762
|
...args.canExposePayload ? {
|
|
1104
763
|
text: data.text,
|
|
@@ -2228,6 +1887,355 @@ async function updateTimerIndex(state, operation) {
|
|
|
2228
1887
|
await state.releaseLock(lock);
|
|
2229
1888
|
}
|
|
2230
1889
|
}
|
|
1890
|
+
// src/chat/slack/workspace-context.ts
|
|
1891
|
+
const workspaceTeamIdStorage = new AsyncLocalStorage();
|
|
1892
|
+
/** Run a callback with the Slack workspace team ID for the inbound webhook. */
|
|
1893
|
+
function runWithWorkspaceTeamId(teamId, fn) {
|
|
1894
|
+
if (!teamId) return fn();
|
|
1895
|
+
return workspaceTeamIdStorage.run(teamId, fn);
|
|
1896
|
+
}
|
|
1897
|
+
/** Return the Slack workspace team ID for the current inbound webhook. */
|
|
1898
|
+
function getWorkspaceTeamId() {
|
|
1899
|
+
return workspaceTeamIdStorage.getStore();
|
|
1900
|
+
}
|
|
1901
|
+
// src/chat/slack/client.ts
|
|
1902
|
+
var SlackActionError = class extends Error {
|
|
1903
|
+
code;
|
|
1904
|
+
apiError;
|
|
1905
|
+
needed;
|
|
1906
|
+
provided;
|
|
1907
|
+
statusCode;
|
|
1908
|
+
requestId;
|
|
1909
|
+
errorData;
|
|
1910
|
+
retryAfterSeconds;
|
|
1911
|
+
detail;
|
|
1912
|
+
detailLine;
|
|
1913
|
+
detailRule;
|
|
1914
|
+
constructor(message, code, options = {}) {
|
|
1915
|
+
super(message);
|
|
1916
|
+
this.name = "SlackActionError";
|
|
1917
|
+
this.code = code;
|
|
1918
|
+
this.apiError = options.apiError;
|
|
1919
|
+
this.needed = options.needed;
|
|
1920
|
+
this.provided = options.provided;
|
|
1921
|
+
this.statusCode = options.statusCode;
|
|
1922
|
+
this.requestId = options.requestId;
|
|
1923
|
+
this.errorData = options.errorData;
|
|
1924
|
+
this.retryAfterSeconds = options.retryAfterSeconds;
|
|
1925
|
+
this.detail = options.detail;
|
|
1926
|
+
this.detailLine = options.detailLine;
|
|
1927
|
+
this.detailRule = options.detailRule;
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
function serializeSlackErrorData(data) {
|
|
1931
|
+
if (!data || typeof data !== "object") return;
|
|
1932
|
+
const filtered = Object.fromEntries(Object.entries(data).filter(([key]) => key !== "error"));
|
|
1933
|
+
if (Object.keys(filtered).length === 0) return;
|
|
1934
|
+
try {
|
|
1935
|
+
const serialized = JSON.stringify(filtered);
|
|
1936
|
+
return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
|
|
1937
|
+
} catch {
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
/** Extract a header value by case-insensitive name from a raw headers object. */
|
|
1942
|
+
function getHeaderString(headers, name) {
|
|
1943
|
+
if (!headers || typeof headers !== "object") return;
|
|
1944
|
+
const key = name.toLowerCase();
|
|
1945
|
+
const entries = headers;
|
|
1946
|
+
for (const [entryKey, value] of Object.entries(entries)) {
|
|
1947
|
+
if (entryKey.toLowerCase() !== key) continue;
|
|
1948
|
+
if (typeof value === "string") return value;
|
|
1949
|
+
if (Array.isArray(value)) {
|
|
1950
|
+
const first = value.find((entry) => typeof entry === "string");
|
|
1951
|
+
return typeof first === "string" ? first : void 0;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
function parseSlackCanvasDetail(detail) {
|
|
1956
|
+
if (typeof detail !== "string") return {};
|
|
1957
|
+
const trimmed = detail.trim();
|
|
1958
|
+
if (!trimmed) return {};
|
|
1959
|
+
const parsed = { detail: trimmed };
|
|
1960
|
+
const lineMatch = trimmed.match(/line\s+(\d+):/i);
|
|
1961
|
+
if (lineMatch) {
|
|
1962
|
+
const line = Number.parseInt(lineMatch[1] ?? "", 10);
|
|
1963
|
+
if (Number.isFinite(line)) parsed.detailLine = line;
|
|
1964
|
+
}
|
|
1965
|
+
if (/unsupported heading depth/i.test(trimmed)) parsed.detailRule = "unsupported_heading_depth";
|
|
1966
|
+
return parsed;
|
|
1967
|
+
}
|
|
1968
|
+
/**
|
|
1969
|
+
* Ambient destination-installation token for Slack outbound calls, bound by
|
|
1970
|
+
* installation-scoped entry points so writes carry the destination
|
|
1971
|
+
* workspace's credentials instead of the process-global env token.
|
|
1972
|
+
*/
|
|
1973
|
+
const installationTokenStorage = new AsyncLocalStorage();
|
|
1974
|
+
const clientsByToken = /* @__PURE__ */ new Map();
|
|
1975
|
+
/**
|
|
1976
|
+
* Bind a workspace installation token for all Slack Web API calls inside
|
|
1977
|
+
* `fn`, so multi-workspace outbound writes use the destination workspace's
|
|
1978
|
+
* credentials rather than the env fallback token.
|
|
1979
|
+
*/
|
|
1980
|
+
function runWithSlackInstallationToken(token, fn) {
|
|
1981
|
+
const trimmed = token.trim();
|
|
1982
|
+
if (!trimmed) throw new SlackActionError("Slack installation token binding requires a non-empty token", "missing_token");
|
|
1983
|
+
return installationTokenStorage.run({ token: trimmed }, fn);
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* Token precedence: ambient installation token, then env token, then a hard
|
|
1987
|
+
* failure for team-scoped calls — a call scoped to one workspace must never
|
|
1988
|
+
* silently fall through to another workspace's credentials.
|
|
1989
|
+
*/
|
|
1990
|
+
function resolveSlackToken() {
|
|
1991
|
+
const ambientToken = installationTokenStorage.getStore()?.token;
|
|
1992
|
+
if (ambientToken) return ambientToken;
|
|
1993
|
+
const envToken = getSlackBotToken();
|
|
1994
|
+
if (envToken) return envToken;
|
|
1995
|
+
const teamId = getWorkspaceTeamId();
|
|
1996
|
+
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");
|
|
1997
|
+
throw new SlackActionError("SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack Web API actions in this service", "missing_token");
|
|
1998
|
+
}
|
|
1999
|
+
/** Normalize Junior Slack references to native Slack conversation IDs. */
|
|
2000
|
+
function normalizeSlackConversationId(channelId) {
|
|
2001
|
+
return parseSlackChannelReferenceId(channelId);
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Return the per-token cached WebClient. `withSlackRetries` owns retry
|
|
2005
|
+
* classification for this boundary: the WebClient's built-in policy would
|
|
2006
|
+
* re-post timed-out writes (a duplicate-message hazard) and sleep for the
|
|
2007
|
+
* full unbounded Retry-After inside the request, so both internal behaviors
|
|
2008
|
+
* are disabled here.
|
|
2009
|
+
*/
|
|
2010
|
+
function getClient() {
|
|
2011
|
+
const token = resolveSlackToken();
|
|
2012
|
+
let cached = clientsByToken.get(token);
|
|
2013
|
+
if (!cached) {
|
|
2014
|
+
cached = new WebClient(token, {
|
|
2015
|
+
retryConfig: { retries: 0 },
|
|
2016
|
+
rejectRateLimitedCalls: true
|
|
2017
|
+
});
|
|
2018
|
+
clientsByToken.set(token, cached);
|
|
2019
|
+
}
|
|
2020
|
+
return cached;
|
|
2021
|
+
}
|
|
2022
|
+
function mapSlackError(error) {
|
|
2023
|
+
if (error instanceof SlackActionError) return error;
|
|
2024
|
+
const candidate = error;
|
|
2025
|
+
const apiError = candidate.data?.error;
|
|
2026
|
+
const message = candidate.message ?? "Slack action failed";
|
|
2027
|
+
const baseOptions = {
|
|
2028
|
+
apiError,
|
|
2029
|
+
statusCode: candidate.statusCode,
|
|
2030
|
+
requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
|
|
2031
|
+
errorData: serializeSlackErrorData(candidate.data),
|
|
2032
|
+
...parseSlackCanvasDetail(candidate.data?.detail)
|
|
2033
|
+
};
|
|
2034
|
+
if (apiError === "missing_scope") return new SlackActionError(message, "missing_scope", {
|
|
2035
|
+
...baseOptions,
|
|
2036
|
+
needed: candidate.data?.needed,
|
|
2037
|
+
provided: candidate.data?.provided
|
|
2038
|
+
});
|
|
2039
|
+
if (apiError === "not_in_channel") return new SlackActionError(message, "not_in_channel", baseOptions);
|
|
2040
|
+
if (apiError === "restricted_action_read_only_channel") return new SlackActionError(message, "read_only_channel", baseOptions);
|
|
2041
|
+
if (apiError === "already_reacted") return new SlackActionError(message, "already_reacted", baseOptions);
|
|
2042
|
+
if (apiError === "no_reaction") return new SlackActionError(message, "no_reaction", baseOptions);
|
|
2043
|
+
if (apiError === "invalid_arguments") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
2044
|
+
if (apiError === "invalid_cursor") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
2045
|
+
if (apiError === "invalid_name") return new SlackActionError(message, "invalid_arguments", baseOptions);
|
|
2046
|
+
if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") return new SlackActionError(message, "not_found", baseOptions);
|
|
2047
|
+
if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") return new SlackActionError(message, "feature_unavailable", baseOptions);
|
|
2048
|
+
if (apiError === "canvas_creation_failed") return new SlackActionError(message, "canvas_creation_failed", baseOptions);
|
|
2049
|
+
if (apiError === "canvas_editing_failed") return new SlackActionError(message, "canvas_editing_failed", baseOptions);
|
|
2050
|
+
if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) return new SlackActionError(message, "rate_limited", {
|
|
2051
|
+
...baseOptions,
|
|
2052
|
+
retryAfterSeconds: candidate.retryAfter
|
|
2053
|
+
});
|
|
2054
|
+
return new SlackActionError(message, "internal_error", baseOptions);
|
|
2055
|
+
}
|
|
2056
|
+
const MAX_RETRY_DELAY_MS = 1e4;
|
|
2057
|
+
const MAX_TOTAL_RETRY_DELAY_MS = 2e4;
|
|
2058
|
+
const CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2059
|
+
"ECONNREFUSED",
|
|
2060
|
+
"ECONNRESET",
|
|
2061
|
+
"ENOTFOUND",
|
|
2062
|
+
"EAI_AGAIN"
|
|
2063
|
+
]);
|
|
2064
|
+
const TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
2065
|
+
"ETIMEDOUT",
|
|
2066
|
+
"ECONNABORTED",
|
|
2067
|
+
"ESOCKETTIMEDOUT"
|
|
2068
|
+
]);
|
|
2069
|
+
function findNetworkErrorCode(error, depth = 0) {
|
|
2070
|
+
if (!error || typeof error !== "object" || depth > 4) return;
|
|
2071
|
+
const candidate = error;
|
|
2072
|
+
if (typeof candidate.code === "string" && (CONNECTION_ERROR_CODES.has(candidate.code) || TIMEOUT_ERROR_CODES.has(candidate.code))) return candidate.code;
|
|
2073
|
+
return findNetworkErrorCode(candidate.original, depth + 1) ?? findNetworkErrorCode(candidate.cause, depth + 1);
|
|
2074
|
+
}
|
|
2075
|
+
function hasSocketHangUpMessage(error) {
|
|
2076
|
+
return error instanceof Error && error.message.toLowerCase().includes("socket hang up");
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Classify a failed Slack call for retry: rate limits, Slack 5xx, and
|
|
2080
|
+
* connection-phase failures are always retryable; timeouts are retried only
|
|
2081
|
+
* for idempotent operations because Slack may already have accepted the
|
|
2082
|
+
* request.
|
|
2083
|
+
*/
|
|
2084
|
+
function classifySlackRetry(raw, mapped) {
|
|
2085
|
+
if (mapped.code === "rate_limited") return "rate_limited";
|
|
2086
|
+
if (mapped.statusCode !== void 0 && mapped.statusCode >= 500) return "server_error";
|
|
2087
|
+
const networkCode = findNetworkErrorCode(raw);
|
|
2088
|
+
if (networkCode && CONNECTION_ERROR_CODES.has(networkCode)) return "connection";
|
|
2089
|
+
if (networkCode && TIMEOUT_ERROR_CODES.has(networkCode)) return "timeout";
|
|
2090
|
+
if (hasSocketHangUpMessage(raw)) return "connection";
|
|
2091
|
+
return "none";
|
|
2092
|
+
}
|
|
2093
|
+
/**
|
|
2094
|
+
* Run a Slack Web API call with bounded retries so transient platform
|
|
2095
|
+
* failures do not surface as turn failures, while non-idempotent posts are
|
|
2096
|
+
* never re-sent when Slack may already have accepted them.
|
|
2097
|
+
*
|
|
2098
|
+
* Retry classes: rate limits (bounded Retry-After), connection-phase network
|
|
2099
|
+
* failures, and Slack 5xx responses always retry; request timeouts retry only
|
|
2100
|
+
* when the caller marks the operation `idempotent`.
|
|
2101
|
+
*/
|
|
2102
|
+
async function withSlackRetries(task, maxAttempts = 3, context = {}) {
|
|
2103
|
+
let attempt = 0;
|
|
2104
|
+
let totalDelayMs = 0;
|
|
2105
|
+
const action = context.action ?? "unknown";
|
|
2106
|
+
while (attempt < maxAttempts) {
|
|
2107
|
+
attempt += 1;
|
|
2108
|
+
const attemptNumber = attempt;
|
|
2109
|
+
try {
|
|
2110
|
+
return await withSpan(`POST slack.com/api/${action}`, "http.client", {}, async () => {
|
|
2111
|
+
try {
|
|
2112
|
+
return await task();
|
|
2113
|
+
} catch (error) {
|
|
2114
|
+
const mapped = mapSlackError(error);
|
|
2115
|
+
const errorAttrs = { "error.type": mapped.code };
|
|
2116
|
+
if (mapped.apiError) errorAttrs["app.slack.api_error_code"] = mapped.apiError;
|
|
2117
|
+
if (mapped.code === "rate_limited") {
|
|
2118
|
+
errorAttrs["http.response.status_code"] = 429;
|
|
2119
|
+
if (mapped.retryAfterSeconds) errorAttrs["app.slack.retry_after_ms"] = mapped.retryAfterSeconds * 1e3;
|
|
2120
|
+
} else if (mapped.statusCode != null) errorAttrs["http.response.status_code"] = mapped.statusCode;
|
|
2121
|
+
setSpanAttributes(errorAttrs);
|
|
2122
|
+
setSpanStatus("error");
|
|
2123
|
+
throw error;
|
|
2124
|
+
}
|
|
2125
|
+
}, {
|
|
2126
|
+
"http.request.method": "POST",
|
|
2127
|
+
"server.address": "slack.com",
|
|
2128
|
+
"url.scheme": "https",
|
|
2129
|
+
"url.path": `/api/${action}`,
|
|
2130
|
+
"app.slack.method": action,
|
|
2131
|
+
"app.retry.max_attempts": maxAttempts,
|
|
2132
|
+
...attemptNumber > 1 ? { "http.request.resend_count": attemptNumber - 1 } : void 0,
|
|
2133
|
+
...context.attributes ?? {},
|
|
2134
|
+
...context.spanAttributes ?? {}
|
|
2135
|
+
});
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
const mapped = mapSlackError(error);
|
|
2138
|
+
const retryClass = classifySlackRetry(error, mapped);
|
|
2139
|
+
const isRetryable = retryClass === "rate_limited" || retryClass === "connection" || retryClass === "server_error" || retryClass === "timeout" && context.idempotent === true;
|
|
2140
|
+
const remainingDelayBudgetMs = MAX_TOTAL_RETRY_DELAY_MS - totalDelayMs;
|
|
2141
|
+
const baseLogAttributes = {
|
|
2142
|
+
"app.slack.action": action,
|
|
2143
|
+
"app.slack.error_code": mapped.code,
|
|
2144
|
+
...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : void 0,
|
|
2145
|
+
...mapped.detail ? { "app.slack.detail": mapped.detail } : void 0,
|
|
2146
|
+
...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : void 0,
|
|
2147
|
+
...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : void 0,
|
|
2148
|
+
...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : void 0,
|
|
2149
|
+
...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : void 0,
|
|
2150
|
+
...context.attributes ?? {}
|
|
2151
|
+
};
|
|
2152
|
+
if (!isRetryable || attempt >= maxAttempts || remainingDelayBudgetMs <= 0) {
|
|
2153
|
+
logWarn("slack.action.failed", {
|
|
2154
|
+
...baseLogAttributes,
|
|
2155
|
+
...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : void 0
|
|
2156
|
+
});
|
|
2157
|
+
throw mapped;
|
|
2158
|
+
}
|
|
2159
|
+
logWarn("slack.action.retrying", {
|
|
2160
|
+
...baseLogAttributes,
|
|
2161
|
+
"app.slack.retry_attempt": attempt,
|
|
2162
|
+
"app.slack.retry_class": retryClass
|
|
2163
|
+
});
|
|
2164
|
+
const retryAfterMs = retryClass === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
|
|
2165
|
+
const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
|
|
2166
|
+
const delayMs = Math.min(retryAfterMs ?? backoffMs, MAX_RETRY_DELAY_MS, remainingDelayBudgetMs);
|
|
2167
|
+
totalDelayMs += delayMs;
|
|
2168
|
+
await sleep$1(delayMs);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
throw new SlackActionError("Slack action exhausted retries", "internal_error");
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Slack Web API client for the current destination workspace: the ambient
|
|
2175
|
+
* installation token when one is bound, otherwise the env bot token for
|
|
2176
|
+
* single-workspace deployments. Fails instead of falling back when the call
|
|
2177
|
+
* is workspace-scoped and no installation token can be resolved, so a write
|
|
2178
|
+
* never goes out with another workspace's credentials.
|
|
2179
|
+
*/
|
|
2180
|
+
function getSlackClient(options) {
|
|
2181
|
+
if (options) return new WebClient(resolveSlackToken(), {
|
|
2182
|
+
retryConfig: { retries: 0 },
|
|
2183
|
+
rejectRateLimitedCalls: true,
|
|
2184
|
+
timeout: options.timeoutMs
|
|
2185
|
+
});
|
|
2186
|
+
return getClient();
|
|
2187
|
+
}
|
|
2188
|
+
/**
|
|
2189
|
+
* Slack channel ID prefixes:
|
|
2190
|
+
* - C: channel — modern private channels also use C, so the prefix never
|
|
2191
|
+
* proves a channel is public
|
|
2192
|
+
* - G: legacy private channel / group DM
|
|
2193
|
+
* - D: direct message (1:1)
|
|
2194
|
+
*/
|
|
2195
|
+
function isDmChannel(channelId) {
|
|
2196
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
2197
|
+
return Boolean(normalized && normalized.startsWith("D"));
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* Conversation-scoped Slack contexts backed by a concrete conversation ID.
|
|
2201
|
+
* Includes channels/groups/DMs (C/G/D).
|
|
2202
|
+
*/
|
|
2203
|
+
function isConversationScopedChannel(channelId) {
|
|
2204
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
2205
|
+
if (!normalized) return false;
|
|
2206
|
+
return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
|
|
2207
|
+
}
|
|
2208
|
+
function isConversationChannel(channelId) {
|
|
2209
|
+
const normalized = normalizeSlackConversationId(channelId);
|
|
2210
|
+
if (!normalized) return false;
|
|
2211
|
+
return normalized.startsWith("C") || normalized.startsWith("G");
|
|
2212
|
+
}
|
|
2213
|
+
async function getFilePermalink(fileId) {
|
|
2214
|
+
const client = getClient();
|
|
2215
|
+
return (await withSlackRetries(() => client.files.info({ file: fileId }), 3, {
|
|
2216
|
+
action: "files.info",
|
|
2217
|
+
idempotent: true,
|
|
2218
|
+
spanAttributes: { "app.slack.file_id": fileId }
|
|
2219
|
+
})).file?.permalink;
|
|
2220
|
+
}
|
|
2221
|
+
async function downloadPrivateSlackFile(url) {
|
|
2222
|
+
const token = resolveSlackToken();
|
|
2223
|
+
return withSpan("GET files.slack.com", "http.client", {}, async () => {
|
|
2224
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
|
|
2225
|
+
setSpanAttributes({ "http.response.status_code": response.status });
|
|
2226
|
+
if (!response.ok) {
|
|
2227
|
+
setSpanAttributes({ "error.type": String(response.status) });
|
|
2228
|
+
setSpanStatus("error");
|
|
2229
|
+
throw new Error(`Slack file download failed: ${response.status}`);
|
|
2230
|
+
}
|
|
2231
|
+
return Buffer.from(await response.arrayBuffer());
|
|
2232
|
+
}, {
|
|
2233
|
+
"http.request.method": "GET",
|
|
2234
|
+
"server.address": "files.slack.com",
|
|
2235
|
+
"url.scheme": "https",
|
|
2236
|
+
"app.slack.method": "files.download"
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2231
2239
|
// src/chat/credentials/subject.ts
|
|
2232
2240
|
const CREDENTIAL_SUBJECT_HMAC_CONTEXT = "junior.credential_subject.v1";
|
|
2233
2241
|
const CREDENTIAL_SUBJECT_SIGNATURE_VERSION = "v1";
|
|
@@ -3057,6 +3065,8 @@ function normalizeWorkspaceRecipe(input) {
|
|
|
3057
3065
|
const setupScript = input.setupScript ?? "";
|
|
3058
3066
|
if (typeof setupScript !== "string") throw new WorkspaceValidationError("Setup script must be a string.");
|
|
3059
3067
|
if (Buffer.byteLength(setupScript, "utf8") > MAX_SETUP_SCRIPT_BYTES) throw new WorkspaceValidationError(`Setup script must be at most ${MAX_SETUP_SCRIPT_BYTES} bytes.`);
|
|
3068
|
+
const vcpus = input.vcpus ?? null;
|
|
3069
|
+
if (vcpus !== null && (!Number.isSafeInteger(vcpus) || vcpus <= 0)) throw new WorkspaceValidationError("vCPUs must be a positive integer.");
|
|
3060
3070
|
if (!Array.isArray(input.repos)) throw new WorkspaceValidationError("Repositories must be an array.");
|
|
3061
3071
|
if (input.repos.length > MAX_REPOS) throw new WorkspaceValidationError(`A Workspace can include at most ${MAX_REPOS} repositories.`);
|
|
3062
3072
|
const repos = input.repos.map((entry, index) => {
|
|
@@ -3086,6 +3096,7 @@ function normalizeWorkspaceRecipe(input) {
|
|
|
3086
3096
|
return {
|
|
3087
3097
|
name,
|
|
3088
3098
|
setupScript,
|
|
3099
|
+
vcpus,
|
|
3089
3100
|
repos
|
|
3090
3101
|
};
|
|
3091
3102
|
}
|
|
@@ -3095,6 +3106,7 @@ function workspaceFromRows(row, repos, snapshot) {
|
|
|
3095
3106
|
id: row.id,
|
|
3096
3107
|
name: row.name,
|
|
3097
3108
|
setupScript: row.setupScript,
|
|
3109
|
+
vcpus: row.vcpus,
|
|
3098
3110
|
repos: repos.map((repo) => ({
|
|
3099
3111
|
provider: repo.provider,
|
|
3100
3112
|
repo: repo.repo
|
|
@@ -3193,6 +3205,7 @@ async function createWorkspace(input) {
|
|
|
3193
3205
|
id,
|
|
3194
3206
|
name: recipe.name,
|
|
3195
3207
|
setupScript: recipe.setupScript,
|
|
3208
|
+
vcpus: recipe.vcpus,
|
|
3196
3209
|
createdAt: now,
|
|
3197
3210
|
updatedAt: now
|
|
3198
3211
|
});
|
|
@@ -3219,6 +3232,7 @@ async function updateWorkspace(id, input) {
|
|
|
3219
3232
|
const rows = await db.update(juniorWorkspaces).set({
|
|
3220
3233
|
name: recipe.name,
|
|
3221
3234
|
setupScript: recipe.setupScript,
|
|
3235
|
+
vcpus: recipe.vcpus,
|
|
3222
3236
|
updatedAt: now
|
|
3223
3237
|
}).where(eq(juniorWorkspaces.id, id)).returning();
|
|
3224
3238
|
await replaceWorkspaceRepos(db, id, recipe.repos);
|
|
@@ -4331,4 +4345,4 @@ function createPluginHookRunner(input = {}) {
|
|
|
4331
4345
|
}
|
|
4332
4346
|
};
|
|
4333
4347
|
}
|
|
4334
|
-
export { bindSlackDirectCredentialSubject as $, authenticationUnlinkedEvent as $t, getWorkspaceSnapshotBuilder as A,
|
|
4348
|
+
export { bindSlackDirectCredentialSubject as $, authenticationUnlinkedEvent as $t, getWorkspaceSnapshotBuilder as A, conversationHasParticipantUser 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, conversationHasParticipantEmail 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 };
|