@sentry/junior 0.240.0 → 0.242.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 (56) hide show
  1. package/dist/{admin-D6fRPRgC.js → admin-C54kvHVC.js} +1 -1
  2. package/dist/{agent-hooks-JFbgedMZ.js → agent-hooks-BVttjZvb.js} +1952 -1952
  3. package/dist/{agent-runner-0IjJxsL1.js → agent-runner-BTgnxJ7g.js} +336 -1125
  4. package/dist/api/schema.d.ts +9 -0
  5. package/dist/api/schema.js +2 -2
  6. package/dist/api.js +21 -21
  7. package/dist/app.js +104 -31
  8. package/dist/{automation-B5_c8hEb.js → automation-gznOMaQ2.js} +11 -2
  9. package/dist/{briefs-BGWTp-nz.js → briefs-CKUsv9F1.js} +2 -2
  10. package/dist/{catalog-runtime-DdigNl7s.js → catalog-runtime-C2YkfMZz.js} +1 -1
  11. package/dist/cli/chat.js +12 -12
  12. package/dist/cli/check.js +3 -3
  13. package/dist/cli/main.js +2 -2
  14. package/dist/cli/plugins.js +4 -4
  15. package/dist/cli/snapshot-create.js +3 -3
  16. package/dist/cli/upgrade.js +3 -3
  17. package/dist/{client-Cn73E7br.js → client-D51G-Jdg.js} +2 -2
  18. package/dist/{config-B0S86yw9.js → config-C6Uu3Re9.js} +2 -2
  19. package/dist/{conversation-privacy-BKlX7D_R.js → conversation-privacy-CzL47nXs.js} +1 -1
  20. package/dist/{credential-sync-DeNQm7st.js → credential-sync-DvDaE-Uj.js} +14 -14
  21. package/dist/{executor-BQ-A5nov.js → executor-CGUukwus.js} +4 -2
  22. package/dist/{images-C7OKd6uy.js → images-DSxZt5vk.js} +2 -2
  23. package/dist/instrumentation.js +2 -2
  24. package/dist/{job-queue-DgXLqoeB.js → job-queue-hLylM9cy.js} +1 -1
  25. package/dist/{logging-BSNF167N.js → logging-BdZ_mK3q.js} +1 -1
  26. package/dist/nitro.js +3 -3
  27. package/dist/{oauth-authorization-BM7g61od.js → oauth-authorization-BokpeHv-.js} +1 -1
  28. package/dist/{oauth-callback-server-Dw-BwI6x.js → oauth-callback-server-C3BOSmT_.js} +2 -2
  29. package/dist/{pending-auth-Chn_EA1_.js → pending-auth-DOzL4D24.js} +2 -2
  30. package/dist/{profile-3t9H_IwL.js → profile-2gPzzMib.js} +4 -4
  31. package/dist/{registry-DLZlAhdP.js → registry-XJkXDNK2.js} +1 -1
  32. package/dist/{resolve-9_cIPQsq.js → resolve-DhcdtDt6.js} +2 -2
  33. package/dist/{runner-C_eUbFyp.js → runner-CGyrnIPm.js} +9 -9
  34. package/dist/{sandbox-egress-signals-C1ycxTNA.js → sandbox-egress-signals-D6F3OZJP.js} +1 -1
  35. package/dist/{schema-C4XwHTY4.js → schema-Cabm4jcj.js} +1 -1
  36. package/dist/{skills-Dqgjr_Tl.js → skills-C6PqcBPU.js} +2 -2
  37. package/dist/{spawn-BfEr3t7b.js → spawn-DL9x3T1o.js} +2 -2
  38. package/dist/{task-queue-CuFZDOXn.js → task-queue-vZnBGu9B.js} +3 -3
  39. package/dist/{task-runner-c7gwV8dj.js → task-runner-C-c0t5F5.js} +54 -12
  40. package/dist/{checkpoint-C1ZnSz7r.js → tasks-EqCtmUJ2.js} +1056 -14
  41. package/dist/{thread-state-exTu-G1w.js → thread-state-B8IKs_J8.js} +6 -6
  42. package/dist/{turn-failure-response-DFp99ziF.js → turn-failure-response-BS7OO1BE.js} +9 -8
  43. package/dist/{turn-lifecycle-C4TjuOzG.js → turn-lifecycle-Cxrt8qMD.js} +1 -1
  44. package/dist/{turn-session-routing-Vpg8jUD1.js → turn-session-routing-BlGt2AUb.js} +1 -1
  45. package/dist/{validation-D2lULEG9.js → validation-C8hci6pj.js} +1 -1
  46. package/dist/{version-BjBMCc2o.js → version-CjnHQ8aY.js} +1 -1
  47. package/dist/version.js +1 -1
  48. package/dist/{viewer-XE3nFqKM.js → viewer-BOgEy5zC.js} +3 -3
  49. package/dist/{web-authorization-8E8IrWOj.js → web-authorization-DSMV5tMK.js} +6 -6
  50. package/dist/{web-input-DF4GGJrf.js → web-input-TD5P9jdo.js} +135 -129
  51. package/dist/{work-Dt_JjfbX.js → work-D39OAwP1.js} +7 -7
  52. package/dist/{worker-BrbnLmD3.js → worker-CWiLgCWH.js} +5 -5
  53. package/migrations/0049_event_automation_status_reason.sql +1 -0
  54. package/migrations/meta/0049_snapshot.json +3581 -0
  55. package/migrations/meta/_journal.json +8 -1
  56. package/package.json +5 -5
@@ -1,18 +1,18 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-B4FMCO8f.js";
2
- import { a as embedTexts, r as completeObject } from "./client-Cn73E7br.js";
3
- import { I as isRecord$1, a as extractGenAiUsageSummary, d as logException, f as logInfo, p as logWarn, v as setSpanAttributes, w as withSpan, y as setSpanStatus } from "./logging-BSNF167N.js";
2
+ import { a as embedTexts, r as completeObject } from "./client-D51G-Jdg.js";
3
+ import { I as isRecord$1, a as extractGenAiUsageSummary, d as logException, f as logInfo, p as logWarn, v as setSpanAttributes, w as withSpan, y as setSpanStatus } from "./logging-BdZ_mK3q.js";
4
4
  import { a as getClient$1 } from "./sentry-CgWZM_kV.js";
5
- import { t as canExposeConversationPayload } from "./conversation-privacy-BKlX7D_R.js";
6
- import { i as getChatConfig, n as botConfig, o as getSlackBotToken } from "./config-B0S86yw9.js";
5
+ import { t as canExposeConversationPayload } from "./conversation-privacy-CzL47nXs.js";
6
+ import { i as getChatConfig, n as botConfig, o as getSlackBotToken } from "./config-C6Uu3Re9.js";
7
7
  import { r as defaultModelId } from "./model-profile-BwU8mqwD.js";
8
8
  import { d as parseSlackTeamId, g as parseSlackMessageTs, i as isActorUserId, o as parseActorUserId, u as parseSlackChannelReferenceId } from "./actor-BTtLaeWy.js";
9
- import { O as readUserTimezone, T as hasAgentTurnUsage, c as getConversationEventStore, d as getDb, f as getSqlExecutor, h as createSqlStore, i as resolveViewerUser, k as saveUserTimezone, r as readActorIdentity } from "./viewer-XE3nFqKM.js";
9
+ import { O as readUserTimezone, T as hasAgentTurnUsage, c as getConversationEventStore, d as getDb, f as getSqlExecutor, h as createSqlStore, i as resolveViewerUser, k as saveUserTimezone, r as readActorIdentity } from "./viewer-BOgEy5zC.js";
10
10
  import { n as juniorDestinations } from "./destinations-BeBiJeEM.js";
11
- import { C as juniorCodeChanges, D as juniorConversations, O as juniorIdentities, S as juniorConversationAnnotations, T as juniorAttachments, _ as juniorConversationBriefs, b as juniorConversationEvents, h as juniorConversationParticipants, i as juniorWorkspaces, k as juniorUsers, n as juniorSnapshots, r as juniorWorkspaceRepos, w as juniorCodeRepositories } from "./executor-BQ-A5nov.js";
11
+ import { C as juniorCodeChanges, D as juniorConversations, O as juniorIdentities, S as juniorConversationAnnotations, T as juniorAttachments, _ as juniorConversationBriefs, b as juniorConversationEvents, h as juniorConversationParticipants, i as juniorWorkspaces, k as juniorUsers, n as juniorSnapshots, r as juniorWorkspaceRepos, w as juniorCodeRepositories } from "./executor-CGUukwus.js";
12
12
  import { o as decodeStoredConversationEvent } from "./history-BLTUgFYO.js";
13
- import { p as credentialSubjectSchema } from "./registry-DLZlAhdP.js";
13
+ import { p as credentialSubjectSchema } from "./registry-XJkXDNK2.js";
14
14
  import { B as parseBriefInput, C as conversationReportEventSchema, N as readMessageAttachments, W as readMessageCardRefs, z as conversationBriefSchema } from "./conversation-DwD-vquD.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-3t9H_IwL.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-2gPzzMib.js";
16
16
  import { i as isValidTimeZone } from "./cadence-D00SCzT1.js";
17
17
  import { n as briefEntriesFromReportEvents, s as generateBrief, t as BRIEF_PROMPT } from "./prompt-CMU-B4FA.js";
18
18
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -143,2095 +143,2095 @@ function renderJuniorNativeConversationEvent(args) {
143
143
  return presentation === void 0 ? void 0 : conversationEventPresentationSchema.parse(presentation);
144
144
  }
145
145
  //#endregion
