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