@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.
Files changed (77) hide show
  1. package/dist/adapter-Dj5BTJRd.js +270 -0
  2. package/dist/{admin-My1SLGVO.js → admin-tzkZDqrH.js} +1 -1
  3. package/dist/{agent-hooks-BJ_5mlRH.js → agent-hooks-V4Hfc2tB.js} +711 -697
  4. package/dist/{agent-runner-DGsFQw72.js → agent-runner-DvTUyGTY.js} +751 -2508
  5. package/dist/api/schema.d.ts +239 -54
  6. package/dist/api/schema.js +4 -4
  7. package/dist/api.js +218 -57
  8. package/dist/{app-PX2H1PxU.d.ts → app-BsBUs3rF.d.ts} +6 -3
  9. package/dist/app.d.ts +2 -2
  10. package/dist/app.js +31 -31
  11. package/dist/{assistant-reply-H1M-91oF.js → assistant-reply-CEOMzsWy.js} +1 -1
  12. package/dist/{automation-DWBFBenG.js → automation-DrpcA-SX.js} +3 -0
  13. package/dist/{briefs-vYoK85rJ.js → briefs-BSCkyYqs.js} +4 -4
  14. package/dist/{catalog-runtime-BN4DVxco.js → catalog-runtime-Bpcs2b2L.js} +1 -1
  15. package/dist/cli/chat.d.ts +1 -1
  16. package/dist/cli/chat.js +16 -16
  17. package/dist/cli/check.js +3 -3
  18. package/dist/cli/main.js +6 -2
  19. package/dist/cli/plugins.d.ts +1 -1
  20. package/dist/cli/plugins.js +4 -4
  21. package/dist/cli/run.d.ts +2 -1
  22. package/dist/cli/run.js +2 -1
  23. package/dist/cli/snapshot-create.js +4 -3
  24. package/dist/cli/upgrade.d.ts +1 -1
  25. package/dist/cli/upgrade.js +3 -3
  26. package/dist/{client-DbyHuMFU.js → client-JeyzRwsT.js} +57 -4
  27. package/dist/{config-_WO_AjCB.js → config-ULs6Zl9b.js} +5 -2
  28. package/dist/{connections-ag_1JOhB.js → connections-CZ7HhEmN.js} +174 -10
  29. package/dist/{conversation-DjN3mlog.js → conversation-CvuKJ3tA.js} +108 -155
  30. package/dist/{conversation-privacy-DHP_xnVj.js → conversation-privacy-BQSlZXgV.js} +1 -1
  31. package/dist/{credential-sync-B-CPX_ag.js → credential-sync-BKcv5rBe.js} +14 -177
  32. package/dist/{executor-RvG6kVe1.js → executor-BoGNVEDn.js} +3 -2
  33. package/dist/{images-C2J_bj2g.js → images-BBmDXQCB.js} +3 -3
  34. package/dist/instrumentation.js +2 -2
  35. package/dist/{job-queue-DVe37it2.js → job-queue-CbdVyaCo.js} +1 -1
  36. package/dist/{logging-DsHONhVm.js → logging-DSMmkHXG.js} +1 -1
  37. package/dist/{message-DyitL8no.js → message-zaj5gBbS.js} +3 -3
  38. package/dist/nitro.d.ts +2 -2
  39. package/dist/nitro.js +3 -3
  40. package/dist/{oauth-callback-server-CDby-pCn.js → oauth-callback-server-yYFG8Own.js} +2 -2
  41. package/dist/{plugins-B11Mfkmv.d.ts → plugins-B_0Cr0MO.d.ts} +5 -1
  42. package/dist/{profile-DpYjfKke.js → profile-ByWkAY7L.js} +24 -293
  43. package/dist/{prompt-xgpyMtg7.js → prompt-DeVax2qO.js} +1 -1
  44. package/dist/{registry-Du566MtJ.js → registry-BL-osYWN.js} +41 -2
  45. package/dist/{resolve-ClCIgh8E.js → resolve-HuPG2VMB.js} +9 -4
  46. package/dist/{result-BBbKA61B.js → result-DS7V4a3-.js} +9 -14
  47. package/dist/{runner-CCuqFSqx.js → runner-Cba_OfGl.js} +11 -11
  48. package/dist/{sandbox-egress-signals-DY1mbKF4.js → sandbox-egress-signals-C7NcPzea.js} +1 -1
  49. package/dist/{schema-DrMh3NtR.js → schema-OzL8Zw4H.js} +82 -3
  50. package/dist/service-account-DqwHhwJA.js +120 -0
  51. package/dist/service-account-Ko1Xs--F.js +661 -0
  52. package/dist/{session-stop-CIoFiwwe.js → session-stop-HfKx-pky.js} +180 -22
  53. package/dist/{skills-DyQBLIrK.js → skills-D6_bcAuW.js} +2 -2
  54. package/dist/{spawn-Dqfeo7XP.js → spawn-BWqK6EwM.js} +2 -2
  55. package/dist/{store-DJLwq4x8.js → store-DzRbd5VT.js} +6 -6
  56. package/dist/{task-queue-BzW4MaWU.js → task-queue-9LICOzcL.js} +3 -3
  57. package/dist/{task-runner-D3qJh4fK.js → task-runner-DGnd_B5X.js} +10 -10
  58. package/dist/{tasks-CEXKTASS.js → tasks-CjrS098O.js} +4928 -2389
  59. package/dist/thread-state-CqkYowEz.js +184 -0
  60. package/dist/{turn-failure-response-CWx7aNIh.js → turn-failure-response-BkKmGqFt.js} +8 -9
  61. package/dist/{turn-lifecycle-C9JFst7-.js → turn-lifecycle-DYv8IV6w.js} +1 -1
  62. package/dist/{turn-session-routing-CUxtfpr7.js → turn-session-routing-TaVhKmdz.js} +1 -1
  63. package/dist/{validation-BHRxa5G4.js → validation-BrXMYPyo.js} +1 -1
  64. package/dist/{version-C0U6rHg0.js → version-Ct049vav.js} +1 -1
  65. package/dist/version.js +1 -1
  66. package/dist/{viewer-CGvvRqKp.js → viewer-C-J0ewlH.js} +19 -10
  67. package/dist/{web-input-Keb5GRWj.js → web-input-C_xB3RXp.js} +41 -37
  68. package/dist/{work-DRcsJ3hV.js → work-D829Ue7k.js} +9 -10
  69. package/dist/{worker-Cjo-QqPq.js → worker-BRTYzlpk.js} +5 -5
  70. package/migrations/0061_workspace_vcpus.sql +1 -0
  71. package/migrations/meta/0061_snapshot.json +3471 -0
  72. package/migrations/meta/_journal.json +7 -0
  73. package/package.json +6 -6
  74. package/dist/experimental-BNK1l1jv.js +0 -49
  75. package/dist/oauth-authorization-djQn5Nhy.js +0 -97
  76. package/dist/pending-auth-D8J1uJUS.js +0 -442
  77. 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 { a as embedTexts, r as completeObject } from "./client-DbyHuMFU.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-DsHONhVm.js";
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-DHP_xnVj.js";
6
- import { i as getChatConfig, n as botConfig, o as getSlackBotToken } from "./config-_WO_AjCB.js";
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-CGvvRqKp.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-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-RvG6kVe1.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-BoGNVEDn.js";
12
12
  import { o as decodeStoredConversationEvent } from "./history-AY8rs-FR.js";