146
- //#region src/chat/slack/user.ts
147
- const slackUserInfoSchema = z.object({
148
- ok: z.literal(true),
149
- user: z.object({
150
- name: z.string().nullish(),
151
- tz: z.string().nullish(),
152
- profile: z.object({
153
- email: z.string().nullish(),
154
- real_name: z.string().nullish()
155
- }).optional()
156
- })
157
- });
158
- const USER_CACHE_TTL_MS = 3e5;
159
- const userCache = /* @__PURE__ */ new Map();
160
- function userCacheKey(teamId, userId) {
161
- return `${teamId}:${userId}`;
162
- }
163
- function readFromCache(teamId, userId) {
164
- const hit = userCache.get(userCacheKey(teamId, userId));
165
- if (!hit) return null;
166
- if (hit.expiresAt < Date.now()) {
167
- userCache.delete(userCacheKey(teamId, userId));
168
- return null;
169
- }
170
- return hit.value;
146
+ //#region src/chat/slack/workspace-context.ts
147
+ const workspaceTeamIdStorage = new AsyncLocalStorage();
148
+ /** Run a callback with the Slack workspace team ID for the inbound webhook. */
149
+ function runWithWorkspaceTeamId(teamId, fn) {
150
+ if (!teamId) return fn();
151
+ return workspaceTeamIdStorage.run(teamId, fn);
171
152
  }
172
- function writeToCache(teamId, userId, value) {
173
- userCache.set(userCacheKey(teamId, userId), {
174
- value,
175
- expiresAt: Date.now() + USER_CACHE_TTL_MS
176
- });
153
+ /** Return the Slack workspace team ID for the current inbound webhook. */
154
+ function getWorkspaceTeamId() {
155
+ return workspaceTeamIdStorage.getStore();
177
156
  }
178
- /** Fetch Slack user profile info with in-memory TTL cache to avoid repeated API calls. */
179
- async function lookupSlackUser(teamId, userId) {
180
- if (!teamId || !userId) return null;
181
- const cached = readFromCache(teamId, userId);
182
- if (cached) return cached;
183
- const token = getSlackBotToken();
184
- if (!token) return null;
157
+ //#endregion
158
+ //#region src/chat/slack/client.ts
159
+ var SlackActionError = class extends Error {
160
+ code;
161
+ apiError;
162
+ needed;
163
+ provided;
164
+ statusCode;
165
+ requestId;
166
+ errorData;
167
+ retryAfterSeconds;
168
+ detail;
169
+ detailLine;
170
+ detailRule;
171
+ constructor(message, code, options = {}) {
172
+ super(message);
173
+ this.name = "SlackActionError";
174
+ this.code = code;
175
+ this.apiError = options.apiError;
176
+ this.needed = options.needed;
177
+ this.provided = options.provided;
178
+ this.statusCode = options.statusCode;
179
+ this.requestId = options.requestId;
180
+ this.errorData = options.errorData;
181
+ this.retryAfterSeconds = options.retryAfterSeconds;
182
+ this.detail = options.detail;
183
+ this.detailLine = options.detailLine;
184
+ this.detailRule = options.detailRule;
185
+ }
186
+ };
187
+ function serializeSlackErrorData(data) {
188
+ if (!data || typeof data !== "object") return;
189
+ const filtered = Object.fromEntries(Object.entries(data).filter(([key]) => key !== "error"));
190
+ if (Object.keys(filtered).length === 0) return;
185
191
  try {
186
- const response = await fetch(`https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`, { headers: { authorization: `Bearer ${token}` } });
187
- if (!response.ok) {
188
- logWarn("slack.user.lookup.failed", {
189
- "enduser.id": userId,
190
- "app.slack.team_id": teamId,
191
- "http.response.status_code": response.status
192
- });
193
- return null;
192
+ const serialized = JSON.stringify(filtered);
193
+ return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
194
+ } catch {
195
+ return;
196
+ }
197
+ }
198
+ /** Extract a header value by case-insensitive name from a raw headers object. */
199
+ function getHeaderString(headers, name) {
200
+ if (!headers || typeof headers !== "object") return;
201
+ const key = name.toLowerCase();
202
+ const entries = headers;
203
+ for (const [entryKey, value] of Object.entries(entries)) {
204
+ if (entryKey.toLowerCase() !== key) continue;
205
+ if (typeof value === "string") return value;
206
+ if (Array.isArray(value)) {
207
+ const first = value.find((entry) => typeof entry === "string");
208
+ return typeof first === "string" ? first : void 0;
194
209
  }
195
- const parsed = slackUserInfoSchema.safeParse(await response.json());
196
- if (!parsed.success) return null;
197
- const payload = parsed.data;
198
- const result = {
199
- userName: payload.user.name?.trim() || void 0,
200
- fullName: payload.user.profile?.real_name?.trim() || void 0,
201
- email: payload.user.profile?.email?.trim() || void 0,
202
- timezone: payload.user.tz?.trim() || void 0
203
- };
204
- writeToCache(teamId, userId, result);
205
- return result;
206
- } catch (error) {
207
- logWarn("slack.user.lookup.failed", {
208
- "enduser.id": userId,
209
- "app.slack.team_id": teamId,
210
- "exception.message": error instanceof Error ? error.message : String(error)
211
- });
212
- return null;
213
210
  }
214
211
  }
215
- //#endregion
216
- //#region src/chat/identities/timezone.ts
217
- const DEFAULT_TIMEZONE = "America/Los_Angeles";
218
- /** The install timezone, used when Junior does not know a person's timezone. */
219
- function defaultTimezone() {
220
- return process.env.JUNIOR_TIMEZONE?.trim() || DEFAULT_TIMEZONE;
212
+ function parseSlackCanvasDetail(detail) {
213
+ if (typeof detail !== "string") return {};
214
+ const trimmed = detail.trim();
215
+ if (!trimmed) return {};
216
+ const parsed = { detail: trimmed };
217
+ const lineMatch = trimmed.match(/line\s+(\d+):/i);
218
+ if (lineMatch) {
219
+ const line = Number.parseInt(lineMatch[1] ?? "", 10);
220
+ if (Number.isFinite(line)) parsed.detailLine = line;
221
+ }
222
+ if (/unsupported heading depth/i.test(trimmed)) parsed.detailRule = "unsupported_heading_depth";
223
+ return parsed;
221
224
  }
222
225
  /**
223
- * Pick a person's timezone: their Slack profile, then the saved user
224
- * timezone, then the install default. A valid Slack timezone is saved on the
225
- * linked user, so later lookups without Slack still find it.
226
+ * Ambient destination-installation token for Slack outbound calls, bound by
227
+ * installation-scoped entry points so writes carry the destination
228
+ * workspace's credentials instead of the process-global env token.
226
229
  */
227
- async function resolveUserTimezone(args) {
228
- const timezone = (args.slack ? await lookupSlackUser(args.slack.teamId, args.slack.userId) : void 0)?.timezone;
229
- if (timezone && isValidTimeZone(timezone)) {
230
- if (args.userId) await saveUserTimezone(args.db, args.userId, timezone, args.nowMs);
231
- return timezone;
232
- }
233
- return (args.userId ? await readUserTimezone(args.db, args.userId) : void 0) ?? defaultTimezone();
234
- }
235
- //#endregion
236
- //#region src/chat/plugins/annotations.ts
237
- function annotationFromRow(row) {
238
- const parsed = conversationAnnotationInputSchema.safeParse(row.annotation);
239
- if (!parsed.success) return void 0;
240
- return {
241
- ...parsed.data,
242
- plugin: row.plugin,
243
- createdAt: row.createdAt.toISOString(),
244
- updatedAt: row.updatedAt.toISOString()
245
- };
230
+ const installationTokenStorage = new AsyncLocalStorage();
231
+ const clientsByToken = /* @__PURE__ */ new Map();
232
+ /**
233
+ * Bind a workspace installation token for all Slack Web API calls inside
234
+ * `fn`, so multi-workspace outbound writes use the destination workspace's
235
+ * credentials rather than the env fallback token.
236
+ */
237
+ function runWithSlackInstallationToken(token, fn) {
238
+ const trimmed = token.trim();
239
+ if (!trimmed) throw new SlackActionError("Slack installation token binding requires a non-empty token", "missing_token");
240
+ return installationTokenStorage.run({ token: trimmed }, fn);
246
241
  }
247
- function createPluginAnnotations(args) {
248
- return {
249
- async upsert(input) {
250
- const annotation = conversationAnnotationInputSchema.parse(input);
251
- const now = /* @__PURE__ */ new Date();
252
- await args.db.insert(juniorConversationAnnotations).values({
253
- annotation,
254
- conversationId: args.conversationId,
255
- createdAt: now,
256
- key: annotation.key,
257
- kind: annotation.kind,
258
- plugin: args.plugin,
259
- updatedAt: now
260
- }).onConflictDoUpdate({
261
- target: [
262
- juniorConversationAnnotations.conversationId,
263
- juniorConversationAnnotations.plugin,
264
- juniorConversationAnnotations.kind,
265
- juniorConversationAnnotations.key
266
- ],
267
- set: {
268
- annotation,
269
- updatedAt: now
270
- }
271
- });
272
- },
273
- async remove(kind, key) {
274
- 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)));
275
- },
276
- async list() {
277
- return (await listConversationAnnotations(args.db, args.conversationId)).filter((a) => a.plugin === args.plugin);
278
- }
279
- };
242
+ /**
243
+ * Token precedence: ambient installation token, then env token, then a hard
244
+ * failure for team-scoped calls — a call scoped to one workspace must never
245
+ * silently fall through to another workspace's credentials.
246
+ */
247
+ function resolveSlackToken() {
248
+ const ambientToken = installationTokenStorage.getStore()?.token;
249
+ if (ambientToken) return ambientToken;
250
+ const envToken = getSlackBotToken();
251
+ if (envToken) return envToken;
252
+ const teamId = getWorkspaceTeamId();
253
+ 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");
254
+ throw new SlackActionError("SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack Web API actions in this service", "missing_token");
280
255
  }
281
- /** Load stored annotations for one conversation, newest update first. */
282
- async function listConversationAnnotations(db, conversationId) {
283
- return (await listConversationAnnotationsById(db, [conversationId])).get(conversationId) ?? [];
256
+ /** Normalize Junior Slack references to native Slack conversation IDs. */
257
+ function normalizeSlackConversationId(channelId) {
258
+ return parseSlackChannelReferenceId(channelId);
284
259
  }
285
- /** Load stored annotations for a bounded set of conversations. */
286
- async function listConversationAnnotationsById(db, conversationIds) {
287
- const byConversation = /* @__PURE__ */ new Map();
288
- if (conversationIds.length === 0) return byConversation;
289
- const rows = await db.select().from(juniorConversationAnnotations).where(inArray(juniorConversationAnnotations.conversationId, [...conversationIds])).orderBy(desc(juniorConversationAnnotations.updatedAt));
290
- for (const row of rows) {
291
- const annotation = annotationFromRow(row);
292
- if (!annotation) continue;
293
- const current = byConversation.get(row.conversationId);
294
- if (current) current.push(annotation);
295
- else byConversation.set(row.conversationId, [annotation]);
260
+ /**
261
+ * Return the per-token cached WebClient. `withSlackRetries` owns retry
262
+ * classification for this boundary: the WebClient's built-in policy would
263
+ * re-post timed-out writes (a duplicate-message hazard) and sleep for the
264
+ * full unbounded Retry-After inside the request, so both internal behaviors
265
+ * are disabled here.
266
+ */
267
+ function getClient() {
268
+ const token = resolveSlackToken();
269
+ let cached = clientsByToken.get(token);
270
+ if (!cached) {
271
+ cached = new WebClient(token, {
272
+ retryConfig: { retries: 0 },
273
+ rejectRateLimitedCalls: true
274
+ });
275
+ clientsByToken.set(token, cached);
296
276
  }
297
- return byConversation;
277
+ return cached;
298
278
  }
299
- //#endregion
300
- //#region src/chat/conversations/annotation-results.ts
301
- /** Save object annotations and retain returned facts in the tool result. */
302
- async function saveObjectAnnotations(conversationId, plugin, value) {
303
- const annotations = objectAnnotationSchema.array().parse(value);
304
- await getDb().transaction(async (db) => {
305
- const store = createPluginAnnotations({
306
- conversationId,
307
- plugin,
308
- db
309
- });
310
- for (const annotation of annotations) {
311
- await store.upsert(annotation);
312
- await store.remove("resource_link", annotation.key);
313
- }
279
+ function mapSlackError(error) {
280
+ if (error instanceof SlackActionError) return error;
281
+ const candidate = error;
282
+ const apiError = candidate.data?.error;
283
+ const message = candidate.message ?? "Slack action failed";
284
+ const baseOptions = {
285
+ apiError,
286
+ statusCode: candidate.statusCode,
287
+ requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
288
+ errorData: serializeSlackErrorData(candidate.data),
289
+ ...parseSlackCanvasDetail(candidate.data?.detail)
290
+ };
291
+ if (apiError === "missing_scope") return new SlackActionError(message, "missing_scope", {
292
+ ...baseOptions,
293
+ needed: candidate.data?.needed,
294
+ provided: candidate.data?.provided
314
295
  });
315
- return annotations.map((annotation) => ({
316
- ...annotation,
317
- plugin
318
- }));
296
+ if (apiError === "not_in_channel") return new SlackActionError(message, "not_in_channel", baseOptions);
297
+ if (apiError === "restricted_action_read_only_channel") return new SlackActionError(message, "read_only_channel", baseOptions);
298
+ if (apiError === "already_reacted") return new SlackActionError(message, "already_reacted", baseOptions);
299
+ if (apiError === "no_reaction") return new SlackActionError(message, "no_reaction", baseOptions);
300
+ if (apiError === "invalid_arguments") return new SlackActionError(message, "invalid_arguments", baseOptions);
301
+ if (apiError === "invalid_cursor") return new SlackActionError(message, "invalid_arguments", baseOptions);
302
+ if (apiError === "invalid_name") return new SlackActionError(message, "invalid_arguments", baseOptions);
303
+ if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") return new SlackActionError(message, "not_found", baseOptions);
304
+ if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") return new SlackActionError(message, "feature_unavailable", baseOptions);
305
+ if (apiError === "canvas_creation_failed") return new SlackActionError(message, "canvas_creation_failed", baseOptions);
306
+ if (apiError === "canvas_editing_failed") return new SlackActionError(message, "canvas_editing_failed", baseOptions);
307
+ if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) return new SlackActionError(message, "rate_limited", {
308
+ ...baseOptions,
309
+ retryAfterSeconds: candidate.retryAfter
310
+ });
311
+ return new SlackActionError(message, "internal_error", baseOptions);
319
312
  }
320
- /** Save annotations from a successful plugin tool result and attach their cards. */
321
- async function annotateToolResult(conversationId, plugin, result) {
322
- if (!isRecord$1(result) || result.isError === true) return result;
323
- const details = isRecord$1(result.details) ? result.details : result;
324
- if (details.timed_out === true || details.isError === true || !Array.isArray(details.objectAnnotations)) return result;
325
- const cards = await saveObjectAnnotations(conversationId, plugin, details.objectAnnotations);
326
- const { objectAnnotations: _annotations, ...rest } = details;
327
- const annotated = {
328
- ...rest,
329
- objectCards: cards
330
- };
331
- if (isRecord$1(result.details)) return {
332
- ...result,
333
- details: annotated
334
- };
335
- return annotated;
313
+ const MAX_RETRY_DELAY_MS = 1e4;
314
+ const MAX_TOTAL_RETRY_DELAY_MS = 2e4;
315
+ const CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
316
+ "ECONNREFUSED",
317
+ "ECONNRESET",
318
+ "ENOTFOUND",
319
+ "EAI_AGAIN"
320
+ ]);
321
+ const TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
322
+ "ETIMEDOUT",
323
+ "ECONNABORTED",
324
+ "ESOCKETTIMEDOUT"
325
+ ]);
326
+ function findNetworkErrorCode(error, depth = 0) {
327
+ if (!error || typeof error !== "object" || depth > 4) return;
328
+ const candidate = error;
329
+ if (typeof candidate.code === "string" && (CONNECTION_ERROR_CODES.has(candidate.code) || TIMEOUT_ERROR_CODES.has(candidate.code))) return candidate.code;
330
+ return findNetworkErrorCode(candidate.original, depth + 1) ?? findNetworkErrorCode(candidate.cause, depth + 1);
336
331
  }
337
- //#endregion
338
- //#region src/chat/conversations/sql/privacy.ts
339
- const privacyRoot = alias(juniorConversations, "privacy_root");
340
- /** Accept only a structurally valid persisted root as privacy authority. */
341
- async function readRootCandidate(executor, conversationId) {
342
- const row = (await executor.db().select({
343
- requestedConversationId: juniorConversations.conversationId,
344
- requestedParentConversationId: juniorConversations.parentConversationId,
345
- requestedRootConversationId: juniorConversations.rootConversationId,
346
- rootConversationId: privacyRoot.conversationId,
347
- rootDestinationId: privacyRoot.destinationId,
348
- rootParentConversationId: privacyRoot.parentConversationId,
349
- rootRootConversationId: privacyRoot.rootConversationId
350
- }).from(juniorConversations).leftJoin(privacyRoot, eq(privacyRoot.conversationId, juniorConversations.rootConversationId)).where(eq(juniorConversations.conversationId, conversationId)))[0];
351
- return row?.requestedRootConversationId !== null && (row?.requestedParentConversationId !== null || row?.requestedRootConversationId === row?.requestedConversationId) && row?.rootConversationId === row.requestedRootConversationId && row.rootRootConversationId === row.rootConversationId && row.rootParentConversationId === null && row.rootDestinationId !== null ? {
352
- destinationId: row.rootDestinationId,
353
- rootConversationId: row.rootConversationId
354
- } : void 0;
332
+ function hasSocketHangUpMessage(error) {
333
+ return error instanceof Error && error.message.toLowerCase().includes("socket hang up");
355
334
  }
356
- async function readCandidateVisibility(executor, candidate) {
357
- const destinations = await executor.db().select({ visibility: juniorDestinations.visibility }).from(juniorDestinations).where(eq(juniorDestinations.id, candidate.destinationId)).for("share");
358
- return {
359
- rootConversationId: candidate.rootConversationId,
360
- visibility: destinations[0]?.visibility ?? null
361
- };
335
+ /**
336
+ * Classify a failed Slack call for retry: rate limits, Slack 5xx, and
337
+ * connection-phase failures are always retryable; timeouts are retried only
338
+ * for idempotent operations because Slack may already have accepted the
339
+ * request.
340
+ */
341
+ function classifySlackRetry(raw, mapped) {
342
+ if (mapped.code === "rate_limited") return "rate_limited";
343
+ if (mapped.statusCode !== void 0 && mapped.statusCode >= 500) return "server_error";
344
+ const networkCode = findNetworkErrorCode(raw);
345
+ if (networkCode && CONNECTION_ERROR_CODES.has(networkCode)) return "connection";
346
+ if (networkCode && TIMEOUT_ERROR_CODES.has(networkCode)) return "timeout";
347
+ if (hasSocketHangUpMessage(raw)) return "connection";
348
+ return "none";
362
349
  }
363
350
  /**
364
- * Resolve a conversation's privacy authority from its persisted root.
351
+ * Run a Slack Web API call with bounded retries so transient platform
352
+ * failures do not surface as turn failures, while non-idempotent posts are
353
+ * never re-sent when Slack may already have accepted them.
365
354
  *
366
- * Missing or structurally invalid root metadata fails closed. The root
367
- * destination is locked so callers that keep the transaction open receive a
368
- * stable visibility decision.
355
+ * Retry classes: rate limits (bounded Retry-After), connection-phase network
356
+ * failures, and Slack 5xx responses always retry; request timeouts retry only
357
+ * when the caller marks the operation `idempotent`.
369
358
  */
370
- async function resolveRootVisibility(executor, conversationId) {
371
- const candidate = await readRootCandidate(executor, conversationId);
372
- if (!candidate) return {
373
- rootConversationId: conversationId,
374
- visibility: null
375
- };
376
- return readCandidateVisibility(executor, candidate);
377
- }
378
- //#endregion
379
- //#region src/chat/briefs/events.ts
380
- /** Durable cost and size record for one stored Brief version. */
381
- const briefUpdatedEvent = defineConversationEvent({
382
- name: "brief_updated",
383
- version: 1,
384
- schema: z.object({
385
- version: z.number().int().positive(),
386
- modelId: z.string().min(1),
387
- costUsd: z.number().finite().nonnegative().optional(),
388
- decisions: z.number().int().nonnegative(),
389
- openDecisions: z.number().int().nonnegative(),
390
- links: z.number().int().nonnegative()
391
- }).strict(),
392
- renderEvent(event) {
393
- return {
394
- icon: "activity",
395
- title: `Brief updated (v${event.version})`
396
- };
359
+ async function withSlackRetries(task, maxAttempts = 3, context = {}) {
360
+ let attempt = 0;
361
+ let totalDelayMs = 0;
362
+ const action = context.action ?? "unknown";
363
+ while (attempt < maxAttempts) {
364
+ attempt += 1;
365
+ const attemptNumber = attempt;
366
+ try {
367
+ return await withSpan(`POST slack.com/api/${action}`, "http.client", {}, async () => {
368
+ try {
369
+ return await task();
370
+ } catch (error) {
371
+ const mapped = mapSlackError(error);
372
+ const errorAttrs = { "error.type": mapped.code };
373
+ if (mapped.apiError) errorAttrs["app.slack.api_error_code"] = mapped.apiError;
374
+ if (mapped.code === "rate_limited") {
375
+ errorAttrs["http.response.status_code"] = 429;
376
+ if (mapped.retryAfterSeconds) errorAttrs["app.slack.retry_after_ms"] = mapped.retryAfterSeconds * 1e3;
377
+ } else if (mapped.statusCode != null) errorAttrs["http.response.status_code"] = mapped.statusCode;
378
+ setSpanAttributes(errorAttrs);
379
+ setSpanStatus("error");
380
+ throw error;
381
+ }
382
+ }, {
383
+ "http.request.method": "POST",
384
+ "server.address": "slack.com",
385
+ "url.scheme": "https",
386
+ "url.path": `/api/${action}`,
387
+ "app.slack.method": action,
388
+ "app.retry.max_attempts": maxAttempts,
389
+ ...attemptNumber > 1 ? { "http.request.resend_count": attemptNumber - 1 } : void 0,
390
+ ...context.attributes ?? {},
391
+ ...context.spanAttributes ?? {}
392
+ });
393
+ } catch (error) {
394
+ const mapped = mapSlackError(error);
395
+ const retryClass = classifySlackRetry(error, mapped);
396
+ const isRetryable = retryClass === "rate_limited" || retryClass === "connection" || retryClass === "server_error" || retryClass === "timeout" && context.idempotent === true;
397
+ const remainingDelayBudgetMs = MAX_TOTAL_RETRY_DELAY_MS - totalDelayMs;
398
+ const baseLogAttributes = {
399
+ "app.slack.action": action,
400
+ "app.slack.error_code": mapped.code,
401
+ ...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : void 0,
402
+ ...mapped.detail ? { "app.slack.detail": mapped.detail } : void 0,
403
+ ...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : void 0,
404
+ ...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : void 0,
405
+ ...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : void 0,
406
+ ...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : void 0,
407
+ ...context.attributes ?? {}
408
+ };
409
+ if (!isRetryable || attempt >= maxAttempts || remainingDelayBudgetMs <= 0) {
410
+ logWarn("slack.action.failed", {
411
+ ...baseLogAttributes,
412
+ ...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : void 0
413
+ });
414
+ throw mapped;
415
+ }
416
+ logWarn("slack.action.retrying", {
417
+ ...baseLogAttributes,
418
+ "app.slack.retry_attempt": attempt,
419
+ "app.slack.retry_class": retryClass
420
+ });
421
+ const retryAfterMs = retryClass === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
422
+ const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
423
+ const delayMs = Math.min(retryAfterMs ?? backoffMs, MAX_RETRY_DELAY_MS, remainingDelayBudgetMs);
424
+ totalDelayMs += delayMs;
425
+ await sleep$1(delayMs);
426
+ }
397
427
  }
398
- });
399
- //#endregion
400
- //#region src/chat/slack/attachments.ts
401
- /** Save the vision summary for a stored Slack attachment. */
402
- async function recordSlackAttachment(args) {
403
- 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)));
404
- }
405
- /** Read a saved vision summary for one live attachment. */
406
- async function readAttachmentVisionSummary(args) {
407
- 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)));
408
- return row?.visionSummary;
409
- }
410
- /** Match live conversation attachments to Slack file ids. */
411
- async function matchSlackAttachments(args) {
412
- if (args.providerIds.length === 0) return [];
413
- return await args.db.db().select({
414
- id: juniorAttachments.id,
415
- providerId: juniorAttachments.providerId,
416
- bytes: juniorAttachments.bytes,
417
- contentType: juniorAttachments.contentType,
418
- filename: juniorAttachments.filename
419
- }).from(juniorAttachments).where(and(eq(juniorAttachments.conversationId, args.conversationId), eq(juniorAttachments.provider, "slack"), inArray(juniorAttachments.providerId, args.providerIds), isNull(juniorAttachments.deleteRequestedAt)));
428
+ throw new SlackActionError("Slack action exhausted retries", "internal_error");
420
429
  }
421
- /** Read the Slack file identity retained by the Chat SDK. */
422
- function getSlackFileId(attachment) {
423
- const metadataId = attachment.fetchMetadata?.fileId;
424
- if (metadataId) return metadataId;
425
- return (attachment.url ?? attachment.fetchMetadata?.url ?? "").match(/(?:^|[-/])(F[A-Z0-9]+)(?:[-/]|$)/i)?.[1];
426
- }
427
- //#endregion
428
- //#region src/api/conversations/message-attachments.ts
429
- /** Resolve saved Slack file references without exposing private storage details. */
430
- async function hydrateMessageAttachments(db, conversationId, events) {
431
- const messages = events.flatMap((event) => event.data.type === "message" ? [event.data] : []);
432
- const ids = [...new Set(messages.flatMap((message) => slackFileIds(message.meta)))];
433
- if (!ids.length) return;
434
- const attachments = await matchSlackAttachments({
435
- db,
436
- conversationId,
437
- providerIds: ids
430
+ /**
431
+ * Slack Web API client for the current destination workspace: the ambient
432
+ * installation token when one is bound, otherwise the env bot token for
433
+ * single-workspace deployments. Fails instead of falling back when the call
434
+ * is workspace-scoped and no installation token can be resolved, so a write
435
+ * never goes out with another workspace's credentials.
436
+ */
437
+ function getSlackClient(options) {
438
+ if (options) return new WebClient(resolveSlackToken(), {
439
+ retryConfig: { retries: 0 },
440
+ rejectRateLimitedCalls: true,
441
+ timeout: options.timeoutMs
438
442
  });
439
- const byFileId = new Map(attachments.map(({ providerId, ...attachment }) => [providerId, attachment]));
440
- for (const message of messages) {
441
- const stored = readMessageAttachments(message.meta?.attachments);
442
- const matched = slackFileIds(message.meta).flatMap((id) => {
443
- const attachment = byFileId.get(id);
444
- return attachment ? [attachment] : [];
445
- });
446
- if (matched.length) message.meta = {
447
- ...message.meta,
448
- attachments: [...stored, ...matched]
449
- };
450
- }
443
+ return getClient();
451
444
  }
452
- function slackFileIds(meta) {
453
- const ids = meta?.slackFileIds ?? meta?.imageFileIds;
454
- return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
445
+ /**
446
+ * Slack channel ID prefixes:
447
+ * - C: channel — modern private channels also use C, so the prefix never
448
+ * proves a channel is public
449
+ * - G: legacy private channel / group DM
450
+ * - D: direct message (1:1)
451
+ */
452
+ function isDmChannel(channelId) {
453
+ const normalized = normalizeSlackConversationId(channelId);
454
+ return Boolean(normalized && normalized.startsWith("D"));
455
455
  }
456
- //#endregion
457
- //#region src/chat/sentry-links.ts
458
- function getSentryOrgSlug() {
459
- return process.env.SENTRY_ORG_SLUG?.trim() || void 0;
456
+ /**
457
+ * Conversation-scoped Slack contexts backed by a concrete conversation ID.
458
+ * Includes channels/groups/DMs (C/G/D).
459
+ */
460
+ function isConversationScopedChannel(channelId) {
461
+ const normalized = normalizeSlackConversationId(channelId);
462
+ if (!normalized) return false;
463
+ return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
460
464
  }
461
- function isSentrySaasDsnHost(host) {
462
- return host === "sentry.io" || host.endsWith(".sentry.io");
465
+ function isConversationChannel(channelId) {
466
+ const normalized = normalizeSlackConversationId(channelId);
467
+ if (!normalized) return false;
468
+ return normalized.startsWith("C") || normalized.startsWith("G");
463
469
  }
464
- function buildSentryWebBaseUrl(dsn) {
465
- if (isSentrySaasDsnHost(dsn.host)) return "https://sentry.io";
466
- const port = dsn.port ? `:${dsn.port}` : "";
467
- const path = dsn.path ? `/${dsn.path}` : "";
468
- return `${dsn.protocol}://${dsn.host}${port}${path}`;
470
+ async function getFilePermalink(fileId) {
471
+ const client = getClient();
472
+ return (await withSlackRetries(() => client.files.info({ file: fileId }), 3, {
473
+ action: "files.info",
474
+ idempotent: true,
475
+ spanAttributes: { "app.slack.file_id": fileId }
476
+ })).file?.permalink;
469
477
  }
470
- function buildSentryOrgPath(args) {
471
- const dsn = getClient$1()?.getDsn();
472
- if (!dsn?.host || !dsn.projectId) return;
473
- const orgSlug = getSentryOrgSlug();
474
- if (!orgSlug) return;
475
- const params = new URLSearchParams();
476
- params.set("project", dsn.projectId);
477
- if (args.query) for (const [key, value] of Object.entries(args.query)) params.set(key, value);
478
- const path = `${args.path}?${params.toString()}`;
479
- if (isSentrySaasDsnHost(dsn.host)) return `https://${orgSlug}.sentry.io/${path}`;
480
- return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path}`;
478
+ async function downloadPrivateSlackFile(url) {
479
+ const token = resolveSlackToken();
480
+ return withSpan("GET files.slack.com", "http.client", {}, async () => {
481
+ const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
482
+ setSpanAttributes({ "http.response.status_code": response.status });
483
+ if (!response.ok) {
484
+ setSpanAttributes({ "error.type": String(response.status) });
485
+ setSpanStatus("error");
486
+ throw new Error(`Slack file download failed: ${response.status}`);
487
+ }
488
+ return Buffer.from(await response.arrayBuffer());
489
+ }, {
490
+ "http.request.method": "GET",
491
+ "server.address": "files.slack.com",
492
+ "url.scheme": "https",
493
+ "app.slack.method": "files.download"
494
+ });
481
495
  }
482
- /** Build a Sentry conversation URL only when the runtime has enough Sentry config. */
483
- function buildSentryConversationUrl(conversationId) {
484
- return buildSentryOrgPath({ path: `explore/conversations/${encodeURIComponent(conversationId)}/` });
496
+ //#endregion
497
+ //#region src/chat/slack/user.ts
498
+ const slackUserInfoSchema = z.object({
499
+ ok: z.literal(true),
500
+ user: z.object({
501
+ name: z.string().nullish(),
502
+ tz: z.string().nullish(),
503
+ profile: z.object({
504
+ email: z.string().nullish(),
505
+ real_name: z.string().nullish()
506
+ }).optional()
507
+ })
508
+ });
509
+ const USER_CACHE_TTL_MS = 3e5;
510
+ const userCache = /* @__PURE__ */ new Map();
511
+ function userCacheKey(teamId, userId) {
512
+ return `${teamId}:${userId}`;
485
513
  }
486
- /** Build a Sentry event URL only when the runtime has enough Sentry config. */
487
- function buildSentryEventUrl(eventId) {
488
- return buildSentryOrgPath({
489
- path: "issues/",
490
- query: { query: eventId }
514
+ function readFromCache(teamId, userId) {
515
+ const hit = userCache.get(userCacheKey(teamId, userId));
516
+ if (!hit) return null;
517
+ if (hit.expiresAt < Date.now()) {
518
+ userCache.delete(userCacheKey(teamId, userId));
519
+ return null;
520
+ }
521
+ return hit.value;
522
+ }
523
+ function writeToCache(teamId, userId, value) {
524
+ userCache.set(userCacheKey(teamId, userId), {
525
+ value,
526
+ expiresAt: Date.now() + USER_CACHE_TTL_MS
491
527
  });
492
528
  }
493
- //#endregion
494
- //#region src/api/conversations/event-model.ts
495
- /** Apply recorded model choices without carrying a previous turn's settings forward. */
496
- function conversationEventModel(data, current) {
497
- if (data.type === "turn_started") return void 0;
498
- if (data.type === "turn_routed" || data.type === "handoff") return {
499
- modelId: data.modelId,
500
- modelProfile: data.modelProfile,
501
- ...data.reasoningLevel ? { reasoningLevel: data.reasoningLevel } : void 0
502
- };
503
- if (data.type === "assistant_message") {
504
- const model = text$1(data.model);
505
- const provider = text$1(data.provider);
506
- if (model) {
507
- const modelId = model.includes("/") || !provider ? model : `${provider}/${model}`;
508
- return {
509
- ...current,
510
- modelId
511
- };
529
+ /** Fetch Slack user profile info with in-memory TTL cache to avoid repeated API calls. */
530
+ async function lookupSlackUser(teamId, userId) {
531
+ if (!teamId || !userId) return null;
532
+ const cached = readFromCache(teamId, userId);
533
+ if (cached) return cached;
534
+ const token = getSlackBotToken();
535
+ if (!token) return null;
536
+ try {
537
+ const response = await fetch(`https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`, { headers: { authorization: `Bearer ${token}` } });
538
+ if (!response.ok) {
539
+ logWarn("slack.user.lookup.failed", {
540
+ "enduser.id": userId,
541
+ "app.slack.team_id": teamId,
542
+ "http.response.status_code": response.status
543
+ });
544
+ return null;
512
545
  }
546
+ const parsed = slackUserInfoSchema.safeParse(await response.json());
547
+ if (!parsed.success) return null;
548
+ const payload = parsed.data;
549
+ const result = {
550
+ userName: payload.user.name?.trim() || void 0,
551
+ fullName: payload.user.profile?.real_name?.trim() || void 0,
552
+ email: payload.user.profile?.email?.trim() || void 0,
553
+ timezone: payload.user.tz?.trim() || void 0
554
+ };
555
+ writeToCache(teamId, userId, result);
556
+ return result;
557
+ } catch (error) {
558
+ logWarn("slack.user.lookup.failed", {
559
+ "enduser.id": userId,
560
+ "app.slack.team_id": teamId,
561
+ "exception.message": error instanceof Error ? error.message : String(error)
562
+ });
563
+ return null;
513
564
  }
514
- return current;
515
565
  }
516
- function text$1(value) {
517
- return typeof value === "string" && value.length > 0 ? value : void 0;
566
+ //#endregion
567
+ //#region src/chat/identities/timezone.ts
568
+ const DEFAULT_TIMEZONE = "America/Los_Angeles";
569
+ /** The install timezone, used when Junior does not know a person's timezone. */
570
+ function defaultTimezone() {
571
+ return process.env.JUNIOR_TIMEZONE?.trim() || DEFAULT_TIMEZONE;
518
572
  }
519
- /** Allowlist model-call diagnostics; never expose opaque provider metadata. */
520
- function conversationEventModelCall(data) {
521
- if (data.type !== "assistant_message") return void 0;
522
- const usage = extractGenAiUsageSummary(data);
523
- const provider = text$1(data.provider);
524
- const api = text$1(data.api);
525
- const stopReason = text$1(data.stopReason);
573
+ /**
574
+ * Pick a person's timezone: their Slack profile, then the saved user
575
+ * timezone, then the install default. A valid Slack timezone is saved on the
576
+ * linked user, so later lookups without Slack still find it.
577
+ */
578
+ async function resolveUserTimezone(args) {
579
+ const timezone = (args.slack ? await lookupSlackUser(args.slack.teamId, args.slack.userId) : void 0)?.timezone;
580
+ if (timezone && isValidTimeZone(timezone)) {
581
+ if (args.userId) await saveUserTimezone(args.db, args.userId, timezone, args.nowMs);
582
+ return timezone;
583
+ }
584
+ return (args.userId ? await readUserTimezone(args.db, args.userId) : void 0) ?? defaultTimezone();
585
+ }
586
+ //#endregion
587
+ //#region src/chat/plugins/annotations.ts
588
+ function annotationFromRow(row) {
589
+ const parsed = conversationAnnotationInputSchema.safeParse(row.annotation);
590
+ if (!parsed.success) return void 0;
526
591
  return {
527
- ...provider ? { provider } : void 0,
528
- ...api ? { api } : void 0,
529
- ...stopReason ? { stopReason } : void 0,
530
- ...hasAgentTurnUsage(usage) ? { usage } : void 0
592
+ ...parsed.data,
593
+ plugin: row.plugin,
594
+ createdAt: row.createdAt.toISOString(),
595
+ updatedAt: row.updatedAt.toISOString()
531
596
  };
532
597
  }
533
- //#endregion
534
- //#region src/api/conversations/events.ts
535
- /** Canonical event types that can contribute to the reporting projection. */
536
- const conversationReportSourceEventTypes = [
537
- "message",
538
- "message_handled",
539
- "assistant_message",
540
- "tool_result",
541
- "tool_execution_started",
542
- "guardian_action_reviewed",
543
- "turn_started",
544
- "turn_context",
545
- "structured_event",
546
- "attachments_delivered",
547
- "turn_routed",
548
- "turn_completed",
549
- "turn_failed",
550
- "compaction",
551
- "handoff",
552
- "subagent_started",
553
- "subagent_ended"
554
- ];
555
- const reportingAssistantMessageSchema = z.object({
556
- type: z.literal("assistant_message"),
557
- content: z.array(z.unknown())
558
- }).passthrough();
559
- const reportingToolCallPartSchema = z.object({
560
- type: z.literal("toolCall"),
561
- id: z.string().min(1),
562
- name: z.string().min(1),
563
- arguments: z.unknown()
564
- }).passthrough();
565
- const reportingToolResultMessageSchema = z.object({
566
- type: z.literal("tool_result"),
567
- toolCallId: z.string().min(1),
568
- toolName: z.string().min(1).optional(),
569
- name: z.string().min(1).optional(),
570
- content: z.array(z.unknown()),
571
- details: z.unknown().optional(),
572
- isError: z.boolean().optional()
573
- }).passthrough();
574
- const reportingToolResultDetailsSchema = z.object({
575
- ok: z.boolean().optional(),
576
- status: z.string().optional()
577
- }).passthrough();
578
- const reportingTextPartSchema = z.object({
579
- type: z.literal("text"),
580
- text: z.string()
581
- }).passthrough();
582
- const reportingReasoningPartSchema = z.object({
583
- type: z.literal("thinking"),
584
- thinking: z.string(),
585
- redacted: z.literal(true).optional()
586
- }).passthrough();
587
- const reportingMediaPartSchema = z.object({
588
- type: z.enum(["image", "audio"]),
589
- mimeType: z.string().min(1).optional()
590
- }).passthrough();
591
- const reportingMessageAuthorSchema = z.object({
592
- fullName: z.string().min(1).optional(),
593
- userId: z.string().min(1).optional(),
594
- userName: z.string().min(1).optional()
595
- }).passthrough();
596
- /** Retain model-visible text and media descriptors, dropping all opaque fields. */
597
- function sanitizeModelContentPart(value) {
598
- const text = reportingTextPartSchema.safeParse(value);
599
- if (text.success) return {
600
- type: "text",
601
- text: text.data.text
602
- };
603
- const media = reportingMediaPartSchema.safeParse(value);
604
- if (!media.success) return void 0;
598
+ function createPluginAnnotations(args) {
605
599
  return {
606
- type: media.data.type,
607
- ...media.data.mimeType ? { mimeType: media.data.mimeType } : void 0
600
+ async upsert(input) {
601
+ const annotation = conversationAnnotationInputSchema.parse(input);
602
+ const now = /* @__PURE__ */ new Date();
603
+ await args.db.insert(juniorConversationAnnotations).values({
604
+ annotation,
605
+ conversationId: args.conversationId,
606
+ createdAt: now,
607
+ key: annotation.key,
608
+ kind: annotation.kind,
609
+ plugin: args.plugin,
610
+ updatedAt: now
611
+ }).onConflictDoUpdate({
612
+ target: [
613
+ juniorConversationAnnotations.conversationId,
614
+ juniorConversationAnnotations.plugin,
615
+ juniorConversationAnnotations.kind,
616
+ juniorConversationAnnotations.key
617
+ ],
618
+ set: {
619
+ annotation,
620
+ updatedAt: now
621
+ }
622
+ });
623
+ },
624
+ async remove(kind, key) {
625
+ 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)));
626
+ },
627
+ async list() {
628
+ return (await listConversationAnnotations(args.db, args.conversationId)).filter((a) => a.plugin === args.plugin);
629
+ }
608
630
  };
609
631
  }
610
- /** Convert allowlisted Pi content parts into the reporting result value. */
611
- function modelVisibleToolOutput(content) {
612
- const sanitized = content.map(sanitizeModelContentPart).filter((part) => part !== void 0);
613
- if (sanitized.length === 0) return void 0;
614
- if (sanitized.length !== 1) return sanitized;
615
- const only = sanitized[0];
616
- return only.type === "text" ? only.text : only;
632
+ /** Load stored annotations for one conversation, newest update first. */
633
+ async function listConversationAnnotations(db, conversationId) {
634
+ return (await listConversationAnnotationsById(db, [conversationId])).get(conversationId) ?? [];
617
635
  }
618
- function reportMessageActorIdentity(data) {
619
- if (data.role !== "user") return void 0;
620
- const author = reportingMessageAuthorSchema.safeParse(data.meta?.author);
621
- if (!author.success) return void 0;
622
- const actorIdentity = {
623
- ...author.data.fullName ? { fullName: author.data.fullName } : void 0,
624
- ...author.data.userId ? { slackUserId: author.data.userId } : void 0,
625
- ...author.data.userName ? { slackUserName: author.data.userName } : void 0
626
- };
627
- return Object.keys(actorIdentity).length > 0 ? actorIdentity : void 0;
636
+ /** Load stored annotations for a bounded set of conversations. */
637
+ async function listConversationAnnotationsById(db, conversationIds) {
638
+ const byConversation = /* @__PURE__ */ new Map();
639
+ if (conversationIds.length === 0) return byConversation;
640
+ const rows = await db.select().from(juniorConversationAnnotations).where(inArray(juniorConversationAnnotations.conversationId, [...conversationIds])).orderBy(desc(juniorConversationAnnotations.updatedAt));
641
+ for (const row of rows) {
642
+ const annotation = annotationFromRow(row);
643
+ if (!annotation) continue;
644
+ const current = byConversation.get(row.conversationId);
645
+ if (current) current.push(annotation);
646
+ else byConversation.set(row.conversationId, [annotation]);
647
+ }
648
+ return byConversation;
628
649
  }
629
- /** Project native assistant tool calls through the existing reporting shape. */
630
- function reportToolCalls(args) {
631
- const message = reportingAssistantMessageSchema.safeParse(args.message);
632
- if (!message.success) return void 0;
633
- const calls = message.data.content.flatMap((part) => {
634
- const call = reportingToolCallPartSchema.safeParse(part);
635
- if (!call.success) return [];
636
- return [{
637
- toolCallId: call.data.id,
638
- name: call.data.name,
639
- status: "running",
640
- startedAt: new Date(args.createdAtMs).toISOString(),
641
- startedSeq: args.seq,
642
- ...args.canExposePayload && call.data.arguments !== void 0 ? { input: call.data.arguments } : void 0
643
- }];
650
+ //#endregion
651
+ //#region src/chat/conversations/annotation-results.ts
652
+ /** Save object annotations and retain returned facts in the tool result. */
653
+ async function saveObjectAnnotations(conversationId, plugin, value) {
654
+ const annotations = objectAnnotationSchema.array().parse(value);
655
+ await getDb().transaction(async (db) => {
656
+ const store = createPluginAnnotations({
657
+ conversationId,
658
+ plugin,
659
+ db
660
+ });
661
+ for (const annotation of annotations) {
662
+ await store.upsert(annotation);
663
+ await store.remove("resource_link", annotation.key);
664
+ }
644
665
  });
645
- return calls.length > 0 ? {
646
- type: "tool_calls",
647
- calls
648
- } : void 0;
666
+ return annotations.map((annotation) => ({
667
+ ...annotation,
668
+ plugin
669
+ }));
649
670
  }
650
- /** Project assistant reasoning with tool ids retained only for display order. */
651
- function reportAssistantMessage(args) {
652
- const message = reportingAssistantMessageSchema.safeParse(args.message);
653
- if (!message.success) return void 0;
654
- if (!message.data.content.some((part) => {
655
- const reasoning = reportingReasoningPartSchema.safeParse(part);
656
- return reasoning.success && reasoning.data.redacted !== true && reasoning.data.thinking.trim().length > 0;
657
- })) return reportToolCalls(args) ?? {
658
- type: "assistant_message",
659
- parts: []
671
+ /** Save annotations from a successful plugin tool result and attach their cards. */
672
+ async function annotateToolResult(conversationId, plugin, result) {
673
+ if (!isRecord$1(result) || result.isError === true) return result;
674
+ const details = isRecord$1(result.details) ? result.details : result;
675
+ if (details.timed_out === true || details.isError === true || !Array.isArray(details.objectAnnotations)) return result;
676
+ const cards = await saveObjectAnnotations(conversationId, plugin, details.objectAnnotations);
677
+ const { objectAnnotations: _annotations, ...rest } = details;
678
+ const annotated = {
679
+ ...rest,
680
+ objectCards: cards
660
681
  };
661
- const reasoningParts = [];
662
- const assistantParts = [];
663
- for (const part of message.data.content) {
664
- const reasoning = reportingReasoningPartSchema.safeParse(part);
665
- if (reasoning.success) {
666
- if (reasoning.data.redacted !== true && reasoning.data.thinking.trim().length > 0) {
667
- const projected = {
668
- type: "reasoning",
669
- ...args.canExposePayload ? { text: reasoning.data.thinking } : { redacted: true }
670
- };
671
- reasoningParts.push(projected);
672
- assistantParts.push(projected);
673
- }
674
- continue;
675
- }
676
- const call = reportingToolCallPartSchema.safeParse(part);
677
- if (call.success) assistantParts.push({
678
- type: "tool_call",
679
- toolCallId: call.data.id
680
- });
681
- }
682
- const toolCalls = reportToolCalls(args);
683
- if (toolCalls?.type === "tool_calls") return {
684
- ...toolCalls,
685
- assistant: { parts: assistantParts }
682
+ if (isRecord$1(result.details)) return {
683
+ ...result,
684
+ details: annotated
686
685
  };
687
- return reasoningParts.length > 0 ? {
688
- type: "assistant_message",
689
- parts: reasoningParts
686
+ return annotated;
687
+ }
688
+ //#endregion
689
+ //#region src/chat/conversations/sql/privacy.ts
690
+ const privacyRoot = alias(juniorConversations, "privacy_root");
691
+ /** Accept only a structurally valid persisted root as privacy authority. */
692
+ async function readRootCandidate(executor, conversationId) {
693
+ const row = (await executor.db().select({
694
+ requestedConversationId: juniorConversations.conversationId,
695
+ requestedParentConversationId: juniorConversations.parentConversationId,
696
+ requestedRootConversationId: juniorConversations.rootConversationId,
697
+ rootConversationId: privacyRoot.conversationId,
698
+ rootDestinationId: privacyRoot.destinationId,
699
+ rootParentConversationId: privacyRoot.parentConversationId,
700
+ rootRootConversationId: privacyRoot.rootConversationId
701
+ }).from(juniorConversations).leftJoin(privacyRoot, eq(privacyRoot.conversationId, juniorConversations.rootConversationId)).where(eq(juniorConversations.conversationId, conversationId)))[0];
702
+ return row?.requestedRootConversationId !== null && (row?.requestedParentConversationId !== null || row?.requestedRootConversationId === row?.requestedConversationId) && row?.rootConversationId === row.requestedRootConversationId && row.rootRootConversationId === row.rootConversationId && row.rootParentConversationId === null && row.rootDestinationId !== null ? {
703
+ destinationId: row.rootDestinationId,
704
+ rootConversationId: row.rootConversationId
690
705
  } : void 0;
691
706
  }
692
- /** Project a native tool result without exposing host-only result details. */
693
- function reportToolResult(args) {
694
- const message = reportingToolResultMessageSchema.safeParse(args.message);
695
- if (!message.success) return void 0;
696
- const details = reportingToolResultDetailsSchema.safeParse(message.data.details);
697
- const outcome = message.data.isError === true || details.success && (details.data.ok === false || details.data.status === "error") ? "error" : "completed";
698
- const output = modelVisibleToolOutput(message.data.content);
699
- const name = message.data.toolName ?? message.data.name ?? args.start?.name;
700
- if (!name) return void 0;
707
+ async function readCandidateVisibility(executor, candidate) {
708
+ const destinations = await executor.db().select({ visibility: juniorDestinations.visibility }).from(juniorDestinations).where(eq(juniorDestinations.id, candidate.destinationId)).for("share");
701
709
  return {
702
- type: "tool_calls",
703
- calls: [{
704
- toolCallId: message.data.toolCallId,
705
- name,
706
- status: outcome,
707
- ...args.start ? {
708
- startedAt: new Date(args.start.createdAtMs).toISOString(),
709
- startedSeq: args.start.seq
710
- } : void 0,
711
- ...args.canExposePayload && output !== void 0 ? { output } : void 0
712
- }]
710
+ rootConversationId: candidate.rootConversationId,
711
+ visibility: destinations[0]?.visibility ?? null
713
712
  };
714
713
  }
715
- /** Return tool result ids that may need start metadata from outside a page. */
716
- function conversationReportToolResultIds(events) {
717
- return [...new Set(events.flatMap((event) => {
718
- const result = reportingToolResultMessageSchema.safeParse(event.data);
719
- return result.success ? [result.data.toolCallId] : [];
720
- }))];
714
+ /**
715
+ * Resolve a conversation's privacy authority from its persisted root.
716
+ *
717
+ * Missing or structurally invalid root metadata fails closed. The root
718
+ * destination is locked so callers that keep the transaction open receive a
719
+ * stable visibility decision.
720
+ */
721
+ async function resolveRootVisibility(executor, conversationId) {
722
+ const candidate = await readRootCandidate(executor, conversationId);
723
+ if (!candidate) return {
724
+ rootConversationId: conversationId,
725
+ visibility: null
726
+ };
727
+ return readCandidateVisibility(executor, candidate);
721
728
  }
722
- function reportEventData(args) {
723
- const { data } = args;
724
- if (data.type === "structured_event") {
725
- if (!args.canExposePayload) return void 0;
726
- const presentation = data.namespace === "junior" ? renderJuniorNativeConversationEvent(data) : renderPluginConversationEvent(data);
727
- if (!presentation) return void 0;
729
+ //#endregion
730
+ //#region src/chat/briefs/events.ts
731
+ /** Durable cost and size record for one stored Brief version. */
732
+ const briefUpdatedEvent = defineConversationEvent({
733
+ name: "brief_updated",
734
+ version: 1,
735
+ schema: z.object({
736
+ version: z.number().int().positive(),
737
+ modelId: z.string().min(1),
738
+ costUsd: z.number().finite().nonnegative().optional(),
739
+ decisions: z.number().int().nonnegative(),
740
+ openDecisions: z.number().int().nonnegative(),
741
+ links: z.number().int().nonnegative()
742
+ }).strict(),
743
+ renderEvent(event) {
728
744
  return {
729
- type: "structured_event",
730
- namespace: data.namespace,
731
- name: data.name,
732
- version: data.version,
733
- ...data.turnId ? { turnId: data.turnId } : void 0,
734
- presentation
745
+ icon: "activity",
746
+ title: `Brief updated (v${event.version})`
735
747
  };
736
748
  }
737
- switch (data.type) {
738
- case "message": {
739
- const actorIdentity = args.canExposePayload ? reportMessageActorIdentity(data) : void 0;
740
- return {
741
- type: "message",
742
- messageId: data.messageId,
743
- role: data.role,
744
- ...data.meta?.source === "web" || data.meta?.source === "slack" ? { source: data.meta.source } : void 0,
745
- ...actorIdentity ? { actorIdentity } : void 0,
746
- eventObjectType: objectTypeSchema.safeParse(data.meta?.eventObjectType).data,
747
- ...typeof data.meta?.eventType === "string" ? { eventType: data.meta.eventType } : void 0,
748
- ...typeof data.meta?.explicitMention === "boolean" ? { explicitMention: data.meta.explicitMention } : void 0,
749
- ...args.canExposePayload && typeof data.meta?.trustedSummary === "string" ? { trustedSummary: data.meta.trustedSummary } : void 0,
750
- ...args.canExposePayload && (data.meta?.cards || data.meta?.objectCards) ? { cards: readMessageCardRefs(data.meta) } : void 0,
751
- ...args.canExposePayload ? {
752
- text: data.text,
753
- ...data.meta?.attachments ? { attachments: readMessageAttachments(data.meta.attachments) } : void 0
754
- } : { redacted: true }
755
- };
756
- }
757
- case "message_handled": return {
758
- type: "message_handled",
759
- messageId: data.messageId
760
- };
761
- case "turn_started": return {
762
- type: "turn_lifecycle",
763
- turnId: data.turnId,
764
- state: "started",
765
- ...data.inputMessageIds.length > 0 ? { inputMessageIds: data.inputMessageIds } : void 0
766
- };
767
- case "turn_context":
768
- if (!args.canExposePayload) return;
769
- return {
770
- type: "turn_context",
771
- turnId: data.turnId,
772
- pluginName: data.pluginName,
773
- kind: data.kind,
774
- version: data.version,
775
- content: data.content
776
- };
777
- case "attachments_delivered":
778
- if (!args.canExposePayload) return;
779
- return {
780
- type: "attachments_delivered",
781
- attachments: data.attachments
782
- };
783
- case "turn_routed": return {
784
- type: "turn_routed",
785
- turnId: data.turnId,
786
- modelProfile: data.modelProfile,
787
- modelId: data.modelId,
788
- ...data.costUsd !== void 0 ? { costUsd: data.costUsd } : void 0,
789
- reasoningLevel: data.reasoningLevel,
790
- ...data.confidence !== void 0 ? { confidence: data.confidence } : void 0,
791
- source: data.source
792
- };
793
- case "guardian_action_reviewed": return {
794
- type: "guardian_action_reviewed",
795
- turnId: data.turnId,
796
- toolCallId: data.toolCallId,
797
- toolName: data.toolName,
798
- decision: data.decision,
799
- riskLevel: data.riskLevel,
800
- userAuthorization: data.userAuthorization
801
- };
802
- case "turn_completed": return {
803
- type: "turn_lifecycle",
804
- turnId: data.turnId,
805
- state: data.outcome === "success" ? "succeeded" : "no_reply"
749
+ });
750
+ //#endregion
751
+ //#region src/chat/slack/attachments.ts
752
+ /** Save the vision summary for a stored Slack attachment. */
753
+ async function recordSlackAttachment(args) {
754
+ 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)));
755
+ }
756
+ /** Read a saved vision summary for one live attachment. */
757
+ async function readAttachmentVisionSummary(args) {
758
+ 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)));
759
+ return row?.visionSummary;
760
+ }
761
+ /** Match live conversation attachments to Slack file ids. */
762
+ async function matchSlackAttachments(args) {
763
+ if (args.providerIds.length === 0) return [];
764
+ return await args.db.db().select({
765
+ id: juniorAttachments.id,
766
+ providerId: juniorAttachments.providerId,
767
+ bytes: juniorAttachments.bytes,
768
+ contentType: juniorAttachments.contentType,
769
+ filename: juniorAttachments.filename
770
+ }).from(juniorAttachments).where(and(eq(juniorAttachments.conversationId, args.conversationId), eq(juniorAttachments.provider, "slack"), inArray(juniorAttachments.providerId, args.providerIds), isNull(juniorAttachments.deleteRequestedAt)));
771
+ }
772
+ /** Read the Slack file identity retained by the Chat SDK. */
773
+ function getSlackFileId(attachment) {
774
+ const metadataId = attachment.fetchMetadata?.fileId;
775
+ if (metadataId) return metadataId;
776
+ return (attachment.url ?? attachment.fetchMetadata?.url ?? "").match(/(?:^|[-/])(F[A-Z0-9]+)(?:[-/]|$)/i)?.[1];
777
+ }
778
+ //#endregion
779
+ //#region src/api/conversations/message-attachments.ts
780
+ /** Resolve saved Slack file references without exposing private storage details. */
781
+ async function hydrateMessageAttachments(db, conversationId, events) {
782
+ const messages = events.flatMap((event) => event.data.type === "message" ? [event.data] : []);
783
+ const ids = [...new Set(messages.flatMap((message) => slackFileIds(message.meta)))];
784
+ if (!ids.length) return;
785
+ const attachments = await matchSlackAttachments({
786
+ db,
787
+ conversationId,
788
+ providerIds: ids
789
+ });
790
+ const byFileId = new Map(attachments.map(({ providerId, ...attachment }) => [providerId, attachment]));
791
+ for (const message of messages) {
792
+ const stored = readMessageAttachments(message.meta?.attachments);
793
+ const matched = slackFileIds(message.meta).flatMap((id) => {
794
+ const attachment = byFileId.get(id);
795
+ return attachment ? [attachment] : [];
796
+ });
797
+ if (matched.length) message.meta = {
798
+ ...message.meta,
799
+ attachments: [...stored, ...matched]
806
800
  };
807
- case "turn_failed": {
808
- const eventId = data.eventId;
809
- const sentryEventUrl = eventId ? buildSentryEventUrl(eventId) : void 0;
801
+ }
802
+ }
803
+ function slackFileIds(meta) {
804
+ const ids = meta?.slackFileIds ?? meta?.imageFileIds;
805
+ return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
806
+ }
807
+ //#endregion
808
+ //#region src/chat/sentry-links.ts
809
+ function getSentryOrgSlug() {
810
+ return process.env.SENTRY_ORG_SLUG?.trim() || void 0;
811
+ }
812
+ function isSentrySaasDsnHost(host) {
813
+ return host === "sentry.io" || host.endsWith(".sentry.io");
814
+ }
815
+ function buildSentryWebBaseUrl(dsn) {
816
+ if (isSentrySaasDsnHost(dsn.host)) return "https://sentry.io";
817
+ const port = dsn.port ? `:${dsn.port}` : "";
818
+ const path = dsn.path ? `/${dsn.path}` : "";
819
+ return `${dsn.protocol}://${dsn.host}${port}${path}`;
820
+ }
821
+ function buildSentryOrgPath(args) {
822
+ const dsn = getClient$1()?.getDsn();
823
+ if (!dsn?.host || !dsn.projectId) return;
824
+ const orgSlug = getSentryOrgSlug();
825
+ if (!orgSlug) return;
826
+ const params = new URLSearchParams();
827
+ params.set("project", dsn.projectId);
828
+ if (args.query) for (const [key, value] of Object.entries(args.query)) params.set(key, value);
829
+ const path = `${args.path}?${params.toString()}`;
830
+ if (isSentrySaasDsnHost(dsn.host)) return `https://${orgSlug}.sentry.io/${path}`;
831
+ return `${buildSentryWebBaseUrl(dsn)}/organizations/${orgSlug}/${path}`;
832
+ }
833
+ /** Build a Sentry conversation URL only when the runtime has enough Sentry config. */
834
+ function buildSentryConversationUrl(conversationId) {
835
+ return buildSentryOrgPath({ path: `explore/conversations/${encodeURIComponent(conversationId)}/` });
836
+ }
837
+ /** Build a Sentry event URL only when the runtime has enough Sentry config. */
838
+ function buildSentryEventUrl(eventId) {
839
+ return buildSentryOrgPath({
840
+ path: "issues/",
841
+ query: { query: eventId }
842
+ });
843
+ }
844
+ //#endregion
845
+ //#region src/api/conversations/event-model.ts
846
+ /** Apply recorded model choices without carrying a previous turn's settings forward. */
847
+ function conversationEventModel(data, current) {
848
+ if (data.type === "turn_started") return void 0;
849
+ if (data.type === "turn_routed" || data.type === "handoff") return {
850
+ modelId: data.modelId,
851
+ modelProfile: data.modelProfile,
852
+ ...data.reasoningLevel ? { reasoningLevel: data.reasoningLevel } : void 0
853
+ };
854
+ if (data.type === "assistant_message") {
855
+ const model = text$1(data.model);
856
+ const provider = text$1(data.provider);
857
+ if (model) {
858
+ const modelId = model.includes("/") || !provider ? model : `${provider}/${model}`;
810
859
  return {
811
- type: "turn_lifecycle",
812
- turnId: data.turnId,
813
- state: "failed",
814
- failureCode: data.failureCode,
815
- ...data.failureReason ? { failureReason: data.failureReason } : void 0,
816
- ...eventId ? { eventId } : void 0,
817
- ...sentryEventUrl ? { sentryEventUrl } : void 0
860
+ ...current,
861
+ modelId
818
862
  };
819
863
  }
820
- case "compaction": return {
821
- type: "compaction",
822
- modelProfile: data.modelProfile,
823
- modelId: data.modelId,
824
- ...args.canExposePayload && data.summary ? { summary: data.summary } : void 0,
825
- ...data.details ? { details: data.details } : void 0
826
- };
827
- default: return;
828
864
  }
865
+ return current;
829
866
  }
830
- /**
831
- * Project canonical events into the ordered reporting boundary.
832
- *
833
- * `canExposePayload` must come from the authorized conversation-detail policy;
834
- * this projection never derives visibility or authorization from event data.
835
- */
836
- function projectConversationReportEventPage(args) {
837
- const subagentStarts = /* @__PURE__ */ new Map();
838
- for (const event of args.subagentStartEvents ?? []) if (event.data.type === "subagent_started") subagentStarts.set(event.data.subagentInvocationId, {
839
- createdAtMs: event.createdAtMs,
840
- data: event.data,
841
- seq: event.seq
842
- });
843
- const toolStarts = /* @__PURE__ */ new Map();
844
- for (const event of args.toolStartEvents ?? []) if (event.data.type === "tool_execution_started") {
845
- const current = toolStarts.get(event.data.toolCallId);
846
- if (!current || event.seq < current.seq) toolStarts.set(event.data.toolCallId, {
847
- createdAtMs: event.createdAtMs,
848
- name: event.data.toolName,
849
- seq: event.seq
850
- });
851
- }
852
- const projected = [];
853
- let model;
854
- for (const event of args.modelContextEvents ?? []) {
855
- model = conversationEventModel(event.data, model);
856
- if (event.data.type === "turn_completed" || event.data.type === "turn_failed") model = void 0;
857
- }
858
- for (const event of args.events) {
859
- model = conversationEventModel(event.data, model);
860
- const eventModel = model;
861
- const modelCall = conversationEventModelCall(event.data);
862
- if (event.data.type === "turn_completed" || event.data.type === "turn_failed") model = void 0;
863
- let data;
864
- if (event.data.type === "assistant_message" || event.data.type === "tool_result") {
865
- const reportArgs = {
866
- canExposePayload: args.canExposePayload,
867
- createdAtMs: event.createdAtMs,
868
- message: event.data,
869
- seq: event.seq
870
- };
871
- const result = reportingToolResultMessageSchema.safeParse(event.data);
872
- const start = result.success ? toolStarts.get(result.data.toolCallId) : void 0;
873
- data = reportAssistantMessage(reportArgs) ?? reportToolResult({
874
- canExposePayload: args.canExposePayload,
875
- message: event.data,
876
- ...start && start.seq < event.seq ? { start } : void 0
877
- });
878
- } else if (event.data.type === "tool_execution_started") {
879
- toolStarts.set(event.data.toolCallId, {
880
- createdAtMs: event.createdAtMs,
881
- name: event.data.toolName,
882
- seq: event.seq
883
- });
884
- data = {
885
- type: "tool_calls",
886
- calls: [{
887
- toolCallId: event.data.toolCallId,
888
- name: event.data.toolName,
889
- status: "running",
890
- startedAt: new Date(event.createdAtMs).toISOString(),
891
- startedSeq: event.seq
892
- }]
893
- };
894
- } else if (event.data.type === "subagent_started") {
895
- subagentStarts.set(event.data.subagentInvocationId, {
896
- createdAtMs: event.createdAtMs,
897
- data: event.data,
898
- seq: event.seq
899
- });
900
- data = {
901
- type: "subagent",
902
- startedSeq: event.seq,
903
- startedAt: new Date(event.createdAtMs).toISOString(),
904
- childConversationId: event.data.childConversationId,
905
- subagentKind: event.data.subagentKind,
906
- status: "running",
907
- ...event.data.parentToolCallId ? { parentToolCallId: event.data.parentToolCallId } : void 0
908
- };
909
- } else if (event.data.type === "subagent_ended") {
910
- const started = subagentStarts.get(event.data.subagentInvocationId);
911
- if (started && started.seq < event.seq) {
912
- data = {
913
- type: "subagent",
914
- startedSeq: started.seq,
915
- startedAt: new Date(started.createdAtMs).toISOString(),
916
- childConversationId: started.data.childConversationId,
917
- subagentKind: started.data.subagentKind,
918
- ...started.data.parentToolCallId ? { parentToolCallId: started.data.parentToolCallId } : void 0,
919
- status: event.data.outcome === "success" ? "completed" : event.data.outcome
920
- };
921
- subagentStarts.delete(event.data.subagentInvocationId);
922
- }
923
- } else if (event.data.type === "handoff") data = {
924
- type: "handoff",
925
- modelProfile: event.data.modelProfile,
926
- modelId: event.data.modelId,
927
- ...event.data.reasoningLevel ? { reasoningLevel: event.data.reasoningLevel } : void 0,
928
- ...event.data.triggeringToolCallId ? { triggeringToolCallId: event.data.triggeringToolCallId } : void 0,
929
- ...args.canExposePayload && event.data.summary ? { summary: event.data.summary } : void 0
930
- };
931
- else data = reportEventData({
932
- canExposePayload: args.canExposePayload,
933
- data: event.data
934
- });
935
- if (!data) continue;
936
- projected.push(conversationReportEventSchema.parse({
937
- seq: event.seq,
938
- createdAt: new Date(event.createdAtMs).toISOString(),
939
- data,
940
- ...eventModel ? { model: eventModel } : void 0,
941
- ...modelCall ? { modelCall } : void 0
942
- }));
943
- }
944
- return projected;
945
- }
946
- //#endregion
947
- //#region src/api/conversations/event-page.ts
948
- const conversationEventColumns = getTableColumns(juniorConversationEvents);
949
- const MIN_SCAN_SIZE = 64;
950
- /** Read bounded canonical rows while excluding model-only replacement history. */
951
- async function readConversationEventRows(executor, args) {
952
- return executor.db().select({
953
- ...conversationEventColumns,
954
- payload: sql`case
955
- when ${juniorConversationEvents.type} in ('compaction', 'handoff')
956
- then jsonb_set(
957
- ${juniorConversationEvents.payload},
958
- '{replacementHistory}',
959
- '[]'::jsonb
960
- )
961
- else ${juniorConversationEvents.payload}
962
- end`
963
- }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, args.conversationId), args.beforeSeq === void 0 ? void 0 : lt(juniorConversationEvents.seq, args.beforeSeq), inArray(juniorConversationEvents.type, args.types ?? [...conversationReportSourceEventTypes]), args.subagentInvocationIds === void 0 ? void 0 : inArray(sql`${juniorConversationEvents.payload}->>'subagentInvocationId'`, args.subagentInvocationIds), args.toolCallIds === void 0 ? void 0 : inArray(sql`${juniorConversationEvents.payload}->>'toolCallId'`, args.toolCallIds))).orderBy(args.direction === "forward" ? asc(juniorConversationEvents.seq) : desc(juniorConversationEvents.seq)).limit(args.limit);
964
- }
965
- function decodeConversationEventRow(row) {
966
- return decodeStoredConversationEvent({
967
- schemaVersion: row.schemaVersion,
968
- seq: row.seq,
969
- historyVersion: row.historyVersion,
970
- ...row.idempotencyKey ? { idempotencyKey: row.idempotencyKey } : void 0,
971
- createdAtMs: row.createdAt.getTime(),
972
- type: row.type,
973
- payload: row.payload
974
- });
975
- }
976
- /** Project stored rows and resolve entity starts that precede the page. */
977
- async function projectConversationEventRows(executor, args) {
978
- const events = args.rows.map(decodeConversationEventRow).sort((left, right) => left.seq - right.seq);
979
- const modelContextRows = events[0] ? (await Promise.all([readConversationEventRows(executor, {
980
- conversationId: args.conversationId,
981
- beforeSeq: events[0].seq,
982
- direction: "backward",
983
- limit: 1,
984
- types: [
985
- "turn_started",
986
- "turn_routed",
987
- "handoff",
988
- "turn_completed",
989
- "turn_failed"
990
- ]
991
- }), readConversationEventRows(executor, {
992
- conversationId: args.conversationId,
993
- beforeSeq: events[0].seq,
994
- direction: "backward",
995
- limit: 1,
996
- types: ["assistant_message"]
997
- })])).flat().sort((left, right) => left.seq - right.seq) : [];
998
- const endedInvocationIds = [...new Set(events.flatMap((event) => event.data.type === "subagent_ended" ? [event.data.subagentInvocationId] : []))];
999
- const subagentStartRows = endedInvocationIds.length === 0 ? [] : await readConversationEventRows(executor, {
1000
- conversationId: args.conversationId,
1001
- direction: "forward",
1002
- limit: endedInvocationIds.length,
1003
- subagentInvocationIds: endedInvocationIds,
1004
- types: ["subagent_started"]
1005
- });
1006
- const toolResultIds = conversationReportToolResultIds(events);
1007
- const toolStartRows = toolResultIds.length === 0 ? [] : await readConversationEventRows(executor, {
1008
- conversationId: args.conversationId,
1009
- direction: "forward",
1010
- limit: toolResultIds.length,
1011
- toolCallIds: toolResultIds,
1012
- types: ["tool_execution_started"]
1013
- });
1014
- if (args.canExposePayload) await hydrateMessageAttachments(executor, args.conversationId, events);
1015
- return projectConversationReportEventPage({
1016
- canExposePayload: args.canExposePayload,
1017
- events,
1018
- subagentStartEvents: subagentStartRows.map(decodeConversationEventRow),
1019
- toolStartEvents: toolStartRows.map(decodeConversationEventRow),
1020
- modelContextEvents: modelContextRows.map(decodeConversationEventRow)
1021
- });
867
+ function text$1(value) {
868
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1022
869
  }
1023
- /**
1024
- * Read the latest reporting events before an exclusive boundary.
1025
- *
1026
- * Source rows are scanned in bounded chunks until the requested projected page
1027
- * and one older event are known. This keeps ordinary pages bounded without
1028
- * assuming every agent history event produces a reporting event.
1029
- */
1030
- async function readConversationEventPage(executor, args) {
1031
- const scanSize = Math.max(args.limit + 1, MIN_SCAN_SIZE);
1032
- const rows = [];
1033
- let beforeSeq = args.beforeSeq;
1034
- let projected = [];
1035
- while (projected.length <= args.limit) {
1036
- const batch = await readConversationEventRows(executor, {
1037
- beforeSeq,
1038
- conversationId: args.conversationId,
1039
- direction: "backward",
1040
- limit: scanSize
1041
- });
1042
- if (batch.length === 0) break;
1043
- rows.push(...batch);
1044
- projected = await projectConversationEventRows(executor, {
1045
- canExposePayload: args.canExposePayload,
1046
- conversationId: args.conversationId,
1047
- rows
1048
- });
1049
- if (projected.length > args.limit || batch.length < scanSize) break;
1050
- beforeSeq = batch.at(-1).seq;
1051
- }
1052
- const events = projected.slice(-args.limit);
870
+ /** Allowlist model-call diagnostics; never expose opaque provider metadata. */
871
+ function conversationEventModelCall(data) {
872
+ if (data.type !== "assistant_message") return void 0;
873
+ const usage = extractGenAiUsageSummary(data);
874
+ const provider = text$1(data.provider);
875
+ const api = text$1(data.api);
876
+ const stopReason = text$1(data.stopReason);
1053
877
  return {
1054
- events,
1055
- ...projected.length > events.length && events[0] ? { previousSeq: events[0].seq } : void 0
878
+ ...provider ? { provider } : void 0,
879
+ ...api ? { api } : void 0,
880
+ ...stopReason ? { stopReason } : void 0,
881
+ ...hasAgentTurnUsage(usage) ? { usage } : void 0
1056
882
  };
1057
883
  }
1058
884
  //#endregion
1059
- //#region src/chat/briefs/input.ts
1060
- const EVENT_PAGE_SIZE = 500;
1061
- async function readReportEventsThrough(executor, conversationId, throughSeq) {
1062
- const pages = [];
1063
- let beforeSeq = throughSeq + 1;
1064
- do {
1065
- const page = await readConversationEventPage(executor, {
1066
- beforeSeq,
1067
- canExposePayload: true,
1068
- conversationId,
1069
- limit: EVENT_PAGE_SIZE
1070
- });
1071
- pages.unshift(page.events);
1072
- beforeSeq = page.previousSeq;
1073
- } while (beforeSeq !== void 0);
1074
- const bySequence = /* @__PURE__ */ new Map();
1075
- for (const event of pages.flat()) bySequence.set(event.seq, event);
1076
- return [...bySequence.values()].sort((left, right) => left.seq - right.seq);
1077
- }
1078
- /**
1079
- * Build generator input from SQL through one durable event. The caller owns
1080
- * the boundary, so a Turn that completes during the read cannot be covered by
1081
- * a Brief that never saw it.
1082
- */
1083
- async function briefInputFromSql(executor, conversationId, throughSeq) {
1084
- const db = executor.db();
1085
- const conversation = await createSqlStore(executor).get({ conversationId });
1086
- if (!conversation) throw new Error(`Conversation ${conversationId} is unavailable`);
1087
- const [events, annotations, codeChanges] = await Promise.all([
1088
- readReportEventsThrough(executor, conversationId, throughSeq),
1089
- listConversationAnnotations(db, conversationId),
1090
- db.select({
1091
- closedAt: juniorCodeChanges.closedAt,
1092
- mergedAt: juniorCodeChanges.mergedAt,
1093
- number: juniorCodeChanges.number,
1094
- openedAt: juniorCodeChanges.openedAt,
1095
- repository: juniorCodeRepositories.name,
1096
- state: juniorCodeChanges.state,
1097
- title: juniorCodeChanges.title,
1098
- url: juniorCodeChanges.url
1099
- }).from(juniorCodeChanges).innerJoin(juniorCodeRepositories, eq(juniorCodeRepositories.id, juniorCodeChanges.repositoryId)).where(and(arrayContains(juniorCodeChanges.conversationIds, [conversationId]), isNotNull(juniorCodeChanges.url)))
1100
- ]);
1101
- const location = conversation.location;
1102
- return parseBriefInput({
1103
- conversationId,
1104
- ...conversation.title ? { title: conversation.title } : void 0,
1105
- visibility: conversation.visibility ?? "private",
1106
- ...location ? { location: {
1107
- provider: location.provider,
1108
- ...conversation.channelName ? { channelName: conversation.channelName } : void 0
1109
- } } : void 0,
1110
- entries: briefEntriesFromReportEvents(events),
1111
- codeChanges: codeChanges.map((change) => ({
1112
- repository: change.repository,
1113
- number: change.number,
1114
- ...change.title ? { title: change.title } : void 0,
1115
- url: change.url,
1116
- state: change.state,
1117
- openedAt: change.openedAt.toISOString(),
1118
- ...change.mergedAt ? { mergedAt: change.mergedAt.toISOString() } : void 0,
1119
- ...change.closedAt ? { closedAt: change.closedAt.toISOString() } : void 0
1120
- })),
1121
- resources: annotations.map((annotation) => ({
1122
- label: annotation.label,
1123
- url: annotation.url,
1124
- ...annotation.status ? { status: annotation.status } : void 0
1125
- }))
1126
- });
1127
- }
1128
- //#endregion
1129
- //#region src/chat/briefs/operational-report.ts
1130
- const DAY_MS$1 = 864e5;
1131
- const WINDOWS = [
1132
- 7,
1133
- 30,
1134
- 90
885
+ //#region src/api/conversations/events.ts
886
+ /** Canonical event types that can contribute to the reporting projection. */
887
+ const conversationReportSourceEventTypes = [
888
+ "message",
889
+ "message_handled",
890
+ "assistant_message",
891
+ "tool_result",
892
+ "tool_execution_started",
893
+ "guardian_action_reviewed",
894
+ "turn_started",
895
+ "turn_context",
896
+ "structured_event",
897
+ "attachments_delivered",
898
+ "turn_routed",
899
+ "turn_completed",
900
+ "turn_failed",
901
+ "compaction",
902
+ "handoff",
903
+ "subagent_started",
904
+ "subagent_ended"
1135
905
  ];
1136
- function startOfUtcDay$1(value) {
1137
- const date = new Date(value);
1138
- date.setUTCHours(0, 0, 0, 0);
1139
- return date;
1140
- }
1141
- function formatCount(value) {
1142
- return new Intl.NumberFormat("en-US").format(value);
1143
- }
1144
- function formatUsd(value) {
1145
- const maximumFractionDigits = value > 0 && value < .01 ? 4 : 2;
1146
- return new Intl.NumberFormat("en-US", {
1147
- currency: "USD",
1148
- maximumFractionDigits,
1149
- minimumFractionDigits: 2,
1150
- style: "currency"
1151
- }).format(value);
1152
- }
1153
- /**
1154
- * Build Brief storage and model-cost diagnostics for the System page. Cost
1155
- * comes from retained Conversation events, so spend on purged private
1156
- * Conversations leaves the report with them; the labels say so.
1157
- */
1158
- async function buildBriefsOperationalReport(args) {
1159
- const newestDay = startOfUtcDay$1(args.nowMs);
1160
- const windowStart = /* @__PURE__ */ new Date(newestDay.getTime() - 29 * DAY_MS$1);
1161
- const windowEnd = new Date(newestDay.getTime() + DAY_MS$1);
1162
- const [[counts], recentCounts] = await Promise.all([args.db.select({
1163
- conversations: sql`count(distinct ${juniorConversationBriefs.conversationId})`.mapWith(Number),
1164
- stored: sql`count(*)`.mapWith(Number)
1165
- }).from(juniorConversationBriefs), args.db.select({ stored: sql`count(*)`.mapWith(Number) }).from(juniorConversationBriefs).where(and(gte(juniorConversationBriefs.createdAt, windowStart), lt(juniorConversationBriefs.createdAt, windowEnd)))]);
1166
- const recentDays = args.briefDays.filter((day) => {
1167
- const dayStart = Date.parse(`${day.date}T00:00:00.000Z`);
1168
- return dayStart >= windowStart.getTime() && dayStart < windowEnd.getTime();
1169
- });
1170
- const recentCost = recentDays.reduce((total, day) => total + day.costUsd, 0);
1171
- const recentBriefs = recentDays.reduce((total, day) => total + day.events, 0);
906
+ const reportingAssistantMessageSchema = z.object({
907
+ type: z.literal("assistant_message"),
908
+ content: z.array(z.unknown())
909
+ }).passthrough();
910
+ const reportingToolCallPartSchema = z.object({
911
+ type: z.literal("toolCall"),
912
+ id: z.string().min(1),
913
+ name: z.string().min(1),
914
+ arguments: z.unknown()
915
+ }).passthrough();
916
+ const reportingToolResultMessageSchema = z.object({
917
+ type: z.literal("tool_result"),
918
+ toolCallId: z.string().min(1),
919
+ toolName: z.string().min(1).optional(),
920
+ name: z.string().min(1).optional(),
921
+ content: z.array(z.unknown()),
922
+ details: z.unknown().optional(),
923
+ isError: z.boolean().optional()
924
+ }).passthrough();
925
+ const reportingToolResultDetailsSchema = z.object({
926
+ ok: z.boolean().optional(),
927
+ status: z.string().optional()
928
+ }).passthrough();
929
+ const reportingTextPartSchema = z.object({
930
+ type: z.literal("text"),
931
+ text: z.string()
932
+ }).passthrough();
933
+ const reportingReasoningPartSchema = z.object({
934
+ type: z.literal("thinking"),
935
+ thinking: z.string(),
936
+ redacted: z.literal(true).optional()
937
+ }).passthrough();
938
+ const reportingMediaPartSchema = z.object({
939
+ type: z.enum(["image", "audio"]),
940
+ mimeType: z.string().min(1).optional()
941
+ }).passthrough();
942
+ const reportingMessageAuthorSchema = z.object({
943
+ fullName: z.string().min(1).optional(),
944
+ userId: z.string().min(1).optional(),
945
+ userName: z.string().min(1).optional()
946
+ }).passthrough();
947
+ /** Retain model-visible text and media descriptors, dropping all opaque fields. */
948
+ function sanitizeModelContentPart(value) {
949
+ const text = reportingTextPartSchema.safeParse(value);
950
+ if (text.success) return {
951
+ type: "text",
952
+ text: text.data.text
953
+ };
954
+ const media = reportingMediaPartSchema.safeParse(value);
955
+ if (!media.success) return void 0;
1172
956
  return {
1173
- generatedAt: new Date(args.nowMs).toISOString(),
1174
- title: "Briefs",
1175
- metrics: [
1176
- {
1177
- label: "briefs stored",
1178
- tone: counts?.stored ? "good" : "neutral",
1179
- value: formatCount(counts?.stored ?? 0)
1180
- },
1181
- {
1182
- label: "conversations with a brief",
1183
- value: formatCount(counts?.conversations ?? 0)
1184
- },
1185
- {
1186
- label: "briefs · 30d",
1187
- value: formatCount(recentCounts[0]?.stored ?? 0)
1188
- },
1189
- {
1190
- label: "cost · 30d · retained",
1191
- value: formatUsd(recentCost)
1192
- },
1193
- {
1194
- label: "average cost per brief · 30d · retained",
1195
- value: formatUsd(recentBriefs === 0 ? 0 : recentCost / recentBriefs)
1196
- }
1197
- ],
1198
- widgets: [{
1199
- categories: args.briefDays.map((day) => ({
1200
- id: day.date,
1201
- label: day.date,
1202
- values: {
1203
- briefs: day.events,
1204
- costUsd: day.costUsd
1205
- }
1206
- })),
1207
- description: "Stored Brief versions and estimated model cost",
1208
- id: "briefs-created",
1209
- series: [{
1210
- key: "briefs",
1211
- label: "Briefs"
1212
- }, {
1213
- format: "usd",
1214
- key: "costUsd",
1215
- label: "Cost"
1216
- }],
1217
- timeRangeDays: [...WINDOWS],
1218
- title: "Briefs created",
1219
- type: "bar_chart"
1220
- }]
957
+ type: media.data.type,
958
+ ...media.data.mimeType ? { mimeType: media.data.mimeType } : void 0
1221
959
  };
1222
960
  }
1223
- //#endregion
1224
- //#region src/chat/briefs/store.ts
1225
- function briefVersionFromRow(row) {
1226
- return {
1227
- conversationId: row.conversationId,
1228
- version: row.version,
1229
- turnId: row.turnId,
1230
- throughSeq: row.throughSeq,
1231
- content: conversationBriefSchema.parse(row.content),
1232
- searchText: row.searchText,
1233
- modelId: row.modelId,
1234
- ...row.costUsd !== null ? { costUsd: row.costUsd } : void 0,
1235
- createdAt: row.createdAt
961
+ /** Convert allowlisted Pi content parts into the reporting result value. */
962
+ function modelVisibleToolOutput(content) {
963
+ const sanitized = content.map(sanitizeModelContentPart).filter((part) => part !== void 0);
964
+ if (sanitized.length === 0) return void 0;
965
+ if (sanitized.length !== 1) return sanitized;
966
+ const only = sanitized[0];
967
+ return only.type === "text" ? only.text : only;
968
+ }
969
+ function reportMessageActorIdentity(data) {
970
+ if (data.role !== "user") return void 0;
971
+ const author = reportingMessageAuthorSchema.safeParse(data.meta?.author);
972
+ if (!author.success) return void 0;
973
+ const actorIdentity = {
974
+ ...author.data.fullName ? { fullName: author.data.fullName } : void 0,
975
+ ...author.data.userId ? { slackUserId: author.data.userId } : void 0,
976
+ ...author.data.userName ? { slackUserName: author.data.userName } : void 0
1236
977
  };
978
+ return Object.keys(actorIdentity).length > 0 ? actorIdentity : void 0;
1237
979
  }
1238
- /** Append the next Brief version, or return the row for an existing Turn. */
1239
- async function appendConversationBrief(db, input) {
1240
- return await db.transaction(async (tx) => {
1241
- const conversation = (await tx.select({
1242
- conversationId: juniorConversations.conversationId,
1243
- transcriptPurgedAt: juniorConversations.transcriptPurgedAt
1244
- }).from(juniorConversations).where(eq(juniorConversations.conversationId, input.conversationId)).for("update"))[0];
1245
- if (!conversation) throw new Error(`Conversation ${input.conversationId} is unavailable`);
1246
- if (conversation.transcriptPurgedAt) {
1247
- if ((await resolveRootVisibility({ db: () => tx }, input.conversationId)).visibility !== "public") throw new Error(`Cannot append a Brief to purged non-public Conversation ${input.conversationId}`);
1248
- }
1249
- const existing = await tx.select().from(juniorConversationBriefs).where(and(eq(juniorConversationBriefs.conversationId, input.conversationId), eq(juniorConversationBriefs.turnId, input.turnId))).limit(1);
1250
- if (existing[0]) return {
1251
- inserted: false,
1252
- value: briefVersionFromRow(existing[0])
1253
- };
1254
- const version = ((await tx.select({ version: max(juniorConversationBriefs.version) }).from(juniorConversationBriefs).where(eq(juniorConversationBriefs.conversationId, input.conversationId)))[0]?.version ?? 0) + 1;
1255
- const row = (await tx.insert(juniorConversationBriefs).values({
1256
- conversationId: input.conversationId,
1257
- version,
1258
- turnId: input.turnId,
1259
- throughSeq: input.throughSeq,
1260
- content: conversationBriefSchema.parse(input.content),
1261
- searchText: input.searchText,
1262
- modelId: input.modelId,
1263
- costUsd: input.costUsd ?? null
1264
- }).onConflictDoNothing({ target: [juniorConversationBriefs.conversationId, juniorConversationBriefs.turnId] }).returning())[0];
1265
- if (!row) throw new Error("Brief insert conflicted after the Turn check");
1266
- return {
1267
- inserted: true,
1268
- value: briefVersionFromRow(row)
1269
- };
980
+ /** Project native assistant tool calls through the existing reporting shape. */
981
+ function reportToolCalls(args) {
982
+ const message = reportingAssistantMessageSchema.safeParse(args.message);
983
+ if (!message.success) return void 0;
984
+ const calls = message.data.content.flatMap((part) => {
985
+ const call = reportingToolCallPartSchema.safeParse(part);
986
+ if (!call.success) return [];
987
+ return [{
988
+ toolCallId: call.data.id,
989
+ name: call.data.name,
990
+ status: "running",
991
+ startedAt: new Date(args.createdAtMs).toISOString(),
992
+ startedSeq: args.seq,
993
+ ...args.canExposePayload && call.data.arguments !== void 0 ? { input: call.data.arguments } : void 0
994
+ }];
1270
995
  });
996
+ return calls.length > 0 ? {
997
+ type: "tool_calls",
998
+ calls
999
+ } : void 0;
1271
1000
  }
1272
- /** Read the Brief version produced for one completed Turn. */
1273
- async function readConversationBriefForTurn(db, conversationId, turnId) {
1274
- const rows = await db.select().from(juniorConversationBriefs).where(and(eq(juniorConversationBriefs.conversationId, conversationId), eq(juniorConversationBriefs.turnId, turnId))).limit(1);
1275
- return rows[0] ? briefVersionFromRow(rows[0]) : void 0;
1276
- }
1277
- /** Read the latest Brief version for one Conversation. */
1278
- async function readLatestConversationBrief(db, conversationId) {
1279
- const rows = await db.select().from(juniorConversationBriefs).where(eq(juniorConversationBriefs.conversationId, conversationId)).orderBy(desc(juniorConversationBriefs.version)).limit(1);
1280
- return rows[0] ? briefVersionFromRow(rows[0]) : void 0;
1281
- }
1282
- //#endregion
1283
- //#region src/chat/briefs/task.ts
1284
- const BRIEF_LOCK_TTL_MS = 6e5;
1285
- function hasUserInstruction(run) {
1286
- return run.transcript.some((entry) => entry.type === "message" && entry.role === "user" && entry.provenance?.authority === "instruction");
1287
- }
1288
- function logSkip(context, conversationId, reason) {
1289
- context.log.info("Brief update skipped", {
1290
- conversationId,
1291
- reason
1292
- });
1001
+ /** Project assistant reasoning with tool ids retained only for display order. */
1002
+ function reportAssistantMessage(args) {
1003
+ const message = reportingAssistantMessageSchema.safeParse(args.message);
1004
+ if (!message.success) return void 0;
1005
+ if (!message.data.content.some((part) => {
1006
+ const reasoning = reportingReasoningPartSchema.safeParse(part);
1007
+ return reasoning.success && reasoning.data.redacted !== true && reasoning.data.thinking.trim().length > 0;
1008
+ })) return reportToolCalls(args) ?? {
1009
+ type: "assistant_message",
1010
+ parts: []
1011
+ };
1012
+ const reasoningParts = [];
1013
+ const assistantParts = [];
1014
+ for (const part of message.data.content) {
1015
+ const reasoning = reportingReasoningPartSchema.safeParse(part);
1016
+ if (reasoning.success) {
1017
+ if (reasoning.data.redacted !== true && reasoning.data.thinking.trim().length > 0) {
1018
+ const projected = {
1019
+ type: "reasoning",
1020
+ ...args.canExposePayload ? { text: reasoning.data.thinking } : { redacted: true }
1021
+ };
1022
+ reasoningParts.push(projected);
1023
+ assistantParts.push(projected);
1024
+ }
1025
+ continue;
1026
+ }
1027
+ const call = reportingToolCallPartSchema.safeParse(part);
1028
+ if (call.success) assistantParts.push({
1029
+ type: "tool_call",
1030
+ toolCallId: call.data.id
1031
+ });
1032
+ }
1033
+ const toolCalls = reportToolCalls(args);
1034
+ if (toolCalls?.type === "tool_calls") return {
1035
+ ...toolCalls,
1036
+ assistant: { parts: assistantParts }
1037
+ };
1038
+ return reasoningParts.length > 0 ? {
1039
+ type: "assistant_message",
1040
+ parts: reasoningParts
1041
+ } : void 0;
1293
1042
  }
1294
- async function readTurnCompletedSeq(db, conversationId, turnId) {
1295
- const seq = (await db.select({ seq: juniorConversationEvents.seq }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.type, "turn_completed"), sql`${juniorConversationEvents.payload}->>'turnId' = ${turnId}`)).orderBy(desc(juniorConversationEvents.seq)).limit(1))[0]?.seq;
1296
- if (seq === void 0) throw new Error(`Completed Turn ${turnId} has no terminal event in Conversation ${conversationId}`);
1297
- return seq;
1043
+ /** Project a native tool result without exposing host-only result details. */
1044
+ function reportToolResult(args) {
1045
+ const message = reportingToolResultMessageSchema.safeParse(args.message);
1046
+ if (!message.success) return void 0;
1047
+ const details = reportingToolResultDetailsSchema.safeParse(message.data.details);
1048
+ const outcome = message.data.isError === true || details.success && (details.data.ok === false || details.data.status === "error") ? "error" : "completed";
1049
+ const output = modelVisibleToolOutput(message.data.content);
1050
+ const name = message.data.toolName ?? message.data.name ?? args.start?.name;
1051
+ if (!name) return void 0;
1052
+ return {
1053
+ type: "tool_calls",
1054
+ calls: [{
1055
+ toolCallId: message.data.toolCallId,
1056
+ name,
1057
+ status: outcome,
1058
+ ...args.start ? {
1059
+ startedAt: new Date(args.start.createdAtMs).toISOString(),
1060
+ startedSeq: args.start.seq
1061
+ } : void 0,
1062
+ ...args.canExposePayload && output !== void 0 ? { output } : void 0
1063
+ }]
1064
+ };
1298
1065
  }
1299
- async function emitBriefUpdated(context, stored) {
1300
- await context.events.emit(briefUpdatedEvent({
1301
- version: stored.version,
1302
- modelId: stored.modelId,
1303
- ...stored.costUsd !== void 0 ? { costUsd: stored.costUsd } : void 0,
1304
- decisions: stored.content.decisions.length,
1305
- openDecisions: stored.content.openDecisions.length,
1306
- links: stored.content.links.length
1307
- }));
1066
+ /** Return tool result ids that may need start metadata from outside a page. */
1067
+ function conversationReportToolResultIds(events) {
1068
+ return [...new Set(events.flatMap((event) => {
1069
+ const result = reportingToolResultMessageSchema.safeParse(event.data);
1070
+ return result.success ? [result.data.toolCallId] : [];
1071
+ }))];
1308
1072
  }
1309
- /** Generate and store the next Brief after one completed Turn. */
1310
- async function updateConversationBrief(context) {
1311
- const run = await context.run.load();
1312
- if (run.source.kind !== "slack" && run.source.kind !== "web" && run.source.kind !== "local") {
1313
- logSkip(context, run.conversationId, "unsupported_source");
1314
- return;
1315
- }
1316
- const executor = getSqlExecutor();
1317
- const conversation = await createSqlStore(executor).get({ conversationId: run.conversationId });
1318
- if (conversation?.parentConversationId) {
1319
- logSkip(context, run.conversationId, "child_conversation");
1320
- return;
1073
+ function reportEventData(args) {
1074
+ const { data } = args;
1075
+ if (data.type === "structured_event") {
1076
+ if (!args.canExposePayload) return void 0;
1077
+ const presentation = data.namespace === "junior" ? renderJuniorNativeConversationEvent(data) : renderPluginConversationEvent(data);
1078
+ if (!presentation) return void 0;
1079
+ return {
1080
+ type: "structured_event",
1081
+ namespace: data.namespace,
1082
+ name: data.name,
1083
+ version: data.version,
1084
+ ...data.turnId ? { turnId: data.turnId } : void 0,
1085
+ presentation
1086
+ };
1321
1087
  }
1322
- if (conversation?.transcriptPurgedAtMs !== void 0) {
1323
- if ((await resolveRootVisibility(executor, run.conversationId)).visibility !== "public") {
1324
- logSkip(context, run.conversationId, "purged_private");
1325
- return;
1088
+ switch (data.type) {
1089
+ case "message": {
1090
+ const actorIdentity = args.canExposePayload ? reportMessageActorIdentity(data) : void 0;
1091
+ return {
1092
+ type: "message",
1093
+ messageId: data.messageId,
1094
+ role: data.role,
1095
+ ...data.meta?.source === "web" || data.meta?.source === "slack" ? { source: data.meta.source } : void 0,
1096
+ ...actorIdentity ? { actorIdentity } : void 0,
1097
+ eventObjectType: objectTypeSchema.safeParse(data.meta?.eventObjectType).data,
1098
+ ...typeof data.meta?.eventType === "string" ? { eventType: data.meta.eventType } : void 0,
1099
+ ...typeof data.meta?.explicitMention === "boolean" ? { explicitMention: data.meta.explicitMention } : void 0,
1100
+ ...args.canExposePayload && typeof data.meta?.trustedSummary === "string" ? { trustedSummary: data.meta.trustedSummary } : void 0,
1101
+ ...args.canExposePayload && (data.meta?.cards || data.meta?.objectCards) ? { cards: readMessageCardRefs(data.meta) } : void 0,
1102
+ ...args.canExposePayload ? {
1103
+ text: data.text,
1104
+ ...data.meta?.attachments ? { attachments: readMessageAttachments(data.meta.attachments) } : void 0
1105
+ } : { redacted: true }
1106
+ };
1107
+ }
1108
+ case "message_handled": return {
1109
+ type: "message_handled",
1110
+ messageId: data.messageId
1111
+ };
1112
+ case "turn_started": return {
1113
+ type: "turn_lifecycle",
1114
+ turnId: data.turnId,
1115
+ state: "started",
1116
+ ...data.inputMessageIds.length > 0 ? { inputMessageIds: data.inputMessageIds } : void 0
1117
+ };
1118
+ case "turn_context":
1119
+ if (!args.canExposePayload) return;
1120
+ return {
1121
+ type: "turn_context",
1122
+ turnId: data.turnId,
1123
+ pluginName: data.pluginName,
1124
+ kind: data.kind,
1125
+ version: data.version,
1126
+ content: data.content
1127
+ };
1128
+ case "attachments_delivered":
1129
+ if (!args.canExposePayload) return;
1130
+ return {
1131
+ type: "attachments_delivered",
1132
+ attachments: data.attachments
1133
+ };
1134
+ case "turn_routed": return {
1135
+ type: "turn_routed",
1136
+ turnId: data.turnId,
1137
+ modelProfile: data.modelProfile,
1138
+ modelId: data.modelId,
1139
+ ...data.costUsd !== void 0 ? { costUsd: data.costUsd } : void 0,
1140
+ reasoningLevel: data.reasoningLevel,
1141
+ ...data.confidence !== void 0 ? { confidence: data.confidence } : void 0,
1142
+ source: data.source
1143
+ };
1144
+ case "guardian_action_reviewed": return {
1145
+ type: "guardian_action_reviewed",
1146
+ turnId: data.turnId,
1147
+ toolCallId: data.toolCallId,
1148
+ toolName: data.toolName,
1149
+ decision: data.decision,
1150
+ riskLevel: data.riskLevel,
1151
+ userAuthorization: data.userAuthorization
1152
+ };
1153
+ case "turn_completed": return {
1154
+ type: "turn_lifecycle",
1155
+ turnId: data.turnId,
1156
+ state: data.outcome === "success" ? "succeeded" : "no_reply"
1157
+ };
1158
+ case "turn_failed": {
1159
+ const eventId = data.eventId;
1160
+ const sentryEventUrl = eventId ? buildSentryEventUrl(eventId) : void 0;
1161
+ return {
1162
+ type: "turn_lifecycle",
1163
+ turnId: data.turnId,
1164
+ state: "failed",
1165
+ failureCode: data.failureCode,
1166
+ ...data.failureReason ? { failureReason: data.failureReason } : void 0,
1167
+ ...eventId ? { eventId } : void 0,
1168
+ ...sentryEventUrl ? { sentryEventUrl } : void 0
1169
+ };
1326
1170
  }
1171
+ case "compaction": return {
1172
+ type: "compaction",
1173
+ modelProfile: data.modelProfile,
1174
+ modelId: data.modelId,
1175
+ ...args.canExposePayload && data.summary ? { summary: data.summary } : void 0,
1176
+ ...data.details ? { details: data.details } : void 0
1177
+ };
1178
+ default: return;
1327
1179
  }
1328
- if (!hasUserInstruction(run)) {
1329
- logSkip(context, run.conversationId, "no_user_instruction");
1330
- return;
1331
- }
1332
- await context.state.withLock(`brief:${run.conversationId}`, BRIEF_LOCK_TTL_MS, async () => {
1333
- const db = context.db;
1334
- const existing = await readConversationBriefForTurn(db, run.conversationId, run.runId);
1335
- if (existing) {
1336
- await emitBriefUpdated(context, existing);
1337
- logSkip(context, run.conversationId, "turn_already_stored");
1338
- return;
1339
- }
1340
- const terminalSeq = await readTurnCompletedSeq(db, run.conversationId, run.runId);
1341
- const previous = await readLatestConversationBrief(db, run.conversationId);
1342
- if (previous && previous.throughSeq >= terminalSeq) {
1343
- logSkip(context, run.conversationId, "turn_already_covered");
1344
- return;
1345
- }
1346
- const input = await briefInputFromSql(executor, run.conversationId, terminalSeq);
1347
- const modelId = defaultModelId(botConfig);
1348
- const generation = await generateBrief({
1349
- completeObject: (request) => completeObject({
1350
- ...request,
1351
- modelId,
1352
- promptName: "junior.brief_update"
1353
- }),
1354
- input,
1355
- previous: previous?.content,
1356
- prompt: BRIEF_PROMPT,
1357
- throughIndex: terminalSeq
1358
- });
1359
- const stored = await appendConversationBrief(db, {
1360
- conversationId: run.conversationId,
1361
- turnId: run.runId,
1362
- throughSeq: terminalSeq,
1363
- content: generation.brief,
1364
- searchText: generation.searchText,
1365
- modelId,
1366
- ...generation.costUsd !== void 0 ? { costUsd: generation.costUsd } : void 0
1367
- });
1368
- await emitBriefUpdated(context, stored.value);
1369
- if (!stored.inserted) logSkip(context, run.conversationId, "turn_already_stored");
1370
- });
1371
- }
1372
- /** Core task registration kept outside the installed plugin catalog. */
1373
- const briefsTaskRegistration = {
1374
- manifest: {
1375
- name: "briefs",
1376
- displayName: "Briefs",
1377
- description: "Durable Conversation Brief generation"
1378
- },
1379
- conversationEvents: [briefUpdatedEvent],
1380
- hooks: { async operationalReport(context) {
1381
- return await buildBriefsOperationalReport({
1382
- briefDays: await context.eventStats.costsByDay({
1383
- days: 90,
1384
- eventName: "brief_updated"
1385
- }),
1386
- db: context.db,
1387
- nowMs: context.nowMs
1388
- });
1389
- } },
1390
- tasks: { updateBrief: { async run(context) {
1391
- await updateConversationBrief(context);
1392
- } } }
1393
- };
1394
- //#endregion
1395
- //#region src/chat/plugins/conversation-events.ts
1396
- function registeredDefinition(plugin, value) {
1397
- return plugin.conversationEvents?.find((definition) => definition === value.definition);
1398
- }
1399
- /** Keep operation idempotency stable when a registered event schema advances. */
1400
- function eventIdentityVersion(plugin, definition) {
1401
- let version = definition.version;
1402
- for (const candidate of plugin.conversationEvents ?? []) if (candidate.eventName === definition.eventName) version = Math.min(version, candidate.version);
1403
- return version;
1404
- }
1405
- /** Create a conversation-bound writer for one plugin operation. */
1406
- function createPluginConversationEvents(args) {
1407
- return { async emit(value) {
1408
- const definition = registeredDefinition(args.plugin, value);
1409
- if (!definition) throw new Error(`Plugin "${args.plugin.manifest.name}" cannot emit an unregistered conversation event.`);
1410
- const content = definition.parse(value.data);
1411
- const identityVersion = eventIdentityVersion(args.plugin, definition);
1412
- await getConversationEventStore().append(args.conversationId, [{
1413
- createdAtMs: Date.now(),
1414
- idempotencyKey: `plugin:${args.plugin.manifest.name}:operation:${args.operationId}:event:${definition.eventName}@${identityVersion}`,
1415
- data: {
1416
- type: "structured_event",
1417
- namespace: args.plugin.manifest.name,
1418
- name: definition.eventName,
1419
- version: definition.version,
1420
- turnId: args.turnId,
1421
- content
1422
- }
1423
- }], { activity: "preserve" });
1424
- } };
1425
- }
1426
- /** Render a stored plugin event through its currently registered definition. */
1427
- function renderPluginConversationEvent(args) {
1428
- const definition = [briefsTaskRegistration, ...getPlugins()].find((candidate) => candidate.manifest.name === args.namespace)?.conversationEvents?.find((candidate) => candidate.eventName === args.name && candidate.version === args.version);
1429
- if (!definition) return void 0;
1430
- return definition.renderEvent(definition.parse(args.content));
1431
- }
1432
- //#endregion
1433
- //#region src/api/conversations/membership.ts
1434
- /** True when the viewer is a materialized participant on this root conversation. */
1435
- function conversationHasParticipantUser(userId) {
1436
- const participant = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorConversationParticipants.userId, userId)));
1437
- return exists(participant);
1438
1180
  }
1439
1181
  /**
1440
- * True when a verified primary-email user participates on this root
1441
- * conversation. Used only for the actorEmail feed fallback.
1182
+ * Project canonical events into the ordered reporting boundary.
1183
+ *
1184
+ * `canExposePayload` must come from the authorized conversation-detail policy;
1185
+ * this projection never derives visibility or authorization from event data.
1442
1186
  */
1443
- function conversationHasParticipantEmail(email) {
1444
- const participant = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorUsers.primaryEmailNormalized, email)));
1445
- return exists(participant);
1446
- }
1447
- /** True when one user's personal feed has archived this root after the cutoff. */
1448
- function conversationArchivedForUser(userId, archivedAfter) {
1449
- const archive = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorConversationParticipants.userId, userId), isNotNull(juniorConversationParticipants.archivedAt), archivedAfter ? gte(juniorConversationParticipants.archivedAt, archivedAfter) : void 0));
1450
- return exists(archive);
1451
- }
1452
- /** True when one user's personal feed has not archived this root. */
1453
- function conversationNotArchivedForUser(userId) {
1454
- const archive = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorConversationParticipants.userId, userId), isNotNull(juniorConversationParticipants.archivedAt)));
1455
- return notExists(archive);
1456
- }
1457
- /** True when one primary-email user's feed archived this root after the cutoff. */
1458
- function conversationArchivedForEmail(email, archivedAfter) {
1459
- const archive = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorUsers.primaryEmailNormalized, email), isNotNull(juniorConversationParticipants.archivedAt), archivedAfter ? gte(juniorConversationParticipants.archivedAt, archivedAfter) : void 0));
1460
- return exists(archive);
1461
- }
1462
- /** True when one primary-email user's personal feed has not archived this root. */
1463
- function conversationNotArchivedForEmail(email) {
1464
- const archive = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorUsers.primaryEmailNormalized, email), isNotNull(juniorConversationParticipants.archivedAt)));
1465
- return notExists(archive);
1466
- }
1467
- /** Root-actor ownership or a materialized participant row for the same person. */
1468
- function viewerConversationMembership(args) {
1469
- if (args.actorMatch && args.participantMatch) return or(args.actorMatch, args.participantMatch);
1470
- return args.actorMatch ?? args.participantMatch;
1471
- }
1472
- /** Read personal archive timestamps for a bounded set of conversation roots. */
1473
- async function conversationArchiveTimes(db, conversationIds, subject) {
1474
- if (!subject || conversationIds.length === 0) return /* @__PURE__ */ new Map();
1475
- const query = db.select({
1476
- archivedAt: juniorConversationParticipants.archivedAt,
1477
- rootConversationId: juniorConversationParticipants.rootConversationId
1478
- }).from(juniorConversationParticipants);
1479
- const rows = "userId" in subject ? await query.where(and(eq(juniorConversationParticipants.userId, subject.userId), inArray(juniorConversationParticipants.rootConversationId, [...conversationIds]), isNotNull(juniorConversationParticipants.archivedAt))) : await query.innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorUsers.primaryEmailNormalized, subject.email), inArray(juniorConversationParticipants.rootConversationId, [...conversationIds]), isNotNull(juniorConversationParticipants.archivedAt)));
1480
- return new Map(rows.flatMap((row) => row.archivedAt ? [[row.rootConversationId, row.archivedAt.getTime()]] : []));
1481
- }
1482
- /** Find which of the supplied roots include the linked viewer as a participant. */
1483
- async function conversationIdsWithParticipantUser(db, conversationIds, userId) {
1484
- if (conversationIds.length === 0) return /* @__PURE__ */ new Set();
1485
- const rows = await db.select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.userId, userId), inArray(juniorConversationParticipants.rootConversationId, [...conversationIds])));
1486
- return new Set(rows.map((row) => row.rootConversationId));
1487
- }
1488
- //#endregion
1489
- //#region src/api/conversations/access.ts
1490
- const rootConversation = alias(juniorConversations, "access_root_conversation");
1491
- const rootDestination = alias(juniorDestinations, "access_root_destination");
1492
- const rootIdentity = alias(juniorIdentities, "access_root_identity");
1493
- /** Resolve viewer access for a bounded set of persisted conversations. */
1494
- async function readConversationAccessFromSql(db, conversationIds, viewer) {
1495
- if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
1496
- const normalizedViewerEmail = viewer?.email.trim().toLowerCase() || void 0;
1497
- const rows = await db.select({
1498
- conversationId: juniorConversations.conversationId,
1499
- parentConversationId: juniorConversations.parentConversationId,
1500
- storedRootConversationId: juniorConversations.rootConversationId,
1501
- rootConversationId: rootConversation.conversationId,
1502
- rootParentConversationId: rootConversation.parentConversationId,
1503
- rootRootConversationId: rootConversation.rootConversationId,
1504
- visibility: rootDestination.visibility,
1505
- rootEmailNormalized: rootIdentity.emailNormalized,
1506
- rootEmailVerified: rootIdentity.emailVerified,
1507
- rootUserId: rootIdentity.userId
1508
- }).from(juniorConversations).leftJoin(rootConversation, eq(rootConversation.conversationId, juniorConversations.rootConversationId)).leftJoin(rootDestination, eq(rootDestination.id, rootConversation.destinationId)).leftJoin(rootIdentity, eq(rootIdentity.id, rootConversation.actorIdentityId)).where(inArray(juniorConversations.conversationId, [...conversationIds]));
1509
- const membershipConversationIds = /* @__PURE__ */ new Set();
1510
- for (const row of rows) {
1511
- membershipConversationIds.add(row.conversationId);
1512
- if (row.storedRootConversationId) membershipConversationIds.add(row.storedRootConversationId);
1187
+ function projectConversationReportEventPage(args) {
1188
+ const subagentStarts = /* @__PURE__ */ new Map();
1189
+ for (const event of args.subagentStartEvents ?? []) if (event.data.type === "subagent_started") subagentStarts.set(event.data.subagentInvocationId, {
1190
+ createdAtMs: event.createdAtMs,
1191
+ data: event.data,
1192
+ seq: event.seq
1193
+ });
1194
+ const toolStarts = /* @__PURE__ */ new Map();
1195
+ for (const event of args.toolStartEvents ?? []) if (event.data.type === "tool_execution_started") {
1196
+ const current = toolStarts.get(event.data.toolCallId);
1197
+ if (!current || event.seq < current.seq) toolStarts.set(event.data.toolCallId, {
1198
+ createdAtMs: event.createdAtMs,
1199
+ name: event.data.toolName,
1200
+ seq: event.seq
1201
+ });
1513
1202
  }
1514
- const participantRootIds = viewer !== void 0 ? await conversationIdsWithParticipantUser(db, [...membershipConversationIds], viewer.id) : /* @__PURE__ */ new Set();
1515
- return new Map(rows.map((row) => {
1516
- const validRootConversationId = row.rootConversationId !== null && row.rootConversationId === row.storedRootConversationId && row.rootRootConversationId === row.rootConversationId && row.rootParentConversationId === null && (row.parentConversationId !== null || row.storedRootConversationId === row.conversationId) ? row.rootConversationId ?? void 0 : void 0;
1517
- const visibility = validRootConversationId === void 0 ? null : row.visibility;
1518
- const isRootActor = validRootConversationId !== void 0 && viewer !== void 0 && (row.rootUserId === viewer.id || normalizedViewerEmail !== void 0 && row.rootEmailVerified === true && row.rootEmailNormalized === normalizedViewerEmail);
1519
- const isMaterializedParticipant = participantRootIds.has(row.conversationId) || validRootConversationId !== void 0 && participantRootIds.has(validRootConversationId);
1520
- const isParticipant = isRootActor || isMaterializedParticipant;
1521
- const canViewPrivateContent = isParticipant || validRootConversationId !== void 0 && canExposeConversationPayload({ visibility: visibility ?? void 0 });
1522
- return [row.conversationId, {
1523
- canViewPrivateContent,
1524
- isParticipant,
1525
- visibility
1526
- }];
1527
- }));
1528
- }
1529
- //#endregion
1530
- //#region src/chat/plugins/conversation-event-reader.ts
1531
- function registeredEventName$1(plugin, eventName) {
1532
- if (!plugin.conversationEvents?.some((definition) => definition.eventName === eventName)) throw new TypeError(`Plugin "${plugin.manifest.name}" did not register event "${eventName}"`);
1533
- return eventName;
1534
- }
1535
- /** Create Conversation event reads bound to one plugin namespace. */
1536
- function createPluginConversationEventReader(plugin) {
1537
- return { async list(input) {
1538
- const eventName = registeredEventName$1(plugin, input.eventName);
1539
- const db = getDb();
1540
- if (!(await readConversationAccessFromSql(db, [input.conversationId], input.viewer)).get(input.conversationId)?.canViewPrivateContent) return void 0;
1541
- return (await db.select({
1542
- content: sql`${juniorConversationEvents.payload}->'content'`,
1543
- createdAt: juniorConversationEvents.createdAt,
1544
- version: sql`(${juniorConversationEvents.payload}->>'version')::integer`
1545
- }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, input.conversationId), eq(juniorConversationEvents.type, "structured_event"), sql`${juniorConversationEvents.payload}->>'namespace' = ${plugin.manifest.name}`, sql`${juniorConversationEvents.payload}->>'name' = ${eventName}`)).orderBy(desc(juniorConversationEvents.seq))).map((row) => ({
1546
- content: row.content,
1547
- createdAt: row.createdAt.toISOString(),
1548
- version: row.version
1203
+ const projected = [];
1204
+ let model;
1205
+ for (const event of args.modelContextEvents ?? []) {
1206
+ model = conversationEventModel(event.data, model);
1207
+ if (event.data.type === "turn_completed" || event.data.type === "turn_failed") model = void 0;
1208
+ }
1209
+ for (const event of args.events) {
1210
+ model = conversationEventModel(event.data, model);
1211
+ const eventModel = model;
1212
+ const modelCall = conversationEventModelCall(event.data);
1213
+ if (event.data.type === "turn_completed" || event.data.type === "turn_failed") model = void 0;
1214
+ let data;
1215
+ if (event.data.type === "assistant_message" || event.data.type === "tool_result") {
1216
+ const reportArgs = {
1217
+ canExposePayload: args.canExposePayload,
1218
+ createdAtMs: event.createdAtMs,
1219
+ message: event.data,
1220
+ seq: event.seq
1221
+ };
1222
+ const result = reportingToolResultMessageSchema.safeParse(event.data);
1223
+ const start = result.success ? toolStarts.get(result.data.toolCallId) : void 0;
1224
+ data = reportAssistantMessage(reportArgs) ?? reportToolResult({
1225
+ canExposePayload: args.canExposePayload,
1226
+ message: event.data,
1227
+ ...start && start.seq < event.seq ? { start } : void 0
1228
+ });
1229
+ } else if (event.data.type === "tool_execution_started") {
1230
+ toolStarts.set(event.data.toolCallId, {
1231
+ createdAtMs: event.createdAtMs,
1232
+ name: event.data.toolName,
1233
+ seq: event.seq
1234
+ });
1235
+ data = {
1236
+ type: "tool_calls",
1237
+ calls: [{
1238
+ toolCallId: event.data.toolCallId,
1239
+ name: event.data.toolName,
1240
+ status: "running",
1241
+ startedAt: new Date(event.createdAtMs).toISOString(),
1242
+ startedSeq: event.seq
1243
+ }]
1244
+ };
1245
+ } else if (event.data.type === "subagent_started") {
1246
+ subagentStarts.set(event.data.subagentInvocationId, {
1247
+ createdAtMs: event.createdAtMs,
1248
+ data: event.data,
1249
+ seq: event.seq
1250
+ });
1251
+ data = {
1252
+ type: "subagent",
1253
+ startedSeq: event.seq,
1254
+ startedAt: new Date(event.createdAtMs).toISOString(),
1255
+ childConversationId: event.data.childConversationId,
1256
+ subagentKind: event.data.subagentKind,
1257
+ status: "running",
1258
+ ...event.data.parentToolCallId ? { parentToolCallId: event.data.parentToolCallId } : void 0
1259
+ };
1260
+ } else if (event.data.type === "subagent_ended") {
1261
+ const started = subagentStarts.get(event.data.subagentInvocationId);
1262
+ if (started && started.seq < event.seq) {
1263
+ data = {
1264
+ type: "subagent",
1265
+ startedSeq: started.seq,
1266
+ startedAt: new Date(started.createdAtMs).toISOString(),
1267
+ childConversationId: started.data.childConversationId,
1268
+ subagentKind: started.data.subagentKind,
1269
+ ...started.data.parentToolCallId ? { parentToolCallId: started.data.parentToolCallId } : void 0,
1270
+ status: event.data.outcome === "success" ? "completed" : event.data.outcome
1271
+ };
1272
+ subagentStarts.delete(event.data.subagentInvocationId);
1273
+ }
1274
+ } else if (event.data.type === "handoff") data = {
1275
+ type: "handoff",
1276
+ modelProfile: event.data.modelProfile,
1277
+ modelId: event.data.modelId,
1278
+ ...event.data.reasoningLevel ? { reasoningLevel: event.data.reasoningLevel } : void 0,
1279
+ ...event.data.triggeringToolCallId ? { triggeringToolCallId: event.data.triggeringToolCallId } : void 0,
1280
+ ...args.canExposePayload && event.data.summary ? { summary: event.data.summary } : void 0
1281
+ };
1282
+ else data = reportEventData({
1283
+ canExposePayload: args.canExposePayload,
1284
+ data: event.data
1285
+ });
1286
+ if (!data) continue;
1287
+ projected.push(conversationReportEventSchema.parse({
1288
+ seq: event.seq,
1289
+ createdAt: new Date(event.createdAtMs).toISOString(),
1290
+ data,
1291
+ ...eventModel ? { model: eventModel } : void 0,
1292
+ ...modelCall ? { modelCall } : void 0
1549
1293
  }));
1550
- } };
1294
+ }
1295
+ return projected;
1551
1296
  }
1552
1297
  //#endregion
1553
- //#region src/chat/plugins/conversation-event-stats.ts
1554
- const DAY_MS = 864e5;
1555
- const costRowSchema = z.object({
1556
- costUsd: z.number().finite().nonnegative(),
1557
- date: z.string().regex(/^\d{4}-\d{2}-\d{2}(T\d{2})?$/),
1558
- events: z.number().int().nonnegative()
1559
- }).strict();
1560
- function startOfUtcDay(value) {
1561
- const date = new Date(value);
1562
- date.setUTCHours(0, 0, 0, 0);
1563
- return date;
1298
+ //#region src/api/conversations/event-page.ts
1299
+ const conversationEventColumns = getTableColumns(juniorConversationEvents);
1300
+ const MIN_SCAN_SIZE = 64;
1301
+ /** Read bounded canonical rows while excluding model-only replacement history. */
1302
+ async function readConversationEventRows(executor, args) {
1303
+ return executor.db().select({
1304
+ ...conversationEventColumns,
1305
+ payload: sql`case
1306
+ when ${juniorConversationEvents.type} in ('compaction', 'handoff')
1307
+ then jsonb_set(
1308
+ ${juniorConversationEvents.payload},
1309
+ '{replacementHistory}',
1310
+ '[]'::jsonb
1311
+ )
1312
+ else ${juniorConversationEvents.payload}
1313
+ end`
1314
+ }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, args.conversationId), args.beforeSeq === void 0 ? void 0 : lt(juniorConversationEvents.seq, args.beforeSeq), inArray(juniorConversationEvents.type, args.types ?? [...conversationReportSourceEventTypes]), args.subagentInvocationIds === void 0 ? void 0 : inArray(sql`${juniorConversationEvents.payload}->>'subagentInvocationId'`, args.subagentInvocationIds), args.toolCallIds === void 0 ? void 0 : inArray(sql`${juniorConversationEvents.payload}->>'toolCallId'`, args.toolCallIds))).orderBy(args.direction === "forward" ? asc(juniorConversationEvents.seq) : desc(juniorConversationEvents.seq)).limit(args.limit);
1564
1315
  }
1565
- function registeredEventName(plugin, eventName) {
1566
- if (!plugin.conversationEvents?.some((definition) => definition.eventName === eventName)) throw new TypeError(`Plugin "${plugin.manifest.name}" did not register event "${eventName}"`);
1567
- return eventName;
1316
+ function decodeConversationEventRow(row) {
1317
+ return decodeStoredConversationEvent({
1318
+ schemaVersion: row.schemaVersion,
1319
+ seq: row.seq,
1320
+ historyVersion: row.historyVersion,
1321
+ ...row.idempotencyKey ? { idempotencyKey: row.idempotencyKey } : void 0,
1322
+ createdAtMs: row.createdAt.getTime(),
1323
+ type: row.type,
1324
+ payload: row.payload
1325
+ });
1568
1326
  }
1569
- /** Create aggregate event-cost reads bound to one plugin's registered namespace. */
1570
- function createPluginConversationEventStats(plugin, now = Date.now) {
1571
- return {
1572
- async costsByHour(input) {
1573
- const eventName = registeredEventName(plugin, input.eventName);
1574
- const hourCount = input.hours ?? 24;
1575
- const end = new Date(now());
1576
- end.setUTCMinutes(0, 0, 0);
1577
- const start = /* @__PURE__ */ new Date(end.getTime() - (hourCount - 1) * 60 * 60 * 1e3);
1578
- const endExclusive = new Date(end.getTime() + 36e5);
1579
- const date = sql`TO_CHAR(
1580
- ${juniorConversationEvents.createdAt} AT TIME ZONE 'UTC',
1581
- 'YYYY-MM-DD"T"HH24'
1582
- )`;
1583
- const cost = sql`CASE
1584
- WHEN jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
1585
- THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::double precision
1586
- ELSE 0
1587
- END`;
1588
- const rows = await getDb().select({
1589
- costUsd: sql`SUM(${cost})::double precision`.mapWith(Number),
1590
- date,
1591
- events: sql`COUNT(*)::integer`.mapWith(Number)
1592
- }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.type, "structured_event"), sql`${juniorConversationEvents.payload}->>'namespace' = ${plugin.manifest.name}`, sql`${juniorConversationEvents.payload}->>'name' = ${eventName}`, gte(juniorConversationEvents.createdAt, start), lt(juniorConversationEvents.createdAt, endExclusive))).groupBy(date);
1593
- const byHour = new Map(z.array(costRowSchema).parse(rows).map((row) => [row.date, row]));
1594
- return Array.from({ length: hourCount }, (_, index) => {
1595
- const key = new Date(start.getTime() + index * 60 * 60 * 1e3).toISOString().slice(0, 13);
1596
- return byHour.get(key) ?? {
1597
- costUsd: 0,
1598
- date: key,
1599
- events: 0
1600
- };
1601
- });
1602
- },
1603
- async costsByDay(input) {
1604
- const eventName = registeredEventName(plugin, input.eventName);
1605
- const end = startOfUtcDay(now());
1606
- const start = /* @__PURE__ */ new Date(end.getTime() - (input.days - 1) * DAY_MS);
1607
- const endExclusive = new Date(end.getTime() + DAY_MS);
1608
- const date = sql`TO_CHAR(
1609
- ${juniorConversationEvents.createdAt} AT TIME ZONE 'UTC',
1610
- 'YYYY-MM-DD'
1611
- )`;
1612
- const cost = sql`CASE
1613
- WHEN jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
1614
- THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::double precision
1615
- ELSE 0
1616
- END`;
1617
- const rows = await getDb().select({
1618
- costUsd: sql`SUM(${cost})::double precision`.mapWith(Number),
1619
- date,
1620
- events: sql`COUNT(*)::integer`.mapWith(Number)
1621
- }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.type, "structured_event"), sql`${juniorConversationEvents.payload}->>'namespace' = ${plugin.manifest.name}`, sql`${juniorConversationEvents.payload}->>'name' = ${eventName}`, gte(juniorConversationEvents.createdAt, start), lt(juniorConversationEvents.createdAt, endExclusive))).groupBy(date);
1622
- const byDate = new Map(z.array(costRowSchema).parse(rows).map((row) => [row.date, row]));
1623
- return Array.from({ length: input.days }, (_, index) => {
1624
- const date = new Date(start.getTime() + index * DAY_MS).toISOString().slice(0, 10);
1625
- return byDate.get(date) ?? {
1626
- costUsd: 0,
1627
- date,
1628
- events: 0
1629
- };
1630
- });
1631
- }
1632
- };
1327
+ /** Project stored rows and resolve entity starts that precede the page. */
1328
+ async function projectConversationEventRows(executor, args) {
1329
+ const events = args.rows.map(decodeConversationEventRow).sort((left, right) => left.seq - right.seq);
1330
+ const modelContextRows = events[0] ? (await Promise.all([readConversationEventRows(executor, {
1331
+ conversationId: args.conversationId,
1332
+ beforeSeq: events[0].seq,
1333
+ direction: "backward",
1334
+ limit: 1,
1335
+ types: [
1336
+ "turn_started",
1337
+ "turn_routed",
1338
+ "handoff",
1339
+ "turn_completed",
1340
+ "turn_failed"
1341
+ ]
1342
+ }), readConversationEventRows(executor, {
1343
+ conversationId: args.conversationId,
1344
+ beforeSeq: events[0].seq,
1345
+ direction: "backward",
1346
+ limit: 1,
1347
+ types: ["assistant_message"]
1348
+ })])).flat().sort((left, right) => left.seq - right.seq) : [];
1349
+ const endedInvocationIds = [...new Set(events.flatMap((event) => event.data.type === "subagent_ended" ? [event.data.subagentInvocationId] : []))];
1350
+ const subagentStartRows = endedInvocationIds.length === 0 ? [] : await readConversationEventRows(executor, {
1351
+ conversationId: args.conversationId,
1352
+ direction: "forward",
1353
+ limit: endedInvocationIds.length,
1354
+ subagentInvocationIds: endedInvocationIds,
1355
+ types: ["subagent_started"]
1356
+ });
1357
+ const toolResultIds = conversationReportToolResultIds(events);
1358
+ const toolStartRows = toolResultIds.length === 0 ? [] : await readConversationEventRows(executor, {
1359
+ conversationId: args.conversationId,
1360
+ direction: "forward",
1361
+ limit: toolResultIds.length,
1362
+ toolCallIds: toolResultIds,
1363
+ types: ["tool_execution_started"]
1364
+ });
1365
+ if (args.canExposePayload) await hydrateMessageAttachments(executor, args.conversationId, events);
1366
+ return projectConversationReportEventPage({
1367
+ canExposePayload: args.canExposePayload,
1368
+ events,
1369
+ subagentStartEvents: subagentStartRows.map(decodeConversationEventRow),
1370
+ toolStartEvents: toolStartRows.map(decodeConversationEventRow),
1371
+ modelContextEvents: modelContextRows.map(decodeConversationEventRow)
1372
+ });
1633
1373
  }
1634
- //#endregion
1635
- //#region src/chat/plugins/logging.ts
1636
- /** Create the host logger exposed to plugin hooks. */
1637
- function createPluginLogger(plugin) {
1374
+ /**
1375
+ * Read the latest reporting events before an exclusive boundary.
1376
+ *
1377
+ * Source rows are scanned in bounded chunks until the requested projected page
1378
+ * and one older event are known. This keeps ordinary pages bounded without
1379
+ * assuming every agent history event produces a reporting event.
1380
+ */
1381
+ async function readConversationEventPage(executor, args) {
1382
+ const scanSize = Math.max(args.limit + 1, MIN_SCAN_SIZE);
1383
+ const rows = [];
1384
+ let beforeSeq = args.beforeSeq;
1385
+ let projected = [];
1386
+ while (projected.length <= args.limit) {
1387
+ const batch = await readConversationEventRows(executor, {
1388
+ beforeSeq,
1389
+ conversationId: args.conversationId,
1390
+ direction: "backward",
1391
+ limit: scanSize
1392
+ });
1393
+ if (batch.length === 0) break;
1394
+ rows.push(...batch);
1395
+ projected = await projectConversationEventRows(executor, {
1396
+ canExposePayload: args.canExposePayload,
1397
+ conversationId: args.conversationId,
1398
+ rows
1399
+ });
1400
+ if (projected.length > args.limit || batch.length < scanSize) break;
1401
+ beforeSeq = batch.at(-1).seq;
1402
+ }
1403
+ const events = projected.slice(-args.limit);
1638
1404
  return {
1639
- info(message, metadata) {
1640
- logInfo("plugin.log.info", {
1641
- "app.log.message": message,
1642
- "app.plugin.name": plugin,
1643
- ...metadata
1644
- });
1645
- },
1646
- warn(message, metadata) {
1647
- logWarn("plugin.log.warn", {
1648
- "app.log.message": message,
1649
- "app.plugin.name": plugin,
1650
- ...metadata
1651
- });
1652
- },
1653
- error(message, metadata) {
1654
- logException(new Error(message), "plugin.log.error", {
1655
- "app.plugin.name": plugin,
1656
- ...metadata
1657
- });
1658
- }
1405
+ events,
1406
+ ...projected.length > events.length && events[0] ? { previousSeq: events[0].seq } : void 0
1659
1407
  };
1660
1408
  }
1661
1409
  //#endregion
1662
- //#region src/chat/plugins/model.ts
1663
- /** Create the host-owned structured model capability exposed to plugins. */
1664
- function createPluginModel(pluginName, options = {}, runtime = {}) {
1665
- return { async completeObject(input) {
1666
- const modelId = options.structuredModelId ?? (options.structuredModel === "default" ? defaultModelId(botConfig) : botConfig.fastModelId);
1667
- const result = await completeObject({
1668
- modelId,
1669
- schema: input.schema,
1670
- prompt: input.prompt,
1671
- ...input.system !== void 0 ? { system: input.system } : void 0,
1672
- ...input.maxTokens !== void 0 ? { maxTokens: input.maxTokens } : void 0,
1673
- signal: runtime.signal,
1674
- promptName: `${pluginName}.structured_completion`,
1675
- metadata: {
1676
- pluginName,
1677
- pluginModelRole: "structured"
1678
- }
1410
+ //#region src/chat/briefs/input.ts
1411
+ const EVENT_PAGE_SIZE = 500;
1412
+ async function readReportEventsThrough(executor, conversationId, throughSeq) {
1413
+ const pages = [];
1414
+ let beforeSeq = throughSeq + 1;
1415
+ do {
1416
+ const page = await readConversationEventPage(executor, {
1417
+ beforeSeq,
1418
+ canExposePayload: true,
1419
+ conversationId,
1420
+ limit: EVENT_PAGE_SIZE
1679
1421
  });
1680
- return {
1681
- object: result.object,
1682
- ...result.costUsd !== void 0 ? { costUsd: result.costUsd } : void 0
1683
- };
1684
- } };
1422
+ pages.unshift(page.events);
1423
+ beforeSeq = page.previousSeq;
1424
+ } while (beforeSeq !== void 0);
1425
+ const bySequence = /* @__PURE__ */ new Map();
1426
+ for (const event of pages.flat()) bySequence.set(event.seq, event);
1427
+ return [...bySequence.values()].sort((left, right) => left.seq - right.seq);
1685
1428
  }
1686
- /** Create the host-owned embedding capability exposed to prompt hooks. */
1687
- function createPluginEmbedder(pluginName, runtime = {}) {
1688
- return { async embedTexts(input) {
1689
- return await embedTexts({
1690
- modelId: botConfig.embeddingModelId,
1691
- texts: input.texts,
1692
- signal: runtime.signal,
1693
- metadata: {
1694
- pluginName,
1695
- pluginModelRole: "embedding"
1696
- }
1697
- });
1698
- } };
1429
+ /**
1430
+ * Build generator input from SQL through one durable event. The caller owns
1431
+ * the boundary, so a Turn that completes during the read cannot be covered by
1432
+ * a Brief that never saw it.
1433
+ */
1434
+ async function briefInputFromSql(executor, conversationId, throughSeq) {
1435
+ const db = executor.db();
1436
+ const conversation = await createSqlStore(executor).get({ conversationId });
1437
+ if (!conversation) throw new Error(`Conversation ${conversationId} is unavailable`);
1438
+ const [events, annotations, codeChanges] = await Promise.all([
1439
+ readReportEventsThrough(executor, conversationId, throughSeq),
1440
+ listConversationAnnotations(db, conversationId),
1441
+ db.select({
1442
+ closedAt: juniorCodeChanges.closedAt,
1443
+ mergedAt: juniorCodeChanges.mergedAt,
1444
+ number: juniorCodeChanges.number,
1445
+ openedAt: juniorCodeChanges.openedAt,
1446
+ repository: juniorCodeRepositories.name,
1447
+ state: juniorCodeChanges.state,
1448
+ title: juniorCodeChanges.title,
1449
+ url: juniorCodeChanges.url
1450
+ }).from(juniorCodeChanges).innerJoin(juniorCodeRepositories, eq(juniorCodeRepositories.id, juniorCodeChanges.repositoryId)).where(and(arrayContains(juniorCodeChanges.conversationIds, [conversationId]), isNotNull(juniorCodeChanges.url)))
1451
+ ]);
1452
+ const location = conversation.location;
1453
+ return parseBriefInput({
1454
+ conversationId,
1455
+ ...conversation.title ? { title: conversation.title } : void 0,
1456
+ visibility: conversation.visibility ?? "private",
1457
+ ...location ? { location: {
1458
+ provider: location.provider,
1459
+ ...conversation.channelName ? { channelName: conversation.channelName } : void 0
1460
+ } } : void 0,
1461
+ entries: briefEntriesFromReportEvents(events),
1462
+ codeChanges: codeChanges.map((change) => ({
1463
+ repository: change.repository,
1464
+ number: change.number,
1465
+ ...change.title ? { title: change.title } : void 0,
1466
+ url: change.url,
1467
+ state: change.state,
1468
+ openedAt: change.openedAt.toISOString(),
1469
+ ...change.mergedAt ? { mergedAt: change.mergedAt.toISOString() } : void 0,
1470
+ ...change.closedAt ? { closedAt: change.closedAt.toISOString() } : void 0
1471
+ })),
1472
+ resources: annotations.map((annotation) => ({
1473
+ label: annotation.label,
1474
+ url: annotation.url,
1475
+ ...annotation.status ? { status: annotation.status } : void 0
1476
+ }))
1477
+ });
1699
1478
  }
1700
1479
  //#endregion
1701
- //#region src/chat/plugins/state.ts
1702
- const MAX_PLUGIN_STATE_KEY_LENGTH = 512;
1703
- function hashKeyPart(value) {
1704
- return createHash("sha256").update(value).digest("hex").slice(0, 32);
1480
+ //#region src/chat/briefs/operational-report.ts
1481
+ const DAY_MS$1 = 864e5;
1482
+ const WINDOWS = [
1483
+ 7,
1484
+ 30,
1485
+ 90
1486
+ ];
1487
+ function startOfUtcDay$1(value) {
1488
+ const date = new Date(value);
1489
+ date.setUTCHours(0, 0, 0, 0);
1490
+ return date;
1705
1491
  }
1706
- function pluginStateKey(plugin, key) {
1707
- const pluginPrefix = `junior:${plugin}`;
1708
- if (key === pluginPrefix || key.startsWith(`${pluginPrefix}:`)) return key;
1709
- return `junior:plugin_state:${hashKeyPart(plugin)}:${hashKeyPart(key)}`;
1492
+ function formatCount(value) {
1493
+ return new Intl.NumberFormat("en-US").format(value);
1710
1494
  }
1711
- function validatePluginStateKey(key) {
1712
- if (!key.trim()) throw new Error("Plugin state key is required");
1713
- if (key.length > MAX_PLUGIN_STATE_KEY_LENGTH) throw new Error("Plugin state key exceeds the maximum length");
1495
+ function formatUsd(value) {
1496
+ const maximumFractionDigits = value > 0 && value < .01 ? 4 : 2;
1497
+ return new Intl.NumberFormat("en-US", {
1498
+ currency: "USD",
1499
+ maximumFractionDigits,
1500
+ minimumFractionDigits: 2,
1501
+ style: "currency"
1502
+ }).format(value);
1714
1503
  }
1715
- /** Create a durable state namespace scoped to one plugin. */
1716
- function createPluginState(plugin, adapter) {
1717
- const getAdapter = () => adapter ?? getStateAdapter();
1504
+ /**
1505
+ * Build Brief storage and model-cost diagnostics for the System page. Cost
1506
+ * comes from retained Conversation events, so spend on purged private
1507
+ * Conversations leaves the report with them; the labels say so.
1508
+ */
1509
+ async function buildBriefsOperationalReport(args) {
1510
+ const newestDay = startOfUtcDay$1(args.nowMs);
1511
+ const windowStart = /* @__PURE__ */ new Date(newestDay.getTime() - 29 * DAY_MS$1);
1512
+ const windowEnd = new Date(newestDay.getTime() + DAY_MS$1);
1513
+ const [[counts], recentCounts] = await Promise.all([args.db.select({
1514
+ conversations: sql`count(distinct ${juniorConversationBriefs.conversationId})`.mapWith(Number),
1515
+ stored: sql`count(*)`.mapWith(Number)
1516
+ }).from(juniorConversationBriefs), args.db.select({ stored: sql`count(*)`.mapWith(Number) }).from(juniorConversationBriefs).where(and(gte(juniorConversationBriefs.createdAt, windowStart), lt(juniorConversationBriefs.createdAt, windowEnd)))]);
1517
+ const recentDays = args.briefDays.filter((day) => {
1518
+ const dayStart = Date.parse(`${day.date}T00:00:00.000Z`);
1519
+ return dayStart >= windowStart.getTime() && dayStart < windowEnd.getTime();
1520
+ });
1521
+ const recentCost = recentDays.reduce((total, day) => total + day.costUsd, 0);
1522
+ const recentBriefs = recentDays.reduce((total, day) => total + day.events, 0);
1718
1523
  return {
1719
- async delete(key) {
1720
- validatePluginStateKey(key);
1721
- const state = getAdapter();
1722
- await state.connect();
1723
- await state.delete(pluginStateKey(plugin, key));
1724
- },
1725
- async get(key) {
1726
- validatePluginStateKey(key);
1727
- const state = getAdapter();
1728
- await state.connect();
1729
- return await state.get(pluginStateKey(plugin, key)) ?? void 0;
1730
- },
1731
- async set(key, value, ttlMs) {
1732
- validatePluginStateKey(key);
1733
- const state = getAdapter();
1734
- await state.connect();
1735
- await state.set(pluginStateKey(plugin, key), value, ttlMs);
1736
- },
1737
- async setIfNotExists(key, value, ttlMs) {
1738
- validatePluginStateKey(key);
1739
- const state = getAdapter();
1740
- await state.connect();
1741
- return await state.setIfNotExists(pluginStateKey(plugin, key), value, ttlMs);
1742
- },
1743
- async withLock(key, ttlMs, callback) {
1744
- validatePluginStateKey(key);
1745
- const state = getAdapter();
1746
- await state.connect();
1747
- const lockKey = pluginStateKey(plugin, key);
1748
- const lock = await state.acquireLock(lockKey, ttlMs);
1749
- if (!lock) throw new Error(`Could not acquire plugin state lock for ${key}`);
1750
- try {
1751
- return await callback();
1752
- } finally {
1753
- await state.releaseLock(lock);
1524
+ generatedAt: new Date(args.nowMs).toISOString(),
1525
+ title: "Briefs",
1526
+ metrics: [
1527
+ {
1528
+ label: "briefs stored",
1529
+ tone: counts?.stored ? "good" : "neutral",
1530
+ value: formatCount(counts?.stored ?? 0)
1531
+ },
1532
+ {
1533
+ label: "conversations with a brief",
1534
+ value: formatCount(counts?.conversations ?? 0)
1535
+ },
1536
+ {
1537
+ label: "briefs · 30d",
1538
+ value: formatCount(recentCounts[0]?.stored ?? 0)
1539
+ },
1540
+ {
1541
+ label: "cost · 30d · retained",
1542
+ value: formatUsd(recentCost)
1543
+ },
1544
+ {
1545
+ label: "average cost per brief · 30d · retained",
1546
+ value: formatUsd(recentBriefs === 0 ? 0 : recentCost / recentBriefs)
1754
1547
  }
1755
- }
1548
+ ],
1549
+ widgets: [{
1550
+ categories: args.briefDays.map((day) => ({
1551
+ id: day.date,
1552
+ label: day.date,
1553
+ values: {
1554
+ briefs: day.events,
1555
+ costUsd: day.costUsd
1556
+ }
1557
+ })),
1558
+ description: "Stored Brief versions and estimated model cost",
1559
+ id: "briefs-created",
1560
+ series: [{
1561
+ key: "briefs",
1562
+ label: "Briefs"
1563
+ }, {
1564
+ format: "usd",
1565
+ key: "costUsd",
1566
+ label: "Cost"
1567
+ }],
1568
+ timeRangeDays: [...WINDOWS],
1569
+ title: "Briefs created",
1570
+ type: "bar_chart"
1571
+ }]
1756
1572
  };
1757
1573
  }
1758
1574
  //#endregion
1759
- //#region src/chat/dashboard-link.ts
1760
- let dashboardConversationLinkOptions;
1761
- function withHttps(host) {
1762
- return /^https?:\/\//.test(host) ? host : `https://${host}`;
1763
- }
1764
- function stripTrailingSlashes(value) {
1765
- let end = value.length;
1766
- while (end > 1 && value.charCodeAt(end - 1) === 47) end -= 1;
1767
- return end === value.length ? value : value.slice(0, end);
1575
+ //#region src/chat/briefs/store.ts
1576
+ function briefVersionFromRow(row) {
1577
+ return {
1578
+ conversationId: row.conversationId,
1579
+ version: row.version,
1580
+ turnId: row.turnId,
1581
+ throughSeq: row.throughSeq,
1582
+ content: conversationBriefSchema.parse(row.content),
1583
+ searchText: row.searchText,
1584
+ modelId: row.modelId,
1585
+ ...row.costUsd !== null ? { costUsd: row.costUsd } : void 0,
1586
+ createdAt: row.createdAt
1587
+ };
1768
1588
  }
1769
- function normalizeDashboardPath(path, fallback) {
1770
- const value = path?.trim() || fallback;
1771
- return stripTrailingSlashes(value.startsWith("/") ? value : `/${value}`);
1589
+ /** Append the next Brief version, or return the row for an existing Turn. */
1590
+ async function appendConversationBrief(db, input) {
1591
+ return await db.transaction(async (tx) => {
1592
+ const conversation = (await tx.select({
1593
+ conversationId: juniorConversations.conversationId,
1594
+ transcriptPurgedAt: juniorConversations.transcriptPurgedAt
1595
+ }).from(juniorConversations).where(eq(juniorConversations.conversationId, input.conversationId)).for("update"))[0];
1596
+ if (!conversation) throw new Error(`Conversation ${input.conversationId} is unavailable`);
1597
+ if (conversation.transcriptPurgedAt) {
1598
+ if ((await resolveRootVisibility({ db: () => tx }, input.conversationId)).visibility !== "public") throw new Error(`Cannot append a Brief to purged non-public Conversation ${input.conversationId}`);
1599
+ }
1600
+ const existing = await tx.select().from(juniorConversationBriefs).where(and(eq(juniorConversationBriefs.conversationId, input.conversationId), eq(juniorConversationBriefs.turnId, input.turnId))).limit(1);
1601
+ if (existing[0]) return {
1602
+ inserted: false,
1603
+ value: briefVersionFromRow(existing[0])
1604
+ };
1605
+ const version = ((await tx.select({ version: max(juniorConversationBriefs.version) }).from(juniorConversationBriefs).where(eq(juniorConversationBriefs.conversationId, input.conversationId)))[0]?.version ?? 0) + 1;
1606
+ const row = (await tx.insert(juniorConversationBriefs).values({
1607
+ conversationId: input.conversationId,
1608
+ version,
1609
+ turnId: input.turnId,
1610
+ throughSeq: input.throughSeq,
1611
+ content: conversationBriefSchema.parse(input.content),
1612
+ searchText: input.searchText,
1613
+ modelId: input.modelId,
1614
+ costUsd: input.costUsd ?? null
1615
+ }).onConflictDoNothing({ target: [juniorConversationBriefs.conversationId, juniorConversationBriefs.turnId] }).returning())[0];
1616
+ if (!row) throw new Error("Brief insert conflicted after the Turn check");
1617
+ return {
1618
+ inserted: true,
1619
+ value: briefVersionFromRow(row)
1620
+ };
1621
+ });
1772
1622
  }
1773
- function resolveDashboardBaseURL(config) {
1774
- const explicit = config.baseURL ?? process.env.JUNIOR_BASE_URL;
1775
- if (explicit?.trim()) return stripTrailingSlashes(withHttps(explicit.trim()));
1776
- const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
1777
- if (vercelProd) return stripTrailingSlashes(withHttps(vercelProd));
1778
- const vercelUrl = process.env.VERCEL_URL?.trim();
1779
- if (vercelUrl) return stripTrailingSlashes(withHttps(vercelUrl));
1780
- return "http://localhost:3000";
1623
+ /** Read the Brief version produced for one completed Turn. */
1624
+ async function readConversationBriefForTurn(db, conversationId, turnId) {
1625
+ const rows = await db.select().from(juniorConversationBriefs).where(and(eq(juniorConversationBriefs.conversationId, conversationId), eq(juniorConversationBriefs.turnId, turnId))).limit(1);
1626
+ return rows[0] ? briefVersionFromRow(rows[0]) : void 0;
1781
1627
  }
1782
- /** Configure core dashboard links. */
1783
- function setDashboardConversationLinkOptions(options) {
1784
- const previous = dashboardConversationLinkOptions;
1785
- dashboardConversationLinkOptions = options?.disabled ? void 0 : options;
1786
- return previous;
1628
+ /** Read the latest Brief version for one Conversation. */
1629
+ async function readLatestConversationBrief(db, conversationId) {
1630
+ const rows = await db.select().from(juniorConversationBriefs).where(eq(juniorConversationBriefs.conversationId, conversationId)).orderBy(desc(juniorConversationBriefs.version)).limit(1);
1631
+ return rows[0] ? briefVersionFromRow(rows[0]) : void 0;
1787
1632
  }
1788
- function resolveDashboardPath(segmentPath) {
1789
- if (!dashboardConversationLinkOptions) return;
1790
- const baseURL = resolveDashboardBaseURL(dashboardConversationLinkOptions);
1791
- const basePath = normalizeDashboardPath(dashboardConversationLinkOptions.basePath, "/");
1792
- return `${baseURL}${basePath === "/" ? segmentPath : `${basePath}${segmentPath}`}`;
1633
+ //#endregion
1634
+ //#region src/chat/briefs/task.ts
1635
+ const BRIEF_LOCK_TTL_MS = 6e5;
1636
+ function hasUserInstruction(run) {
1637
+ return run.transcript.some((entry) => entry.type === "message" && entry.role === "user" && entry.provenance?.authority === "instruction");
1793
1638
  }
1794
- /** Build the dashboard conversation URL when the core dashboard is enabled. */
1795
- function getDashboardConversationLink(conversationId) {
1796
- return resolveDashboardPath(`/conversations/${encodeURIComponent(conversationId)}`);
1639
+ function logSkip(context, conversationId, reason) {
1640
+ context.log.info("Brief update skipped", {
1641
+ conversationId,
1642
+ reason
1643
+ });
1797
1644
  }
1798
- /** Build the dashboard task detail URL when the core dashboard is enabled. */
1799
- function getDashboardTaskLink(taskId) {
1800
- return resolveDashboardPath(`/automations/${encodeURIComponent(taskId)}`);
1645
+ async function readTurnCompletedSeq(db, conversationId, turnId) {
1646
+ const seq = (await db.select({ seq: juniorConversationEvents.seq }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, conversationId), eq(juniorConversationEvents.type, "turn_completed"), sql`${juniorConversationEvents.payload}->>'turnId' = ${turnId}`)).orderBy(desc(juniorConversationEvents.seq)).limit(1))[0]?.seq;
1647
+ if (seq === void 0) throw new Error(`Completed Turn ${turnId} has no terminal event in Conversation ${conversationId}`);
1648
+ return seq;
1801
1649
  }
1802
- /** Public icon asset URL. Headless and local installs keep text-only identity. */
1803
- function getDashboardObjectIconLink(icon) {
1804
- if (!dashboardConversationLinkOptions) return void 0;
1805
- const url = new URL(objectIconPath(icon), resolveDashboardBaseURL(dashboardConversationLinkOptions));
1806
- if (url.protocol !== "https:" || url.hostname === "localhost") return void 0;
1807
- return url.href;
1650
+ async function emitBriefUpdated(context, stored) {
1651
+ await context.events.emit(briefUpdatedEvent({
1652
+ version: stored.version,
1653
+ modelId: stored.modelId,
1654
+ ...stored.costUsd !== void 0 ? { costUsd: stored.costUsd } : void 0,
1655
+ decisions: stored.content.decisions.length,
1656
+ openDecisions: stored.content.openDecisions.length,
1657
+ links: stored.content.links.length
1658
+ }));
1808
1659
  }
1809
- //#endregion
1810
- //#region src/chat/state/ttl.ts
1811
- /** Retain expiring conversation/runtime state long enough for resume and dashboard history. */
1812
- const JUNIOR_THREAD_STATE_TTL_MS = 6048e5;
1813
- //#endregion
1814
- //#region src/chat/events/timer-index.ts
1815
- const DUE_KEY = "junior:watches:due";
1816
- const CLAIM_MS = 12e4;
1817
- const BATCH_SIZE = 25;
1818
- const entriesSchema = z.array(z.object({
1819
- id: z.string(),
1820
- atMs: z.number()
1821
- }));
1822
- /** Keep timer index operations atomic in Redis and the local memory adapter. */
1823
- async function updateTimerIndex(state, operation) {
1824
- const redis = await getDefaultRedisStateAdapterFor(state);
1825
- if (redis) {
1826
- const prefix = getChatConfig().state.keyPrefix;
1827
- const key = prefix ? `${prefix}:${DUE_KEY}` : DUE_KEY;
1828
- const client = redis.getClient();
1829
- if (operation.kind === "add") {
1830
- await client.sendCommand([
1831
- "ZADD",
1832
- key,
1833
- String(operation.atMs),
1834
- operation.id
1835
- ]);
1836
- return [];
1837
- }
1838
- if (operation.kind === "remove") {
1839
- await client.sendCommand([
1840
- "ZREM",
1841
- key,
1842
- operation.id
1843
- ]);
1844
- return [];
1845
- }
1846
- return await client.sendCommand([
1847
- "EVAL",
1848
- `local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, ARGV[3])
1849
- for _, id in ipairs(ids) do
1850
- redis.call('ZADD', KEYS[1], ARGV[2], id)
1851
- end
1852
- return ids`,
1853
- "1",
1854
- key,
1855
- String(operation.nowMs),
1856
- String(operation.nowMs + CLAIM_MS),
1857
- String(BATCH_SIZE)
1858
- ]);
1660
+ /** Generate and store the next Brief after one completed Turn. */
1661
+ async function updateConversationBrief(context) {
1662
+ const run = await context.run.load();
1663
+ if (run.source.kind !== "slack" && run.source.kind !== "web" && run.source.kind !== "local") {
1664
+ logSkip(context, run.conversationId, "unsupported_source");
1665
+ return;
1859
1666
  }
1860
- const lock = await state.acquireLock(`${DUE_KEY}:lock`, 1e4);
1861
- if (!lock) throw new Error("Could not acquire timer index lock");
1862
- try {
1863
- let entries = entriesSchema.parse(await state.get(DUE_KEY) ?? []);
1864
- let ids = [];
1865
- if (operation.kind === "claim") {
1866
- ids = entries.filter((entry) => entry.atMs <= operation.nowMs).sort((a, b) => a.atMs - b.atMs).slice(0, BATCH_SIZE).map((entry) => entry.id);
1867
- const claimed = new Set(ids);
1868
- entries = entries.map((entry) => claimed.has(entry.id) ? {
1869
- ...entry,
1870
- atMs: operation.nowMs + CLAIM_MS
1871
- } : entry);
1872
- } else {
1873
- entries = entries.filter((entry) => entry.id !== operation.id);
1874
- if (operation.kind === "add") entries.push({
1875
- id: operation.id,
1876
- atMs: operation.atMs
1877
- });
1667
+ const executor = getSqlExecutor();
1668
+ const conversation = await createSqlStore(executor).get({ conversationId: run.conversationId });
1669
+ if (conversation?.parentConversationId) {
1670
+ logSkip(context, run.conversationId, "child_conversation");
1671
+ return;
1672
+ }
1673
+ if (conversation?.transcriptPurgedAtMs !== void 0) {
1674
+ if ((await resolveRootVisibility(executor, run.conversationId)).visibility !== "public") {
1675
+ logSkip(context, run.conversationId, "purged_private");
1676
+ return;
1878
1677
  }
1879
- await state.set(DUE_KEY, entries);
1880
- return ids;
1881
- } finally {
1882
- await state.releaseLock(lock);
1883
1678
  }
1679
+ if (!hasUserInstruction(run)) {
1680
+ logSkip(context, run.conversationId, "no_user_instruction");
1681
+ return;
1682
+ }
1683
+ await context.state.withLock(`brief:${run.conversationId}`, BRIEF_LOCK_TTL_MS, async () => {
1684
+ const db = context.db;
1685
+ const existing = await readConversationBriefForTurn(db, run.conversationId, run.runId);
1686
+ if (existing) {
1687
+ await emitBriefUpdated(context, existing);
1688
+ logSkip(context, run.conversationId, "turn_already_stored");
1689
+ return;
1690
+ }
1691
+ const terminalSeq = await readTurnCompletedSeq(db, run.conversationId, run.runId);
1692
+ const previous = await readLatestConversationBrief(db, run.conversationId);
1693
+ if (previous && previous.throughSeq >= terminalSeq) {
1694
+ logSkip(context, run.conversationId, "turn_already_covered");
1695
+ return;
1696
+ }
1697
+ const input = await briefInputFromSql(executor, run.conversationId, terminalSeq);
1698
+ const modelId = defaultModelId(botConfig);
1699
+ const generation = await generateBrief({
1700
+ completeObject: (request) => completeObject({
1701
+ ...request,
1702
+ modelId,
1703
+ promptName: "junior.brief_update"
1704
+ }),
1705
+ input,
1706
+ previous: previous?.content,
1707
+ prompt: BRIEF_PROMPT,
1708
+ throughIndex: terminalSeq
1709
+ });
1710
+ const stored = await appendConversationBrief(db, {
1711
+ conversationId: run.conversationId,
1712
+ turnId: run.runId,
1713
+ throughSeq: terminalSeq,
1714
+ content: generation.brief,
1715
+ searchText: generation.searchText,
1716
+ modelId,
1717
+ ...generation.costUsd !== void 0 ? { costUsd: generation.costUsd } : void 0
1718
+ });
1719
+ await emitBriefUpdated(context, stored.value);
1720
+ if (!stored.inserted) logSkip(context, run.conversationId, "turn_already_stored");
1721
+ });
1722
+ }
1723
+ /** Core task registration kept outside the installed plugin catalog. */
1724
+ const briefsTaskRegistration = {
1725
+ manifest: {
1726
+ name: "briefs",
1727
+ displayName: "Briefs",
1728
+ description: "Durable Conversation Brief generation"
1729
+ },
1730
+ conversationEvents: [briefUpdatedEvent],
1731
+ hooks: { async operationalReport(context) {
1732
+ return await buildBriefsOperationalReport({
1733
+ briefDays: await context.eventStats.costsByDay({
1734
+ days: 90,
1735
+ eventName: "brief_updated"
1736
+ }),
1737
+ db: context.db,
1738
+ nowMs: context.nowMs
1739
+ });
1740
+ } },
1741
+ tasks: { updateBrief: { async run(context) {
1742
+ await updateConversationBrief(context);
1743
+ } } }
1744
+ };
1745
+ //#endregion
1746
+ //#region src/chat/plugins/conversation-events.ts
1747
+ function registeredDefinition(plugin, value) {
1748
+ return plugin.conversationEvents?.find((definition) => definition === value.definition);
1749
+ }
1750
+ /** Keep operation idempotency stable when a registered event schema advances. */
1751
+ function eventIdentityVersion(plugin, definition) {
1752
+ let version = definition.version;
1753
+ for (const candidate of plugin.conversationEvents ?? []) if (candidate.eventName === definition.eventName) version = Math.min(version, candidate.version);
1754
+ return version;
1755
+ }
1756
+ /** Create a conversation-bound writer for one plugin operation. */
1757
+ function createPluginConversationEvents(args) {
1758
+ return { async emit(value) {
1759
+ const definition = registeredDefinition(args.plugin, value);
1760
+ if (!definition) throw new Error(`Plugin "${args.plugin.manifest.name}" cannot emit an unregistered conversation event.`);
1761
+ const content = definition.parse(value.data);
1762
+ const identityVersion = eventIdentityVersion(args.plugin, definition);
1763
+ await getConversationEventStore().append(args.conversationId, [{
1764
+ createdAtMs: Date.now(),
1765
+ idempotencyKey: `plugin:${args.plugin.manifest.name}:operation:${args.operationId}:event:${definition.eventName}@${identityVersion}`,
1766
+ data: {
1767
+ type: "structured_event",
1768
+ namespace: args.plugin.manifest.name,
1769
+ name: definition.eventName,
1770
+ version: definition.version,
1771
+ turnId: args.turnId,
1772
+ content
1773
+ }
1774
+ }], { activity: "preserve" });
1775
+ } };
1776
+ }
1777
+ /** Render a stored plugin event through its currently registered definition. */
1778
+ function renderPluginConversationEvent(args) {
1779
+ const definition = [briefsTaskRegistration, ...getPlugins()].find((candidate) => candidate.manifest.name === args.namespace)?.conversationEvents?.find((candidate) => candidate.eventName === args.name && candidate.version === args.version);
1780
+ if (!definition) return void 0;
1781
+ return definition.renderEvent(definition.parse(args.content));
1884
1782
  }
1885
1783
  //#endregion
1886
- //#region src/chat/slack/workspace-context.ts
1887
- const workspaceTeamIdStorage = new AsyncLocalStorage();
1888
- /** Run a callback with the Slack workspace team ID for the inbound webhook. */
1889
- function runWithWorkspaceTeamId(teamId, fn) {
1890
- if (!teamId) return fn();
1891
- return workspaceTeamIdStorage.run(teamId, fn);
1784
+ //#region src/api/conversations/membership.ts
1785
+ /** True when the viewer is a materialized participant on this root conversation. */
1786
+ function conversationHasParticipantUser(userId) {
1787
+ const participant = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorConversationParticipants.userId, userId)));
1788
+ return exists(participant);
1892
1789
  }
1893
- /** Return the Slack workspace team ID for the current inbound webhook. */
1894
- function getWorkspaceTeamId() {
1895
- return workspaceTeamIdStorage.getStore();
1790
+ /**
1791
+ * True when a verified primary-email user participates on this root
1792
+ * conversation. Used only for the actorEmail feed fallback.
1793
+ */
1794
+ function conversationHasParticipantEmail(email) {
1795
+ const participant = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorUsers.primaryEmailNormalized, email)));
1796
+ return exists(participant);
1896
1797
  }
1897
- //#endregion
1898
- //#region src/chat/slack/client.ts
1899
- var SlackActionError = class extends Error {
1900
- code;
1901
- apiError;
1902
- needed;
1903
- provided;
1904
- statusCode;
1905
- requestId;
1906
- errorData;
1907
- retryAfterSeconds;
1908
- detail;
1909
- detailLine;
1910
- detailRule;
1911
- constructor(message, code, options = {}) {
1912
- super(message);
1913
- this.name = "SlackActionError";
1914
- this.code = code;
1915
- this.apiError = options.apiError;
1916
- this.needed = options.needed;
1917
- this.provided = options.provided;
1918
- this.statusCode = options.statusCode;
1919
- this.requestId = options.requestId;
1920
- this.errorData = options.errorData;
1921
- this.retryAfterSeconds = options.retryAfterSeconds;
1922
- this.detail = options.detail;
1923
- this.detailLine = options.detailLine;
1924
- this.detailRule = options.detailRule;
1925
- }
1926
- };
1927
- function serializeSlackErrorData(data) {
1928
- if (!data || typeof data !== "object") return;
1929
- const filtered = Object.fromEntries(Object.entries(data).filter(([key]) => key !== "error"));
1930
- if (Object.keys(filtered).length === 0) return;
1931
- try {
1932
- const serialized = JSON.stringify(filtered);
1933
- return serialized.length <= 600 ? serialized : `${serialized.slice(0, 597)}...`;
1934
- } catch {
1935
- return;
1936
- }
1798
+ /** True when one user's personal feed has archived this root after the cutoff. */
1799
+ function conversationArchivedForUser(userId, archivedAfter) {
1800
+ const archive = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorConversationParticipants.userId, userId), isNotNull(juniorConversationParticipants.archivedAt), archivedAfter ? gte(juniorConversationParticipants.archivedAt, archivedAfter) : void 0));
1801
+ return exists(archive);
1937
1802
  }
1938
- /** Extract a header value by case-insensitive name from a raw headers object. */
1939
- function getHeaderString(headers, name) {
1940
- if (!headers || typeof headers !== "object") return;
1941
- const key = name.toLowerCase();
1942
- const entries = headers;
1943
- for (const [entryKey, value] of Object.entries(entries)) {
1944
- if (entryKey.toLowerCase() !== key) continue;
1945
- if (typeof value === "string") return value;
1946
- if (Array.isArray(value)) {
1947
- const first = value.find((entry) => typeof entry === "string");
1948
- return typeof first === "string" ? first : void 0;
1949
- }
1950
- }
1803
+ /** True when one user's personal feed has not archived this root. */
1804
+ function conversationNotArchivedForUser(userId) {
1805
+ const archive = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorConversationParticipants.userId, userId), isNotNull(juniorConversationParticipants.archivedAt)));
1806
+ return notExists(archive);
1951
1807
  }
1952
- function parseSlackCanvasDetail(detail) {
1953
- if (typeof detail !== "string") return {};
1954
- const trimmed = detail.trim();
1955
- if (!trimmed) return {};
1956
- const parsed = { detail: trimmed };
1957
- const lineMatch = trimmed.match(/line\s+(\d+):/i);
1958
- if (lineMatch) {
1959
- const line = Number.parseInt(lineMatch[1] ?? "", 10);
1960
- if (Number.isFinite(line)) parsed.detailLine = line;
1961
- }
1962
- if (/unsupported heading depth/i.test(trimmed)) parsed.detailRule = "unsupported_heading_depth";
1963
- return parsed;
1808
+ /** True when one primary-email user's feed archived this root after the cutoff. */
1809
+ function conversationArchivedForEmail(email, archivedAfter) {
1810
+ const archive = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorUsers.primaryEmailNormalized, email), isNotNull(juniorConversationParticipants.archivedAt), archivedAfter ? gte(juniorConversationParticipants.archivedAt, archivedAfter) : void 0));
1811
+ return exists(archive);
1964
1812
  }
1965
- /**
1966
- * Ambient destination-installation token for Slack outbound calls, bound by
1967
- * installation-scoped entry points so writes carry the destination
1968
- * workspace's credentials instead of the process-global env token.
1969
- */
1970
- const installationTokenStorage = new AsyncLocalStorage();
1971
- const clientsByToken = /* @__PURE__ */ new Map();
1972
- /**
1973
- * Bind a workspace installation token for all Slack Web API calls inside
1974
- * `fn`, so multi-workspace outbound writes use the destination workspace's
1975
- * credentials rather than the env fallback token.
1976
- */
1977
- function runWithSlackInstallationToken(token, fn) {
1978
- const trimmed = token.trim();
1979
- if (!trimmed) throw new SlackActionError("Slack installation token binding requires a non-empty token", "missing_token");
1980
- return installationTokenStorage.run({ token: trimmed }, fn);
1813
+ /** True when one primary-email user's personal feed has not archived this root. */
1814
+ function conversationNotArchivedForEmail(email) {
1815
+ const archive = getDb().select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorConversationParticipants.rootConversationId, juniorConversations.conversationId), eq(juniorUsers.primaryEmailNormalized, email), isNotNull(juniorConversationParticipants.archivedAt)));
1816
+ return notExists(archive);
1981
1817
  }
1982
- /**
1983
- * Token precedence: ambient installation token, then env token, then a hard
1984
- * failure for team-scoped calls — a call scoped to one workspace must never
1985
- * silently fall through to another workspace's credentials.
1986
- */
1987
- function resolveSlackToken() {
1988
- const ambientToken = installationTokenStorage.getStore()?.token;
1989
- if (ambientToken) return ambientToken;
1990
- const envToken = getSlackBotToken();
1991
- if (envToken) return envToken;
1992
- const teamId = getWorkspaceTeamId();
1993
- 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");
1994
- throw new SlackActionError("SLACK_BOT_TOKEN (or SLACK_BOT_USER_TOKEN) is required for Slack Web API actions in this service", "missing_token");
1818
+ /** Root-actor ownership or a materialized participant row for the same person. */
1819
+ function viewerConversationMembership(args) {
1820
+ if (args.actorMatch && args.participantMatch) return or(args.actorMatch, args.participantMatch);
1821
+ return args.actorMatch ?? args.participantMatch;
1995
1822
  }
1996
- /** Normalize Junior Slack references to native Slack conversation IDs. */
1997
- function normalizeSlackConversationId(channelId) {
1998
- return parseSlackChannelReferenceId(channelId);
1823
+ /** Read personal archive timestamps for a bounded set of conversation roots. */
1824
+ async function conversationArchiveTimes(db, conversationIds, subject) {
1825
+ if (!subject || conversationIds.length === 0) return /* @__PURE__ */ new Map();
1826
+ const query = db.select({
1827
+ archivedAt: juniorConversationParticipants.archivedAt,
1828
+ rootConversationId: juniorConversationParticipants.rootConversationId
1829
+ }).from(juniorConversationParticipants);
1830
+ const rows = "userId" in subject ? await query.where(and(eq(juniorConversationParticipants.userId, subject.userId), inArray(juniorConversationParticipants.rootConversationId, [...conversationIds]), isNotNull(juniorConversationParticipants.archivedAt))) : await query.innerJoin(juniorUsers, eq(juniorUsers.id, juniorConversationParticipants.userId)).where(and(eq(juniorUsers.primaryEmailNormalized, subject.email), inArray(juniorConversationParticipants.rootConversationId, [...conversationIds]), isNotNull(juniorConversationParticipants.archivedAt)));
1831
+ return new Map(rows.flatMap((row) => row.archivedAt ? [[row.rootConversationId, row.archivedAt.getTime()]] : []));
1999
1832
  }
2000
- /**
2001
- * Return the per-token cached WebClient. `withSlackRetries` owns retry
2002
- * classification for this boundary: the WebClient's built-in policy would
2003
- * re-post timed-out writes (a duplicate-message hazard) and sleep for the
2004
- * full unbounded Retry-After inside the request, so both internal behaviors
2005
- * are disabled here.
2006
- */
2007
- function getClient() {
2008
- const token = resolveSlackToken();
2009
- let cached = clientsByToken.get(token);
2010
- if (!cached) {
2011
- cached = new WebClient(token, {
2012
- retryConfig: { retries: 0 },
2013
- rejectRateLimitedCalls: true
2014
- });
2015
- clientsByToken.set(token, cached);
2016
- }
2017
- return cached;
1833
+ /** Find which of the supplied roots include the linked viewer as a participant. */
1834
+ async function conversationIdsWithParticipantUser(db, conversationIds, userId) {
1835
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Set();
1836
+ const rows = await db.select({ rootConversationId: juniorConversationParticipants.rootConversationId }).from(juniorConversationParticipants).where(and(eq(juniorConversationParticipants.userId, userId), inArray(juniorConversationParticipants.rootConversationId, [...conversationIds])));
1837
+ return new Set(rows.map((row) => row.rootConversationId));
2018
1838
  }
2019
- function mapSlackError(error) {
2020
- if (error instanceof SlackActionError) return error;
2021
- const candidate = error;
2022
- const apiError = candidate.data?.error;
2023
- const message = candidate.message ?? "Slack action failed";
2024
- const baseOptions = {
2025
- apiError,
2026
- statusCode: candidate.statusCode,
2027
- requestId: getHeaderString(candidate.headers, "x-slack-req-id"),
2028
- errorData: serializeSlackErrorData(candidate.data),
2029
- ...parseSlackCanvasDetail(candidate.data?.detail)
2030
- };
2031
- if (apiError === "missing_scope") return new SlackActionError(message, "missing_scope", {
2032
- ...baseOptions,
2033
- needed: candidate.data?.needed,
2034
- provided: candidate.data?.provided
2035
- });
2036
- if (apiError === "not_in_channel") return new SlackActionError(message, "not_in_channel", baseOptions);
2037
- if (apiError === "restricted_action_read_only_channel") return new SlackActionError(message, "read_only_channel", baseOptions);
2038
- if (apiError === "already_reacted") return new SlackActionError(message, "already_reacted", baseOptions);
2039
- if (apiError === "no_reaction") return new SlackActionError(message, "no_reaction", baseOptions);
2040
- if (apiError === "invalid_arguments") return new SlackActionError(message, "invalid_arguments", baseOptions);
2041
- if (apiError === "invalid_cursor") return new SlackActionError(message, "invalid_arguments", baseOptions);
2042
- if (apiError === "invalid_name") return new SlackActionError(message, "invalid_arguments", baseOptions);
2043
- if (apiError === "not_found" || apiError === "channel_not_found" || apiError === "message_not_found") return new SlackActionError(message, "not_found", baseOptions);
2044
- if (apiError === "feature_not_enabled" || apiError === "not_allowed_token_type") return new SlackActionError(message, "feature_unavailable", baseOptions);
2045
- if (apiError === "canvas_creation_failed") return new SlackActionError(message, "canvas_creation_failed", baseOptions);
2046
- if (apiError === "canvas_editing_failed") return new SlackActionError(message, "canvas_editing_failed", baseOptions);
2047
- if (candidate.code === "slack_webapi_rate_limited_error" || candidate.statusCode === 429) return new SlackActionError(message, "rate_limited", {
2048
- ...baseOptions,
2049
- retryAfterSeconds: candidate.retryAfter
2050
- });
2051
- return new SlackActionError(message, "internal_error", baseOptions);
1839
+ //#endregion
1840
+ //#region src/api/conversations/access.ts
1841
+ const rootConversation = alias(juniorConversations, "access_root_conversation");
1842
+ const rootDestination = alias(juniorDestinations, "access_root_destination");
1843
+ const rootIdentity = alias(juniorIdentities, "access_root_identity");
1844
+ /** Resolve viewer access for a bounded set of persisted conversations. */
1845
+ async function readConversationAccessFromSql(db, conversationIds, viewer) {
1846
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
1847
+ const normalizedViewerEmail = viewer?.email.trim().toLowerCase() || void 0;
1848
+ const rows = await db.select({
1849
+ conversationId: juniorConversations.conversationId,
1850
+ parentConversationId: juniorConversations.parentConversationId,
1851
+ storedRootConversationId: juniorConversations.rootConversationId,
1852
+ rootConversationId: rootConversation.conversationId,
1853
+ rootParentConversationId: rootConversation.parentConversationId,
1854
+ rootRootConversationId: rootConversation.rootConversationId,
1855
+ visibility: rootDestination.visibility,
1856
+ rootEmailNormalized: rootIdentity.emailNormalized,
1857
+ rootEmailVerified: rootIdentity.emailVerified,
1858
+ rootUserId: rootIdentity.userId
1859
+ }).from(juniorConversations).leftJoin(rootConversation, eq(rootConversation.conversationId, juniorConversations.rootConversationId)).leftJoin(rootDestination, eq(rootDestination.id, rootConversation.destinationId)).leftJoin(rootIdentity, eq(rootIdentity.id, rootConversation.actorIdentityId)).where(inArray(juniorConversations.conversationId, [...conversationIds]));
1860
+ const membershipConversationIds = /* @__PURE__ */ new Set();
1861
+ for (const row of rows) {
1862
+ membershipConversationIds.add(row.conversationId);
1863
+ if (row.storedRootConversationId) membershipConversationIds.add(row.storedRootConversationId);
1864
+ }
1865
+ const participantRootIds = viewer !== void 0 ? await conversationIdsWithParticipantUser(db, [...membershipConversationIds], viewer.id) : /* @__PURE__ */ new Set();
1866
+ return new Map(rows.map((row) => {
1867
+ const validRootConversationId = row.rootConversationId !== null && row.rootConversationId === row.storedRootConversationId && row.rootRootConversationId === row.rootConversationId && row.rootParentConversationId === null && (row.parentConversationId !== null || row.storedRootConversationId === row.conversationId) ? row.rootConversationId ?? void 0 : void 0;
1868
+ const visibility = validRootConversationId === void 0 ? null : row.visibility;
1869
+ const isRootActor = validRootConversationId !== void 0 && viewer !== void 0 && (row.rootUserId === viewer.id || normalizedViewerEmail !== void 0 && row.rootEmailVerified === true && row.rootEmailNormalized === normalizedViewerEmail);
1870
+ const isMaterializedParticipant = participantRootIds.has(row.conversationId) || validRootConversationId !== void 0 && participantRootIds.has(validRootConversationId);
1871
+ const isParticipant = isRootActor || isMaterializedParticipant;
1872
+ const canViewPrivateContent = isParticipant || validRootConversationId !== void 0 && canExposeConversationPayload({ visibility: visibility ?? void 0 });
1873
+ return [row.conversationId, {
1874
+ canViewPrivateContent,
1875
+ isParticipant,
1876
+ visibility
1877
+ }];
1878
+ }));
2052
1879
  }
2053
- const MAX_RETRY_DELAY_MS = 1e4;
2054
- const MAX_TOTAL_RETRY_DELAY_MS = 2e4;
2055
- const CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
2056
- "ECONNREFUSED",
2057
- "ECONNRESET",
2058
- "ENOTFOUND",
2059
- "EAI_AGAIN"
2060
- ]);
2061
- const TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
2062
- "ETIMEDOUT",
2063
- "ECONNABORTED",
2064
- "ESOCKETTIMEDOUT"
2065
- ]);
2066
- function findNetworkErrorCode(error, depth = 0) {
2067
- if (!error || typeof error !== "object" || depth > 4) return;
2068
- const candidate = error;
2069
- if (typeof candidate.code === "string" && (CONNECTION_ERROR_CODES.has(candidate.code) || TIMEOUT_ERROR_CODES.has(candidate.code))) return candidate.code;
2070
- return findNetworkErrorCode(candidate.original, depth + 1) ?? findNetworkErrorCode(candidate.cause, depth + 1);
1880
+ //#endregion
1881
+ //#region src/chat/plugins/conversation-event-reader.ts
1882
+ function registeredEventName$1(plugin, eventName) {
1883
+ if (!plugin.conversationEvents?.some((definition) => definition.eventName === eventName)) throw new TypeError(`Plugin "${plugin.manifest.name}" did not register event "${eventName}"`);
1884
+ return eventName;
2071
1885
  }
2072
- function hasSocketHangUpMessage(error) {
2073
- return error instanceof Error && error.message.toLowerCase().includes("socket hang up");
1886
+ /** Create Conversation event reads bound to one plugin namespace. */
1887
+ function createPluginConversationEventReader(plugin) {
1888
+ return { async list(input) {
1889
+ const eventName = registeredEventName$1(plugin, input.eventName);
1890
+ const db = getDb();
1891
+ if (!(await readConversationAccessFromSql(db, [input.conversationId], input.viewer)).get(input.conversationId)?.canViewPrivateContent) return void 0;
1892
+ return (await db.select({
1893
+ content: sql`${juniorConversationEvents.payload}->'content'`,
1894
+ createdAt: juniorConversationEvents.createdAt,
1895
+ version: sql`(${juniorConversationEvents.payload}->>'version')::integer`
1896
+ }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.conversationId, input.conversationId), eq(juniorConversationEvents.type, "structured_event"), sql`${juniorConversationEvents.payload}->>'namespace' = ${plugin.manifest.name}`, sql`${juniorConversationEvents.payload}->>'name' = ${eventName}`)).orderBy(desc(juniorConversationEvents.seq))).map((row) => ({
1897
+ content: row.content,
1898
+ createdAt: row.createdAt.toISOString(),
1899
+ version: row.version
1900
+ }));
1901
+ } };
2074
1902
  }
2075
- /**
2076
- * Classify a failed Slack call for retry: rate limits, Slack 5xx, and
2077
- * connection-phase failures are always retryable; timeouts are retried only
2078
- * for idempotent operations because Slack may already have accepted the
2079
- * request.
2080
- */
2081
- function classifySlackRetry(raw, mapped) {
2082
- if (mapped.code === "rate_limited") return "rate_limited";
2083
- if (mapped.statusCode !== void 0 && mapped.statusCode >= 500) return "server_error";
2084
- const networkCode = findNetworkErrorCode(raw);
2085
- if (networkCode && CONNECTION_ERROR_CODES.has(networkCode)) return "connection";
2086
- if (networkCode && TIMEOUT_ERROR_CODES.has(networkCode)) return "timeout";
2087
- if (hasSocketHangUpMessage(raw)) return "connection";
2088
- return "none";
1903
+ //#endregion
1904
+ //#region src/chat/plugins/conversation-event-stats.ts
1905
+ const DAY_MS = 864e5;
1906
+ const costRowSchema = z.object({
1907
+ costUsd: z.number().finite().nonnegative(),
1908
+ date: z.string().regex(/^\d{4}-\d{2}-\d{2}(T\d{2})?$/),
1909
+ events: z.number().int().nonnegative()
1910
+ }).strict();
1911
+ function startOfUtcDay(value) {
1912
+ const date = new Date(value);
1913
+ date.setUTCHours(0, 0, 0, 0);
1914
+ return date;
2089
1915
  }
2090
- /**
2091
- * Run a Slack Web API call with bounded retries so transient platform
2092
- * failures do not surface as turn failures, while non-idempotent posts are
2093
- * never re-sent when Slack may already have accepted them.
2094
- *
2095
- * Retry classes: rate limits (bounded Retry-After), connection-phase network
2096
- * failures, and Slack 5xx responses always retry; request timeouts retry only
2097
- * when the caller marks the operation `idempotent`.
2098
- */
2099
- async function withSlackRetries(task, maxAttempts = 3, context = {}) {
2100
- let attempt = 0;
2101
- let totalDelayMs = 0;
2102
- const action = context.action ?? "unknown";
2103
- while (attempt < maxAttempts) {
2104
- attempt += 1;
2105
- const attemptNumber = attempt;
2106
- try {
2107
- return await withSpan(`POST slack.com/api/${action}`, "http.client", {}, async () => {
2108
- try {
2109
- return await task();
2110
- } catch (error) {
2111
- const mapped = mapSlackError(error);
2112
- const errorAttrs = { "error.type": mapped.code };
2113
- if (mapped.apiError) errorAttrs["app.slack.api_error_code"] = mapped.apiError;
2114
- if (mapped.code === "rate_limited") {
2115
- errorAttrs["http.response.status_code"] = 429;
2116
- if (mapped.retryAfterSeconds) errorAttrs["app.slack.retry_after_ms"] = mapped.retryAfterSeconds * 1e3;
2117
- } else if (mapped.statusCode != null) errorAttrs["http.response.status_code"] = mapped.statusCode;
2118
- setSpanAttributes(errorAttrs);
2119
- setSpanStatus("error");
2120
- throw error;
2121
- }
2122
- }, {
2123
- "http.request.method": "POST",
2124
- "server.address": "slack.com",
2125
- "url.scheme": "https",
2126
- "url.path": `/api/${action}`,
2127
- "app.slack.method": action,
2128
- "app.retry.max_attempts": maxAttempts,
2129
- ...attemptNumber > 1 ? { "http.request.resend_count": attemptNumber - 1 } : void 0,
2130
- ...context.attributes ?? {},
2131
- ...context.spanAttributes ?? {}
1916
+ function registeredEventName(plugin, eventName) {
1917
+ if (!plugin.conversationEvents?.some((definition) => definition.eventName === eventName)) throw new TypeError(`Plugin "${plugin.manifest.name}" did not register event "${eventName}"`);
1918
+ return eventName;
1919
+ }
1920
+ /** Create aggregate event-cost reads bound to one plugin's registered namespace. */
1921
+ function createPluginConversationEventStats(plugin, now = Date.now) {
1922
+ return {
1923
+ async costsByHour(input) {
1924
+ const eventName = registeredEventName(plugin, input.eventName);
1925
+ const hourCount = input.hours ?? 24;
1926
+ const end = new Date(now());
1927
+ end.setUTCMinutes(0, 0, 0);
1928
+ const start = /* @__PURE__ */ new Date(end.getTime() - (hourCount - 1) * 60 * 60 * 1e3);
1929
+ const endExclusive = new Date(end.getTime() + 36e5);
1930
+ const date = sql`TO_CHAR(
1931
+ ${juniorConversationEvents.createdAt} AT TIME ZONE 'UTC',
1932
+ 'YYYY-MM-DD"T"HH24'
1933
+ )`;
1934
+ const cost = sql`CASE
1935
+ WHEN jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
1936
+ THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::double precision
1937
+ ELSE 0
1938
+ END`;
1939
+ const rows = await getDb().select({
1940
+ costUsd: sql`SUM(${cost})::double precision`.mapWith(Number),
1941
+ date,
1942
+ events: sql`COUNT(*)::integer`.mapWith(Number)
1943
+ }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.type, "structured_event"), sql`${juniorConversationEvents.payload}->>'namespace' = ${plugin.manifest.name}`, sql`${juniorConversationEvents.payload}->>'name' = ${eventName}`, gte(juniorConversationEvents.createdAt, start), lt(juniorConversationEvents.createdAt, endExclusive))).groupBy(date);
1944
+ const byHour = new Map(z.array(costRowSchema).parse(rows).map((row) => [row.date, row]));
1945
+ return Array.from({ length: hourCount }, (_, index) => {
1946
+ const key = new Date(start.getTime() + index * 60 * 60 * 1e3).toISOString().slice(0, 13);
1947
+ return byHour.get(key) ?? {
1948
+ costUsd: 0,
1949
+ date: key,
1950
+ events: 0
1951
+ };
2132
1952
  });
2133
- } catch (error) {
2134
- const mapped = mapSlackError(error);
2135
- const retryClass = classifySlackRetry(error, mapped);
2136
- const isRetryable = retryClass === "rate_limited" || retryClass === "connection" || retryClass === "server_error" || retryClass === "timeout" && context.idempotent === true;
2137
- const remainingDelayBudgetMs = MAX_TOTAL_RETRY_DELAY_MS - totalDelayMs;
2138
- const baseLogAttributes = {
2139
- "app.slack.action": action,
2140
- "app.slack.error_code": mapped.code,
2141
- ...mapped.apiError ? { "app.slack.api_error": mapped.apiError } : void 0,
2142
- ...mapped.detail ? { "app.slack.detail": mapped.detail } : void 0,
2143
- ...mapped.detailLine !== void 0 ? { "app.slack.detail_line": mapped.detailLine } : void 0,
2144
- ...mapped.detailRule ? { "app.slack.detail_rule": mapped.detailRule } : void 0,
2145
- ...mapped.requestId ? { "app.slack.request_id": mapped.requestId } : void 0,
2146
- ...mapped.statusCode !== void 0 ? { "http.response.status_code": mapped.statusCode } : void 0,
2147
- ...context.attributes ?? {}
2148
- };
2149
- if (!isRetryable || attempt >= maxAttempts || remainingDelayBudgetMs <= 0) {
2150
- logWarn("slack.action.failed", {
2151
- ...baseLogAttributes,
2152
- ...mapped.errorData ? { "app.slack.error_data": mapped.errorData } : void 0
2153
- });
2154
- throw mapped;
1953
+ },
1954
+ async costsByDay(input) {
1955
+ const eventName = registeredEventName(plugin, input.eventName);
1956
+ const end = startOfUtcDay(now());
1957
+ const start = /* @__PURE__ */ new Date(end.getTime() - (input.days - 1) * DAY_MS);
1958
+ const endExclusive = new Date(end.getTime() + DAY_MS);
1959
+ const date = sql`TO_CHAR(
1960
+ ${juniorConversationEvents.createdAt} AT TIME ZONE 'UTC',
1961
+ 'YYYY-MM-DD'
1962
+ )`;
1963
+ const cost = sql`CASE
1964
+ WHEN jsonb_typeof(${juniorConversationEvents.payload}->'content'->'costUsd') = 'number'
1965
+ THEN (${juniorConversationEvents.payload}->'content'->>'costUsd')::double precision
1966
+ ELSE 0
1967
+ END`;
1968
+ const rows = await getDb().select({
1969
+ costUsd: sql`SUM(${cost})::double precision`.mapWith(Number),
1970
+ date,
1971
+ events: sql`COUNT(*)::integer`.mapWith(Number)
1972
+ }).from(juniorConversationEvents).where(and(eq(juniorConversationEvents.type, "structured_event"), sql`${juniorConversationEvents.payload}->>'namespace' = ${plugin.manifest.name}`, sql`${juniorConversationEvents.payload}->>'name' = ${eventName}`, gte(juniorConversationEvents.createdAt, start), lt(juniorConversationEvents.createdAt, endExclusive))).groupBy(date);
1973
+ const byDate = new Map(z.array(costRowSchema).parse(rows).map((row) => [row.date, row]));
1974
+ return Array.from({ length: input.days }, (_, index) => {
1975
+ const date = new Date(start.getTime() + index * DAY_MS).toISOString().slice(0, 10);
1976
+ return byDate.get(date) ?? {
1977
+ costUsd: 0,
1978
+ date,
1979
+ events: 0
1980
+ };
1981
+ });
1982
+ }
1983
+ };
1984
+ }
1985
+ //#endregion
1986
+ //#region src/chat/plugins/logging.ts
1987
+ /** Create the host logger exposed to plugin hooks. */
1988
+ function createPluginLogger(plugin) {
1989
+ return {
1990
+ info(message, metadata) {
1991
+ logInfo("plugin.log.info", {
1992
+ "app.log.message": message,
1993
+ "app.plugin.name": plugin,
1994
+ ...metadata
1995
+ });
1996
+ },
1997
+ warn(message, metadata) {
1998
+ logWarn("plugin.log.warn", {
1999
+ "app.log.message": message,
2000
+ "app.plugin.name": plugin,
2001
+ ...metadata
2002
+ });
2003
+ },
2004
+ error(message, metadata) {
2005
+ logException(new Error(message), "plugin.log.error", {
2006
+ "app.plugin.name": plugin,
2007
+ ...metadata
2008
+ });
2009
+ }
2010
+ };
2011
+ }
2012
+ //#endregion
2013
+ //#region src/chat/plugins/model.ts
2014
+ /** Create the host-owned structured model capability exposed to plugins. */
2015
+ function createPluginModel(pluginName, options = {}, runtime = {}) {
2016
+ return { async completeObject(input) {
2017
+ const modelId = options.structuredModelId ?? (options.structuredModel === "default" ? defaultModelId(botConfig) : botConfig.fastModelId);
2018
+ const result = await completeObject({
2019
+ modelId,
2020
+ schema: input.schema,
2021
+ prompt: input.prompt,
2022
+ ...input.system !== void 0 ? { system: input.system } : void 0,
2023
+ ...input.maxTokens !== void 0 ? { maxTokens: input.maxTokens } : void 0,
2024
+ signal: runtime.signal,
2025
+ promptName: `${pluginName}.structured_completion`,
2026
+ metadata: {
2027
+ pluginName,
2028
+ pluginModelRole: "structured"
2029
+ }
2030
+ });
2031
+ return {
2032
+ object: result.object,
2033
+ ...result.costUsd !== void 0 ? { costUsd: result.costUsd } : void 0
2034
+ };
2035
+ } };
2036
+ }
2037
+ /** Create the host-owned embedding capability exposed to prompt hooks. */
2038
+ function createPluginEmbedder(pluginName, runtime = {}) {
2039
+ return { async embedTexts(input) {
2040
+ return await embedTexts({
2041
+ modelId: botConfig.embeddingModelId,
2042
+ texts: input.texts,
2043
+ signal: runtime.signal,
2044
+ metadata: {
2045
+ pluginName,
2046
+ pluginModelRole: "embedding"
2047
+ }
2048
+ });
2049
+ } };
2050
+ }
2051
+ //#endregion
2052
+ //#region src/chat/plugins/state.ts
2053
+ const MAX_PLUGIN_STATE_KEY_LENGTH = 512;
2054
+ function hashKeyPart(value) {
2055
+ return createHash("sha256").update(value).digest("hex").slice(0, 32);
2056
+ }
2057
+ function pluginStateKey(plugin, key) {
2058
+ const pluginPrefix = `junior:${plugin}`;
2059
+ if (key === pluginPrefix || key.startsWith(`${pluginPrefix}:`)) return key;
2060
+ return `junior:plugin_state:${hashKeyPart(plugin)}:${hashKeyPart(key)}`;
2061
+ }
2062
+ function validatePluginStateKey(key) {
2063
+ if (!key.trim()) throw new Error("Plugin state key is required");
2064
+ if (key.length > MAX_PLUGIN_STATE_KEY_LENGTH) throw new Error("Plugin state key exceeds the maximum length");
2065
+ }
2066
+ /** Create a durable state namespace scoped to one plugin. */
2067
+ function createPluginState(plugin, adapter) {
2068
+ const getAdapter = () => adapter ?? getStateAdapter();
2069
+ return {
2070
+ async delete(key) {
2071
+ validatePluginStateKey(key);
2072
+ const state = getAdapter();
2073
+ await state.connect();
2074
+ await state.delete(pluginStateKey(plugin, key));
2075
+ },
2076
+ async get(key) {
2077
+ validatePluginStateKey(key);
2078
+ const state = getAdapter();
2079
+ await state.connect();
2080
+ return await state.get(pluginStateKey(plugin, key)) ?? void 0;
2081
+ },
2082
+ async set(key, value, ttlMs) {
2083
+ validatePluginStateKey(key);
2084
+ const state = getAdapter();
2085
+ await state.connect();
2086
+ await state.set(pluginStateKey(plugin, key), value, ttlMs);
2087
+ },
2088
+ async setIfNotExists(key, value, ttlMs) {
2089
+ validatePluginStateKey(key);
2090
+ const state = getAdapter();
2091
+ await state.connect();
2092
+ return await state.setIfNotExists(pluginStateKey(plugin, key), value, ttlMs);
2093
+ },
2094
+ async withLock(key, ttlMs, callback) {
2095
+ validatePluginStateKey(key);
2096
+ const state = getAdapter();
2097
+ await state.connect();
2098
+ const lockKey = pluginStateKey(plugin, key);
2099
+ const lock = await state.acquireLock(lockKey, ttlMs);
2100
+ if (!lock) throw new Error(`Could not acquire plugin state lock for ${key}`);
2101
+ try {
2102
+ return await callback();
2103
+ } finally {
2104
+ await state.releaseLock(lock);
2155
2105
  }
2156
- logWarn("slack.action.retrying", {
2157
- ...baseLogAttributes,
2158
- "app.slack.retry_attempt": attempt,
2159
- "app.slack.retry_class": retryClass
2160
- });
2161
- const retryAfterMs = retryClass === "rate_limited" && mapped.retryAfterSeconds && mapped.retryAfterSeconds > 0 ? mapped.retryAfterSeconds * 1e3 : void 0;
2162
- const backoffMs = Math.min(2e3, 250 * 2 ** (attempt - 1));
2163
- const delayMs = Math.min(retryAfterMs ?? backoffMs, MAX_RETRY_DELAY_MS, remainingDelayBudgetMs);
2164
- totalDelayMs += delayMs;
2165
- await sleep$1(delayMs);
2166
2106
  }
2167
- }
2168
- throw new SlackActionError("Slack action exhausted retries", "internal_error");
2107
+ };
2169
2108
  }
2170
- /**
2171
- * Slack Web API client for the current destination workspace: the ambient
2172
- * installation token when one is bound, otherwise the env bot token for
2173
- * single-workspace deployments. Fails instead of falling back when the call
2174
- * is workspace-scoped and no installation token can be resolved, so a write
2175
- * never goes out with another workspace's credentials.
2176
- */
2177
- function getSlackClient(options) {
2178
- if (options) return new WebClient(resolveSlackToken(), {
2179
- retryConfig: { retries: 0 },
2180
- rejectRateLimitedCalls: true,
2181
- timeout: options.timeoutMs
2182
- });
2183
- return getClient();
2109
+ //#endregion
2110
+ //#region src/chat/dashboard-link.ts
2111
+ let dashboardConversationLinkOptions;
2112
+ function withHttps(host) {
2113
+ return /^https?:\/\//.test(host) ? host : `https://${host}`;
2184
2114
  }
2185
- /**
2186
- * Slack channel ID prefixes:
2187
- * - C: channel — modern private channels also use C, so the prefix never
2188
- * proves a channel is public
2189
- * - G: legacy private channel / group DM
2190
- * - D: direct message (1:1)
2191
- */
2192
- function isDmChannel(channelId) {
2193
- const normalized = normalizeSlackConversationId(channelId);
2194
- return Boolean(normalized && normalized.startsWith("D"));
2115
+ function stripTrailingSlashes(value) {
2116
+ let end = value.length;
2117
+ while (end > 1 && value.charCodeAt(end - 1) === 47) end -= 1;
2118
+ return end === value.length ? value : value.slice(0, end);
2195
2119
  }
2196
- /**
2197
- * Conversation-scoped Slack contexts backed by a concrete conversation ID.
2198
- * Includes channels/groups/DMs (C/G/D).
2199
- */
2200
- function isConversationScopedChannel(channelId) {
2201
- const normalized = normalizeSlackConversationId(channelId);
2202
- if (!normalized) return false;
2203
- return normalized.startsWith("C") || normalized.startsWith("G") || normalized.startsWith("D");
2120
+ function normalizeDashboardPath(path, fallback) {
2121
+ const value = path?.trim() || fallback;
2122
+ return stripTrailingSlashes(value.startsWith("/") ? value : `/${value}`);
2204
2123
  }
2205
- function isConversationChannel(channelId) {
2206
- const normalized = normalizeSlackConversationId(channelId);
2207
- if (!normalized) return false;
2208
- return normalized.startsWith("C") || normalized.startsWith("G");
2124
+ function resolveDashboardBaseURL(config) {
2125
+ const explicit = config.baseURL ?? process.env.JUNIOR_BASE_URL;
2126
+ if (explicit?.trim()) return stripTrailingSlashes(withHttps(explicit.trim()));
2127
+ const vercelProd = process.env.VERCEL_PROJECT_PRODUCTION_URL?.trim();
2128
+ if (vercelProd) return stripTrailingSlashes(withHttps(vercelProd));
2129
+ const vercelUrl = process.env.VERCEL_URL?.trim();
2130
+ if (vercelUrl) return stripTrailingSlashes(withHttps(vercelUrl));
2131
+ return "http://localhost:3000";
2209
2132
  }
2210
- async function getFilePermalink(fileId) {
2211
- const client = getClient();
2212
- return (await withSlackRetries(() => client.files.info({ file: fileId }), 3, {
2213
- action: "files.info",
2214
- idempotent: true,
2215
- spanAttributes: { "app.slack.file_id": fileId }
2216
- })).file?.permalink;
2133
+ /** Configure core dashboard links. */
2134
+ function setDashboardConversationLinkOptions(options) {
2135
+ const previous = dashboardConversationLinkOptions;
2136
+ dashboardConversationLinkOptions = options?.disabled ? void 0 : options;
2137
+ return previous;
2217
2138
  }
2218
- async function downloadPrivateSlackFile(url) {
2219
- const token = resolveSlackToken();
2220
- return withSpan("GET files.slack.com", "http.client", {}, async () => {
2221
- const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
2222
- setSpanAttributes({ "http.response.status_code": response.status });
2223
- if (!response.ok) {
2224
- setSpanAttributes({ "error.type": String(response.status) });
2225
- setSpanStatus("error");
2226
- throw new Error(`Slack file download failed: ${response.status}`);
2139
+ function resolveDashboardPath(segmentPath) {
2140
+ if (!dashboardConversationLinkOptions) return;
2141
+ const baseURL = resolveDashboardBaseURL(dashboardConversationLinkOptions);
2142
+ const basePath = normalizeDashboardPath(dashboardConversationLinkOptions.basePath, "/");
2143
+ return `${baseURL}${basePath === "/" ? segmentPath : `${basePath}${segmentPath}`}`;
2144
+ }
2145
+ /** Build the dashboard conversation URL when the core dashboard is enabled. */
2146
+ function getDashboardConversationLink(conversationId) {
2147
+ return resolveDashboardPath(`/conversations/${encodeURIComponent(conversationId)}`);
2148
+ }
2149
+ /** Build the dashboard task detail URL when the core dashboard is enabled. */
2150
+ function getDashboardTaskLink(taskId) {
2151
+ return resolveDashboardPath(`/automations/${encodeURIComponent(taskId)}`);
2152
+ }
2153
+ /** Public icon asset URL. Headless and local installs keep text-only identity. */
2154
+ function getDashboardObjectIconLink(icon) {
2155
+ if (!dashboardConversationLinkOptions) return void 0;
2156
+ const url = new URL(objectIconPath(icon), resolveDashboardBaseURL(dashboardConversationLinkOptions));
2157
+ if (url.protocol !== "https:" || url.hostname === "localhost") return void 0;
2158
+ return url.href;
2159
+ }
2160
+ //#endregion
2161
+ //#region src/chat/state/ttl.ts
2162
+ /** Retain expiring conversation/runtime state long enough for resume and dashboard history. */
2163
+ const JUNIOR_THREAD_STATE_TTL_MS = 6048e5;
2164
+ //#endregion
2165
+ //#region src/chat/events/timer-index.ts
2166
+ const DUE_KEY = "junior:watches:due";
2167
+ const CLAIM_MS = 12e4;
2168
+ const BATCH_SIZE = 25;
2169
+ const entriesSchema = z.array(z.object({
2170
+ id: z.string(),
2171
+ atMs: z.number()
2172
+ }));
2173
+ /** Keep timer index operations atomic in Redis and the local memory adapter. */
2174
+ async function updateTimerIndex(state, operation) {
2175
+ const redis = await getDefaultRedisStateAdapterFor(state);
2176
+ if (redis) {
2177
+ const prefix = getChatConfig().state.keyPrefix;
2178
+ const key = prefix ? `${prefix}:${DUE_KEY}` : DUE_KEY;
2179
+ const client = redis.getClient();
2180
+ if (operation.kind === "add") {
2181
+ await client.sendCommand([
2182
+ "ZADD",
2183
+ key,
2184
+ String(operation.atMs),
2185
+ operation.id
2186
+ ]);
2187
+ return [];
2227
2188
  }
2228
- return Buffer.from(await response.arrayBuffer());
2229
- }, {
2230
- "http.request.method": "GET",
2231
- "server.address": "files.slack.com",
2232
- "url.scheme": "https",
2233
- "app.slack.method": "files.download"
2234
- });
2189
+ if (operation.kind === "remove") {
2190
+ await client.sendCommand([
2191
+ "ZREM",
2192
+ key,
2193
+ operation.id
2194
+ ]);
2195
+ return [];
2196
+ }
2197
+ return await client.sendCommand([
2198
+ "EVAL",
2199
+ `local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, ARGV[3])
2200
+ for _, id in ipairs(ids) do
2201
+ redis.call('ZADD', KEYS[1], ARGV[2], id)
2202
+ end
2203
+ return ids`,
2204
+ "1",
2205
+ key,
2206
+ String(operation.nowMs),
2207
+ String(operation.nowMs + CLAIM_MS),
2208
+ String(BATCH_SIZE)
2209
+ ]);
2210
+ }
2211
+ const lock = await state.acquireLock(`${DUE_KEY}:lock`, 1e4);
2212
+ if (!lock) throw new Error("Could not acquire timer index lock");
2213
+ try {
2214
+ let entries = entriesSchema.parse(await state.get(DUE_KEY) ?? []);
2215
+ let ids = [];
2216
+ if (operation.kind === "claim") {
2217
+ ids = entries.filter((entry) => entry.atMs <= operation.nowMs).sort((a, b) => a.atMs - b.atMs).slice(0, BATCH_SIZE).map((entry) => entry.id);
2218
+ const claimed = new Set(ids);
2219
+ entries = entries.map((entry) => claimed.has(entry.id) ? {
2220
+ ...entry,
2221
+ atMs: operation.nowMs + CLAIM_MS
2222
+ } : entry);
2223
+ } else {
2224
+ entries = entries.filter((entry) => entry.id !== operation.id);
2225
+ if (operation.kind === "add") entries.push({
2226
+ id: operation.id,
2227
+ atMs: operation.atMs
2228
+ });
2229
+ }
2230
+ await state.set(DUE_KEY, entries);
2231
+ return ids;
2232
+ } finally {
2233
+ await state.releaseLock(lock);
2234
+ }
2235
2235
  }
2236
2236
  //#endregion
2237
2237
  //#region src/chat/credentials/subject.ts
@@ -4310,4 +4310,4 @@ function createPluginHookRunner(input = {}) {
4310
4310
  };
4311
4311
  }
4312
4312
  //#endregion
4313
- export { verifySlackDirectCredentialSubject as $, getWorkspaceSnapshotBuilder as A, viewerConversationMembership as At, cancelWatch as B, saveObjectAnnotations as Bt, WorkspaceValidationError as C, conversationArchiveTimes as Ct, setWorkspaceSnapshotBuild as D, conversationHasParticipantUser as Dt, setWorkspaceSnapshot as E, conversationHasParticipantEmail as Et, RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS as F, getSlackFileId as Ft, findMatchingWatches as G, resolveUserTimezone as Gt, createTimerWatch as H, listConversationAnnotations as Ht, RESOURCE_SUBSCRIPTION_MAX_TTL_MS as I, matchSlackAttachments as It, bindEventAutomationCredentialSubject as J, agentsInstructionsUpdatedEvent as Jt, listWatches as K, lookupSlackUser as Kt, RESOURCE_WATCH_TOOL_SOURCE as L, readAttachmentVisionSummary as Lt, workspaceRepoCheckoutPath as M, readLatestConversationBrief as Mt, resolveChannelCapabilities as N, readConversationEventPage as Nt, SANDBOX_RUNTIME as O, conversationNotArchivedForEmail as Ot, getSlackToolContext as P, buildSentryConversationUrl as Pt, verifyScheduledAutomationCredentialSubject as Q, STOP_WATCHING_TOOL_NAME as R, recordSlackAttachment as Rt, updateWorkspace as S, readConversationAccessFromSql as St, loadSnapshotsForProfile as T, conversationArchivedForUser as Tt, createWatch as U, listConversationAnnotationsById as Ut, collectEventMatchKeys as V, createPluginAnnotations as Vt, deliverWatch as W, defaultTimezone as Wt, bindSlackDirectCredentialSubject as X, authenticationUnlinkedEvent as Xt, bindScheduledAutomationCredentialSubject as Y, authenticationLinkedEvent as Yt, verifyEventAutomationCredentialSubject as Z, deleteWorkspace as _, setDashboardConversationLinkOptions as _t, getPluginRoutes as a, getSlackClient as at, listWorkspaces as b, createPluginModel as bt, getPluginTools as c, normalizeSlackConversationId as ct, setPlugins as d, getWorkspaceTeamId as dt, verifyTimerWatchCredentialSubject as et, validatePlugins as f, runWithWorkspaceTeamId as ft, createWorkspace as g, getDashboardTaskLink as gt, setBriefsConfig as h, getDashboardObjectIconLink as ht, getPluginApiRoutes as i, getHeaderString as it, tryWorkspaceRepoCheckoutPath as j, createPluginConversationEvents as jt, deleteWorkspaceSnapshotBuilders as k, conversationNotArchivedForUser as kt, getPluginUserPromptContributions as l, runWithSlackInstallationToken as lt, registration_exports as m, getDashboardConversationLink as mt, applyPluginFormatMarkdown as n, downloadPrivateSlackFile as nt, getPluginSlackConversationLink as o, isConversationScopedChannel as ot, coreTaskRegistrations as p, JUNIOR_THREAD_STATE_TTL_MS as pt, takeDueTimerWatches as q, JUNIOR_NATIVE_EVENT_NAMESPACE as qt, createPluginHookRunner as r, getFilePermalink as rt, getPluginSystemPromptContributions as s, isDmChannel as st, agent_hooks_exports as t, SlackActionError as tt, getPlugins as u, withSlackRetries as ut, getWorkspace as v, createPluginState as vt, invalidateReadySnapshot as w, conversationArchivedForEmail as wt, listWorkspacesByRepository as x, createPluginLogger as xt, getWorkspaceByName as y, createPluginEmbedder as yt, cancelSubscriptions as z, resolveRootVisibility as zt };
4313
+ export { verifySlackDirectCredentialSubject as $, getWorkspaceSnapshotBuilder as A, listConversationAnnotations as At, cancelWatch as B, isConversationScopedChannel as Bt, WorkspaceValidationError as C, getSlackFileId as Ct, setWorkspaceSnapshotBuild as D, resolveRootVisibility as Dt, setWorkspaceSnapshot as E, recordSlackAttachment as Et, RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS as F, SlackActionError as Ft, findMatchingWatches as G, getWorkspaceTeamId as Gt, createTimerWatch as H, normalizeSlackConversationId as Ht, RESOURCE_SUBSCRIPTION_MAX_TTL_MS as I, downloadPrivateSlackFile as It, bindEventAutomationCredentialSubject as J, agentsInstructionsUpdatedEvent as Jt, listWatches as K, runWithWorkspaceTeamId as Kt, RESOURCE_WATCH_TOOL_SOURCE as L, getFilePermalink as Lt, workspaceRepoCheckoutPath as M, defaultTimezone as Mt, resolveChannelCapabilities as N, resolveUserTimezone as Nt, SANDBOX_RUNTIME as O, saveObjectAnnotations as Ot, getSlackToolContext as P, lookupSlackUser as Pt, verifyScheduledAutomationCredentialSubject as Q, STOP_WATCHING_TOOL_NAME as R, getHeaderString as Rt, updateWorkspace as S, buildSentryConversationUrl as St, loadSnapshotsForProfile as T, readAttachmentVisionSummary as Tt, createWatch as U, runWithSlackInstallationToken as Ut, collectEventMatchKeys as V, isDmChannel as Vt, deliverWatch as W, withSlackRetries as Wt, bindSlackDirectCredentialSubject as X, authenticationUnlinkedEvent as Xt, bindScheduledAutomationCredentialSubject as Y, authenticationLinkedEvent as Yt, verifyEventAutomationCredentialSubject as Z, deleteWorkspace as _, conversationNotArchivedForUser as _t, getPluginRoutes as a, setDashboardConversationLinkOptions as at, listWorkspaces as b, readLatestConversationBrief as bt, getPluginTools as c, createPluginModel as ct, setPlugins as d, conversationArchiveTimes as dt, verifyTimerWatchCredentialSubject as et, validatePlugins as f, conversationArchivedForEmail as ft, createWorkspace as g, conversationNotArchivedForEmail as gt, setBriefsConfig as h, conversationHasParticipantUser as ht, getPluginApiRoutes as i, getDashboardTaskLink as it, tryWorkspaceRepoCheckoutPath as j, listConversationAnnotationsById as jt, deleteWorkspaceSnapshotBuilders as k, createPluginAnnotations as kt, getPluginUserPromptContributions as l, createPluginLogger as lt, registration_exports as m, conversationHasParticipantEmail as mt, applyPluginFormatMarkdown as n, getDashboardConversationLink as nt, getPluginSlackConversationLink as o, createPluginState as ot, coreTaskRegistrations as p, conversationArchivedForUser as pt, takeDueTimerWatches as q, JUNIOR_NATIVE_EVENT_NAMESPACE as qt, createPluginHookRunner as r, getDashboardObjectIconLink as rt, getPluginSystemPromptContributions as s, createPluginEmbedder as st, agent_hooks_exports as t, JUNIOR_THREAD_STATE_TTL_MS as tt, getPlugins as u, readConversationAccessFromSql as ut, getWorkspace as v, viewerConversationMembership as vt, invalidateReadySnapshot as w, matchSlackAttachments as wt, listWorkspacesByRepository as x, readConversationEventPage as xt, getWorkspaceByName as y, createPluginConversationEvents as yt, cancelSubscriptions as z, getSlackClient as zt };