13
- import { h as credentialSubjectSchema } from "./registry-Du566MtJ.js";
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 { n as briefEntriesFromReportEvents, s as generateBrief, t as BRIEF_PROMPT } from "./prompt-xgpyMtg7.js";
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/workspace-context.ts
160
- const workspaceTeamIdStorage = new AsyncLocalStorage();
161
- /** Run a callback with the Slack workspace team ID for the inbound webhook. */
162
- function runWithWorkspaceTeamId(teamId, fn) {
163
- if (!teamId) return fn();
164
- return workspaceTeamIdStorage.run(teamId, fn);
165
- }
166
- /** Return the Slack workspace team ID for the current inbound webhook. */
167
- function getWorkspaceTeamId() {
168
- return workspaceTeamIdStorage.getStore();
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
- // src/chat/slack/client.ts
171
- var SlackActionError = class extends Error {
172
- code;
173
- apiError;
174
- needed;
175
- provided;
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
- /** Extract a header value by case-insensitive name from a raw headers object. */
211
- function getHeaderString(headers, name) {
212
- if (!headers || typeof headers !== "object") return;
213
- const key = name.toLowerCase();
214
- const entries = headers;
215
- for (const [entryKey, value] of Object.entries(entries)) {
216
- if (entryKey.toLowerCase() !== key) continue;
217
- if (typeof value === "string") return value;
218
- if (Array.isArray(value)) {
219
- const first = value.find((entry) => typeof entry === "string");
220
- return typeof first === "string" ? first : void 0;
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
- function parseSlackCanvasDetail(detail) {
225
- if (typeof detail !== "string") return {};
226
- const trimmed = detail.trim();
227
- if (!trimmed) return {};
228
- const parsed = { detail: trimmed };
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
- * Ambient destination-installation token for Slack outbound calls, bound by
239
- * installation-scoped entry points so writes carry the destination
240
- * workspace's credentials instead of the process-global env token.
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 runWithSlackInstallationToken(token, fn) {
250
- const trimmed = token.trim();
251
- if (!trimmed) throw new SlackActionError("Slack installation token binding requires a non-empty token", "missing_token");
252
- return installationTokenStorage.run({ token: trimmed }, fn);
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
- * Token precedence: ambient installation token, then env token, then a hard
256
- * failure for team-scoped calls — a call scoped to one workspace must never
257
- * silently fall through to another workspace's credentials.
258
- */
259
- function resolveSlackToken() {
260
- const ambientToken = installationTokenStorage.getStore()?.token;
261
- if (ambientToken) return ambientToken;
262
- const envToken = getSlackBotToken();
263
- if (envToken) return envToken;
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
- /** Normalize Junior Slack references to native Slack conversation IDs. */
269
- function normalizeSlackConversationId(channelId) {
270
- return parseSlackChannelReferenceId(channelId);
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
- * Return the per-token cached WebClient. `withSlackRetries` owns retry
274
- * classification for this boundary: the WebClient's built-in policy would
275
- * re-post timed-out writes (a duplicate-message hazard) and sleep for the
276
- * full unbounded Retry-After inside the request, so both internal behaviors
277
- * are disabled here.
278
- */
279
- function getClient() {
280
- const token = resolveSlackToken();
281
- let cached = clientsByToken.get(token);
282
- if (!cached) {
283
- cached = new WebClient(token, {
284
- retryConfig: { retries: 0 },
285
- rejectRateLimitedCalls: true
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 cached;
309
+ return byConversation;
290
310
  }
291
- function mapSlackError(error) {
292
- if (error instanceof SlackActionError) return error;
293
- const candidate = error;
294
- const apiError = candidate.data?.error;
295
- const message = candidate.message ?? "Slack action failed";
296
- const baseOptions = {
297
- apiError,
298
- statusCode: candidate.statusCode,
299
- requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
300
- errorData: serializeSlackErrorData(candidate.data),
301
- ...parseSlackCanvasDetail(candidate.data?.detail)
302
- };
303
- if (apiError === "missing_scope") return new SlackActionError(message, "missing_scope", {
304
- ...baseOptions,
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 new SlackActionError(message, "internal_error", baseOptions);
326
+ return annotations.map((annotation) => ({
327
+ ...annotation,
328
+ plugin
329
+ }));
324
330
  }
325
- const MAX_RETRY_DELAY_MS = 1e4;
326
- const MAX_TOTAL_RETRY_DELAY_MS = 2e4;
327
- const CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
328
- "ECONNREFUSED",
329
- "ECONNRESET",
330
- "ENOTFOUND",
331
- "EAI_AGAIN"
332
- ]);
333
- const TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
334
- "ETIMEDOUT",
335
- "ECONNABORTED",
336
- "ESOCKETTIMEDOUT"
337
- ]);
338
- function findNetworkErrorCode(error, depth = 0) {
339
- if (!error || typeof error !== "object" || depth > 4) return;
340
- const candidate = error;
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
- function hasSocketHangUpMessage(error) {
345
- return error instanceof Error && error.message.toLowerCase().includes("socket hang up");
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
- * Classify a failed Slack call for retry: rate limits, Slack 5xx, and
349
- * connection-phase failures are always retryable; timeouts are retried only
350
- * for idempotent operations because Slack may already have accepted the
351
- * request.
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
- * Run a Slack Web API call with bounded retries so transient platform
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
- * Retry classes: rate limits (bounded Retry-After), connection-phase network
368
- * failures, and Slack 5xx responses always retry; request timeouts retry only
369
- * when the caller marks the operation `idempotent`.
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 withSlackRetries(task, maxAttempts = 3, context = {}) {
372
- let attempt = 0;
373
- let totalDelayMs = 0;
374
- const action = context.action ?? "unknown";
375
- while (attempt < maxAttempts) {
376
- attempt += 1;
377
- const attemptNumber = attempt;
378
- try {
379
- return await withSpan(`POST slack.com/api/${action}`, "http.client", {}, async () => {
380
- try {
381
- return await task();
382
- } catch (error) {
383
- const mapped = mapSlackError(error);
384
- const errorAttrs = { "error.type": mapped.code };
385
- if (mapped.apiError) errorAttrs["app.slack.api_error_code"] = mapped.apiError;
386
- if (mapped.code === "rate_limited") {
387
- errorAttrs["http.response.status_code"] = 429;
388
- if (mapped.retryAfterSeconds) errorAttrs["app.slack.retry_after_ms"] = mapped.retryAfterSeconds * 1e3;
389
- } else if (mapped.statusCode != null) errorAttrs["http.response.status_code"] = mapped.statusCode;
390
- setSpanAttributes(errorAttrs);
391
- setSpanStatus("error");
392
- throw error;
393
- }
394
- }, {
395
- "http.request.method": "POST",
396
- "server.address": "slack.com",
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
- throw new SlackActionError("Slack action exhausted retries", "internal_error");
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
- * Slack Web API client for the current destination workspace: the ambient
444
- * installation token when one is bound, otherwise the env bot token for
445
- * single-workspace deployments. Fails instead of falling back when the call
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
- * Slack channel ID prefixes:
459
- * - C: channel — modern private channels also use C, so the prefix never
460
- * proves a channel is public
461
- * - G: legacy private channel / group DM
462
- * - D: direct message (1:1)
463
- */
464
- function isDmChannel(channelId) {
465
- const normalized = normalizeSlackConversationId(channelId);
466
- return Boolean(normalized && normalized.startsWith("D"));
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
- * Conversation-scoped Slack contexts backed by a concrete conversation ID.
470
- * Includes channels/groups/DMs (C/G/D).
471
- */
472
- function isConversationScopedChannel(channelId) {
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
- function isConversationChannel(channelId) {
478
- const normalized = normalizeSlackConversationId(channelId);
479
- if (!normalized) return false;
480
- return normalized.startsWith("C") || normalized.startsWith("G");
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
- async function getFilePermalink(fileId) {
483
- const client = getClient();
484
- return (await withSlackRetries(() => client.files.info({ file: fileId }), 3, {
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
- async function downloadPrivateSlackFile(url) {
491
- const token = resolveSlackToken();
492
- return withSpan("GET files.slack.com", "http.client", {}, async () => {
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
- // src/chat/slack/user.ts
509
- const slackUserInfoSchema = z.object({
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 readFromCache(teamId, userId) {
526
- const hit = userCache.get(userCacheKey(teamId, userId));
527
- if (!hit) return null;
528
- if (hit.expiresAt < Date.now()) {
529
- userCache.delete(userCacheKey(teamId, userId));
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 writeToCache(teamId, userId, value) {
535
- userCache.set(userCacheKey(teamId, userId), {
536
- value,
537
- expiresAt: Date.now() + USER_CACHE_TTL_MS
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
- /** Fetch Slack user profile info with in-memory TTL cache to avoid repeated API calls. */
541
- async function lookupSlackUser(teamId, userId) {
542
- if (!teamId || !userId) return null;
543
- const cached = readFromCache(teamId, userId);
544
- if (cached) return cached;
545
- const token = getSlackBotToken();
546
- if (!token) return null;
547
- try {
548
- const response = await fetch(`https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`, { headers: { authorization: `Bearer ${token}` } });
549
- if (!response.ok) {
550
- logWarn("slack.user.lookup.failed", {
551
- "app.slack.user_id": userId,
552
- "app.slack.team_id": teamId,
553
- "http.response.status_code": response.status
554
- });
555
- return null;
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
- // src/chat/identities/timezone.ts
578
- const DEFAULT_TIMEZONE = "America/Los_Angeles";
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
- // src/chat/plugins/annotations.ts
597
- function annotationFromRow(row) {
598
- const parsed = conversationAnnotationInputSchema.safeParse(row.annotation);
599
- if (!parsed.success) return void 0;
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
- ...parsed.data,
602
- plugin: row.plugin,
603
- createdAt: row.createdAt.toISOString(),
604
- updatedAt: row.updatedAt.toISOString()
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, resolveRootVisibility as At, STOP_WATCHING_TOOL_NAME as B, getFilePermalink as Bt, WorkspaceValidationError as C, readLatestConversationBrief as Ct, setWorkspaceSnapshotBuild as D, matchSlackAttachments as Dt, setWorkspaceSnapshot as E, getSlackFileId as Et, isSlackMessageTurn as F, defaultTimezone as Ft, createWatch as G, normalizeSlackConversationId as Gt, cancelWatch as H, getSlackClient as Ht, slackMessageTurnDisabledReason as I, resolveUserTimezone as It, getWatch as J, getWorkspaceTeamId as Jt, deliverWatch as K, runWithSlackInstallationToken as Kt, RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS as L, lookupSlackUser as Lt, workspaceRepoCheckoutPath as M, createPluginAnnotations as Mt, resolveChannelCapabilities as N, listConversationAnnotations as Nt, SANDBOX_RUNTIME as O, readAttachmentVisionSummary as Ot, getSlackToolContext as P, listConversationAnnotationsById as Pt, bindScheduledAutomationCredentialSubject as Q, authenticationLinkedEvent as Qt, RESOURCE_SUBSCRIPTION_MAX_TTL_MS as R, SlackActionError as Rt, updateWorkspace as S, createPluginConversationEvents as St, loadSnapshotsForProfile as T, buildSentryConversationUrl as Tt, collectEventMatchKeys as U, isConversationScopedChannel as Ut, cancelSubscriptions as V, getHeaderString as Vt, createTimerWatch as W, isDmChannel as Wt, takeDueTimerWatches as X, JUNIOR_NATIVE_EVENT_NAMESPACE as Xt, listWatches as Y, runWithWorkspaceTeamId as Yt, bindEventAutomationCredentialSubject as Z, agentsInstructionsUpdatedEvent as Zt, deleteWorkspace as _, conversationHasParticipantEmail as _t, getPluginRoutes as a, getDashboardConversationLink as at, listWorkspaces as b, conversationNotArchivedForUser as bt, getPluginTools as c, setDashboardConversationLinkOptions as ct, setPlugins as d, createPluginModel as dt, conversationNoteEvent as en, verifyEventAutomationCredentialSubject as et, validatePlugins as f, createPluginLogger as ft, createWorkspace as g, conversationArchivedForUser as gt, setBriefsConfig as h, conversationArchivedForEmail as ht, getPluginApiRoutes as i, JUNIOR_THREAD_STATE_TTL_MS as it, tryWorkspaceRepoCheckoutPath as j, saveObjectAnnotations as jt, deleteWorkspaceSnapshotBuilders as k, recordSlackAttachment as kt, getPluginUserPromptContributions as l, createPluginState as lt, registration_exports as m, conversationArchiveTimes as mt, applyPluginFormatMarkdown as n, verifySlackDirectCredentialSubject as nt, getPluginSlackConversationLink as o, getDashboardObjectIconLink as ot, coreTaskRegistrations as p, readConversationAccessFromSql as pt, findMatchingWatches as q, withSlackRetries as qt, createPluginHookRunner as r, verifyTimerWatchCredentialSubject as rt, getPluginSystemPromptContributions as s, getDashboardTaskLink as st, agent_hooks_exports as t, verifyScheduledAutomationCredentialSubject as tt, getPlugins as u, createPluginEmbedder as ut, getWorkspace as v, conversationHasParticipantUser as vt, invalidateReadySnapshot as w, readConversationEventPage as wt, listWorkspacesByRepository as x, viewerConversationMembership as xt, getWorkspaceByName as y, conversationNotArchivedForEmail as yt, RESOURCE_WATCH_TOOL_SOURCE as z, downloadPrivateSlackFile as zt };
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 };