@sentry/junior 0.108.0 → 0.110.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 (109) hide show
  1. package/dist/{agent-hooks-NU5HK3PS.js → agent-hooks-ELT4ODFF.js} +12 -13
  2. package/dist/api/conversations/access.d.ts +9 -0
  3. package/dist/api/conversations/aggregate.d.ts +12 -1
  4. package/dist/api/conversations/archive.d.ts +3 -5
  5. package/dist/api/conversations/detail.d.ts +156 -7
  6. package/dist/api/conversations/events.d.ts +2 -2
  7. package/dist/api/conversations/list.d.ts +57 -6
  8. package/dist/api/conversations/projection.d.ts +6 -7
  9. package/dist/api/conversations/reporting.d.ts +10 -4
  10. package/dist/api/conversations/routes.d.ts +2 -1
  11. package/dist/api/conversations/shared.d.ts +1 -1
  12. package/dist/api/conversations/stats.d.ts +37 -6
  13. package/dist/api/conversations/stats.query.d.ts +1 -1
  14. package/dist/api/conversations/usage.d.ts +9 -0
  15. package/dist/api/http.d.ts +7 -0
  16. package/dist/api/locations/detail.d.ts +102 -6
  17. package/dist/api/locations/list.d.ts +43 -5
  18. package/dist/api/locations/query.d.ts +4 -2
  19. package/dist/api/locations/routes.d.ts +2 -1
  20. package/dist/api/people/list.d.ts +28 -6
  21. package/dist/api/people/list.query.d.ts +1 -1
  22. package/dist/api/people/profile.d.ts +96 -7
  23. package/dist/api/people/profile.query.d.ts +4 -2
  24. package/dist/api/people/routes.d.ts +2 -1
  25. package/dist/api/people/shared.d.ts +1219 -2
  26. package/dist/api/route.d.ts +18 -4
  27. package/dist/api/schema/common.d.ts +6 -0
  28. package/dist/api/{conversations/schema.d.ts → schema/conversation.d.ts} +88 -9
  29. package/dist/api/{locations/schema.d.ts → schema/location.d.ts} +5 -0
  30. package/dist/api/{people/schema.d.ts → schema/person.d.ts} +37 -45
  31. package/dist/api/schema.d.ts +8 -22
  32. package/dist/api/schema.js +10 -2
  33. package/dist/api.d.ts +5 -1
  34. package/dist/api.js +838 -369
  35. package/dist/app.d.ts +1 -0
  36. package/dist/app.js +1564 -486
  37. package/dist/catalog-runtime-4GI22PXK.js +16 -0
  38. package/dist/chat/agent/resume.d.ts +11 -12
  39. package/dist/chat/conversations/history.d.ts +3 -0
  40. package/dist/chat/conversations/sql/privacy.d.ts +3 -15
  41. package/dist/chat/conversations/sql/store.d.ts +0 -7
  42. package/dist/chat/log-context.d.ts +40 -0
  43. package/dist/chat/logging.d.ts +3 -26
  44. package/dist/chat/pi/messages.d.ts +2 -2
  45. package/dist/chat/pi/sql-model-usage.d.ts +4 -1
  46. package/dist/chat/runtime/slack-resume.d.ts +2 -2
  47. package/dist/chat/services/auth-pause.d.ts +4 -0
  48. package/dist/chat/services/context-compaction.d.ts +1 -0
  49. package/dist/chat/services/turn-session-record.d.ts +0 -2
  50. package/dist/chat/slack/reply.d.ts +7 -19
  51. package/dist/chat/state/conversation.d.ts +1 -0
  52. package/dist/chat/task-execution/queue-signing.d.ts +10 -8
  53. package/dist/chat/task-execution/queue.d.ts +9 -7
  54. package/dist/chat/task-execution/slack-work.d.ts +6 -6
  55. package/dist/{chunk-UD6THJ2I.js → chunk-2T7TIEXI.js} +197 -12
  56. package/dist/{chunk-CEA3A3UA.js → chunk-2ZTTXTSZ.js} +24 -1
  57. package/dist/{chunk-XKB7LGIW.js → chunk-3S7YYHBI.js} +3 -139
  58. package/dist/chunk-3TV7OEHL.js +10 -0
  59. package/dist/{chunk-A5CO2EHL.js → chunk-3UBQI73J.js} +11 -15
  60. package/dist/{chunk-7FBGKXPE.js → chunk-42WRQLJ6.js} +10 -33
  61. package/dist/{chunk-TE4QHJH4.js → chunk-4QU44P7Q.js} +135 -595
  62. package/dist/{chunk-RT5BUHR5.js → chunk-6Y5L4BOM.js} +3 -5
  63. package/dist/{chunk-VIXFG2VC.js → chunk-C4MZ3GP5.js} +1 -2
  64. package/dist/{chunk-3RGQLX2F.js → chunk-C5XAFTHY.js} +1011 -29
  65. package/dist/{chunk-PUAMO4PI.js → chunk-FPK7XY3F.js} +139 -99
  66. package/dist/{chunk-H3QYZL7K.js → chunk-GK47NBDE.js} +28 -3
  67. package/dist/{chunk-O24R4DCK.js → chunk-IYKPU7H6.js} +94 -56
  68. package/dist/chunk-JQFJO5WH.js +477 -0
  69. package/dist/{chunk-VH6KWKG2.js → chunk-L5XQPJ7T.js} +3 -3
  70. package/dist/{chunk-2XXEKR44.js → chunk-MIAKQNYX.js} +43 -31
  71. package/dist/{chunk-KPMPQ6AA.js → chunk-RHHRKSPI.js} +249 -223
  72. package/dist/{chunk-YT72TOEK.js → chunk-SBOBTS27.js} +2 -2
  73. package/dist/{chunk-B5I5LMSP.js → chunk-W6RJZG7F.js} +5 -7
  74. package/dist/chunk-W75ZPAGG.js +44 -0
  75. package/dist/{chunk-SS67LUOK.js → chunk-WNOVRQ67.js} +6 -7
  76. package/dist/{chunk-YIPD3L6L.js → chunk-Y3H2DN6F.js} +8 -10
  77. package/dist/{chunk-DY7TRYB5.js → chunk-Y5FK3CNG.js} +1 -1
  78. package/dist/chunk-ZKIOWEFL.js +125 -0
  79. package/dist/cli/chat.js +23 -24
  80. package/dist/cli/check.js +9 -7
  81. package/dist/cli/plugins.js +16 -17
  82. package/dist/cli/snapshot-warmup.js +11 -10
  83. package/dist/cli/upgrade.js +314 -3505
  84. package/dist/db/db.d.ts +1 -1
  85. package/dist/db/schema/conversations.d.ts +17 -0
  86. package/dist/db/schema.d.ts +17 -0
  87. package/dist/{db-AMRBAT5D.js → db-Y6W6CWWW.js} +8 -7
  88. package/dist/instrumentation.js +4 -3
  89. package/dist/nitro.js +4 -4
  90. package/dist/reporting-schema.d.ts +46 -0
  91. package/dist/{runner-TQH5GAJ4.js → runner-BBFV5GLR.js} +17 -21
  92. package/dist/{skills-ITNHBARB.js → skills-AR4AF32L.js} +8 -6
  93. package/dist/{validation-AGQ46ZGV.js → validation-FBIEPTC2.js} +8 -6
  94. package/dist/version.d.ts +2 -0
  95. package/dist/version.js +7 -0
  96. package/migrations/0006_drop_legacy_conversation_messages.sql +1 -0
  97. package/migrations/0007_thick_sugar_man.sql +23 -0
  98. package/migrations/README.md +12 -9
  99. package/migrations/meta/0006_snapshot.json +948 -0
  100. package/migrations/meta/0007_snapshot.json +1013 -0
  101. package/migrations/meta/_journal.json +14 -0
  102. package/package.json +9 -5
  103. package/dist/catalog-runtime-FOFNPR3P.js +0 -14
  104. package/dist/chunk-AHJR2IFF.js +0 -115
  105. package/dist/chunk-B7PUBQQM.js +0 -53
  106. package/dist/chunk-G3E7SCME.js +0 -28
  107. package/dist/chunk-J3B3FPP2.js +0 -1061
  108. package/dist/chunk-OB42YVAE.js +0 -16
  109. package/dist/chunk-TWINAEZQ.js +0 -1283
package/dist/api.js CHANGED
@@ -2,6 +2,9 @@ import {
2
2
  activityDays,
3
3
  actorDirectoryReportSchema,
4
4
  actorProfileReportSchema,
5
+ apiErrorSchema,
6
+ archiveConversationBodySchema,
7
+ archiveConversationResponseSchema,
5
8
  conversationDetailReportSchema,
6
9
  conversationFeedQuerySchema,
7
10
  conversationFeedSchema,
@@ -12,50 +15,51 @@ import {
12
15
  locationDirectoryReportSchema,
13
16
  locationParamsSchema,
14
17
  personParamsSchema
15
- } from "./chunk-PUAMO4PI.js";
18
+ } from "./chunk-FPK7XY3F.js";
16
19
  import {
17
20
  buildSentryConversationUrl,
18
21
  formatSlackConversationRedactedLabel,
19
- readConversationEventPrivacySnapshot,
20
22
  readHealthReport,
21
23
  resolveSlackConversationContextFromThreadId
22
- } from "./chunk-XKB7LGIW.js";
24
+ } from "./chunk-3S7YYHBI.js";
23
25
  import {
26
+ healthReportSchema,
24
27
  pluginOperationalReportFeedSchema,
25
28
  pluginReportsSchema,
26
29
  runtimeInfoReportSchema,
27
30
  skillReportsSchema
28
- } from "./chunk-CEA3A3UA.js";
31
+ } from "./chunk-2ZTTXTSZ.js";
29
32
  import {
30
33
  agentTurnUsageSchema,
31
34
  hasAgentTurnUsage
32
35
  } from "./chunk-MU6HHZEN.js";
33
36
  import "./chunk-PDO5BLNM.js";
34
- import "./chunk-VH6KWKG2.js";
37
+ import "./chunk-L5XQPJ7T.js";
35
38
  import {
36
39
  decodeStoredConversationEvent,
37
40
  getDb,
38
- getSqlExecutor,
41
+ getSqlExecutor
42
+ } from "./chunk-4QU44P7Q.js";
43
+ import {
39
44
  juniorConversationEvents,
40
45
  juniorConversations,
41
46
  juniorIdentities,
42
47
  juniorUsers
43
- } from "./chunk-TE4QHJH4.js";
48
+ } from "./chunk-JQFJO5WH.js";
44
49
  import "./chunk-4ZNGQH7C.js";
45
- import "./chunk-SS67LUOK.js";
50
+ import "./chunk-WNOVRQ67.js";
46
51
  import {
47
52
  juniorDestinations
48
53
  } from "./chunk-VFUK3X5B.js";
49
54
  import {
50
55
  canExposeConversationPayload,
51
- parseSlackThreadId,
52
- resolveConversationPrivacy
53
- } from "./chunk-RT5BUHR5.js";
54
- import "./chunk-B7PUBQQM.js";
55
- import "./chunk-O24R4DCK.js";
56
- import "./chunk-OB42YVAE.js";
56
+ parseSlackThreadId
57
+ } from "./chunk-6Y5L4BOM.js";
58
+ import "./chunk-IYKPU7H6.js";
59
+ import "./chunk-42WRQLJ6.js";
57
60
  import "./chunk-6GWA276C.js";
58
- import "./chunk-7FBGKXPE.js";
61
+ import "./chunk-W75ZPAGG.js";
62
+ import "./chunk-ZKIOWEFL.js";
59
63
  import "./chunk-MLKGABMK.js";
60
64
 
61
65
  // src/api.ts
@@ -64,32 +68,50 @@ import { Hono as Hono4 } from "hono";
64
68
  // src/api/conversations/routes.ts
65
69
  import { Hono } from "hono";
66
70
 
67
- // src/api/conversations/archive.ts
68
- import { and, eq, isNotNull } from "drizzle-orm";
69
- import { HTTPException as HTTPException2 } from "hono/http-exception";
70
- import { z } from "zod";
71
-
72
71
  // src/api/http.ts
73
72
  import { HTTPException } from "hono/http-exception";
73
+ function jsonResponse(schema, value, init) {
74
+ return Response.json(schema.parse(value), init);
75
+ }
76
+ function throwApiError(status, message2, cause) {
77
+ throw new HTTPException(status, {
78
+ ...cause === void 0 ? {} : { cause },
79
+ message: message2,
80
+ res: jsonResponse(apiErrorSchema, { error: message2 }, { status })
81
+ });
82
+ }
74
83
  function parseParams(schema, params) {
75
84
  const result = schema.safeParse(params);
76
85
  if (result.success) return result.data;
77
- throw new HTTPException(400, {
78
- cause: result.error,
79
- message: "Invalid route parameters."
80
- });
86
+ return throwApiError(400, "Invalid route parameters.", result.error);
81
87
  }
82
88
  function parseQuery(schema, query) {
83
89
  const result = schema.safeParse(query);
84
90
  if (result.success) return result.data;
85
- throw new HTTPException(400, {
86
- cause: result.error,
87
- message: "Invalid query parameters."
88
- });
91
+ return throwApiError(400, "Invalid query parameters.", result.error);
92
+ }
93
+ function parseBody(schema, body) {
94
+ const result = schema.safeParse(body);
95
+ if (result.success) return result.data;
96
+ return throwApiError(400, "Invalid request body.", result.error);
97
+ }
98
+
99
+ // src/api/route.ts
100
+ function defineApiRoute(route) {
101
+ return route;
102
+ }
103
+ function registerApiRoutes(app, routes5) {
104
+ for (const route of routes5) {
105
+ app.on(
106
+ route.method,
107
+ route.path,
108
+ async (context) => jsonResponse(route.responseSchema, await route.handler(context))
109
+ );
110
+ }
89
111
  }
90
112
 
91
113
  // src/api/conversations/archive.ts
92
- var archiveBodySchema = z.object({ archived: z.boolean(), lastSeenAt: z.string().datetime() }).strict();
114
+ import { and, eq, isNotNull } from "drizzle-orm";
93
115
  async function archiveIfUnchanged(args) {
94
116
  const db = getDb();
95
117
  const rows = await db.update(juniorConversations).set({ archivedAt: args.archived ? /* @__PURE__ */ new Date() : null }).where(
@@ -107,9 +129,10 @@ async function archiveIfUnchanged(args) {
107
129
  if (!args.archived && existing.archivedAt === null) return "updated";
108
130
  return "conflict";
109
131
  }
110
- var archive_default = {
132
+ var archive_default = defineApiRoute({
111
133
  method: "patch",
112
134
  path: "/:conversationId/archive",
135
+ responseSchema: archiveConversationResponseSchema,
113
136
  handler: async (c) => {
114
137
  const { conversationId } = parseParams(
115
138
  conversationParamsSchema,
@@ -119,35 +142,22 @@ var archive_default = {
119
142
  try {
120
143
  input = await c.req.json();
121
144
  } catch (error) {
122
- throw new HTTPException2(400, {
123
- cause: error,
124
- message: "Invalid request body."
125
- });
145
+ throwApiError(400, "Invalid request body.", error);
126
146
  }
127
- const parsed = archiveBodySchema.safeParse(input);
128
- if (!parsed.success) {
129
- throw new HTTPException2(400, {
130
- cause: parsed.error,
131
- message: "Invalid request body."
132
- });
133
- }
134
- const body = parsed.data;
147
+ const body = parseBody(archiveConversationBodySchema, input);
135
148
  const result = await archiveIfUnchanged({ ...body, conversationId });
136
149
  if (result === "not_found") {
137
- return Response.json(
138
- { error: "Conversation not found." },
139
- { status: 404 }
140
- );
150
+ throwApiError(404, "Conversation not found.");
141
151
  }
142
152
  if (result === "conflict") {
143
- return Response.json(
144
- { error: "Conversation received new activity." },
145
- { status: 409 }
146
- );
153
+ throwApiError(409, "Conversation received new activity.");
147
154
  }
148
- return Response.json({ archived: body.archived });
155
+ return { archived: body.archived };
149
156
  }
150
- };
157
+ });
158
+
159
+ // src/api/conversations/detail.ts
160
+ import { and as and5, asc as asc2, eq as eq6, getTableColumns, inArray as inArray3, sql as sql3 } from "drizzle-orm";
151
161
 
152
162
  // src/chat/pi/sql-model-usage.ts
153
163
  import { and as and2, eq as eq2, sql } from "drizzle-orm";
@@ -216,7 +226,7 @@ function usageFromRow(row) {
216
226
  };
217
227
  return hasAgentTurnUsage(result) ? agentTurnUsageSchema.parse(result) : void 0;
218
228
  }
219
- async function readConversationModelUsageFromSql(executor, conversationId) {
229
+ async function readConversationModelUsageFromSql(executor, options) {
220
230
  const modelId = sql`concat(${message}->>'provider', '/', ${message}->>'model')`;
221
231
  const inputTokens = token("input", "inputTokens");
222
232
  const outputTokens = token("output", "outputTokens");
@@ -242,9 +252,15 @@ async function readConversationModelUsageFromSql(executor, conversationId) {
242
252
  costCacheRead: summedCost(nonnegativeNumber(cost, "cacheRead")),
243
253
  costCacheWrite: summedCost(nonnegativeNumber(cost, "cacheWrite")),
244
254
  costTotal: summedCost(nonnegativeNumber(cost, "total"))
245
- }).from(juniorConversationEvents).where(
255
+ }).from(juniorConversationEvents).innerJoin(
256
+ juniorConversations,
257
+ eq2(
258
+ juniorConversations.conversationId,
259
+ juniorConversationEvents.conversationId
260
+ )
261
+ ).where(
246
262
  and2(
247
- eq2(juniorConversationEvents.conversationId, conversationId),
263
+ options.includeDescendants ? eq2(juniorConversations.rootConversationId, options.conversationId) : eq2(juniorConversationEvents.conversationId, options.conversationId),
248
264
  eq2(juniorConversationEvents.type, "agent_step"),
249
265
  sql`${message}->>'role' = 'assistant'`,
250
266
  sql`jsonb_typeof(${message}->'provider') = 'string'`,
@@ -260,9 +276,11 @@ async function readConversationModelUsageFromSql(executor, conversationId) {
260
276
  }
261
277
 
262
278
  // src/api/conversations/events.ts
279
+ import { z } from "zod";
263
280
  var conversationReportSourceEventTypes = [
264
281
  "message",
265
282
  "message_handled",
283
+ "agent_step",
266
284
  "tool_execution_started",
267
285
  "turn_started",
268
286
  "turn_completed",
@@ -272,6 +290,80 @@ var conversationReportSourceEventTypes = [
272
290
  "subagent_started",
273
291
  "subagent_ended"
274
292
  ];
293
+ var reportingAssistantMessageSchema = z.object({
294
+ role: z.literal("assistant"),
295
+ content: z.array(z.unknown())
296
+ }).passthrough();
297
+ var reportingToolCallPartSchema = z.object({
298
+ type: z.literal("toolCall"),
299
+ id: z.string().min(1),
300
+ name: z.string().min(1),
301
+ arguments: z.unknown()
302
+ }).passthrough();
303
+ var reportingToolResultMessageSchema = z.object({
304
+ role: z.literal("toolResult"),
305
+ toolCallId: z.string().min(1),
306
+ content: z.array(z.unknown()),
307
+ details: z.unknown().optional(),
308
+ isError: z.boolean().optional()
309
+ }).passthrough();
310
+ var reportingToolResultDetailsSchema = z.object({
311
+ ok: z.boolean().optional(),
312
+ status: z.string().optional()
313
+ }).passthrough();
314
+ var reportingTextPartSchema = z.object({ type: z.literal("text"), text: z.string() }).passthrough();
315
+ var reportingMediaPartSchema = z.object({
316
+ type: z.enum(["image", "audio"]),
317
+ mimeType: z.string().min(1).optional()
318
+ }).passthrough();
319
+ function sanitizeModelContentPart(value) {
320
+ const text = reportingTextPartSchema.safeParse(value);
321
+ if (text.success) return { type: "text", text: text.data.text };
322
+ const media = reportingMediaPartSchema.safeParse(value);
323
+ if (!media.success) return void 0;
324
+ return {
325
+ type: media.data.type,
326
+ ...media.data.mimeType ? { mimeType: media.data.mimeType } : {}
327
+ };
328
+ }
329
+ function modelVisibleToolOutput(content) {
330
+ const sanitized = content.map(sanitizeModelContentPart).filter((part) => part !== void 0);
331
+ if (sanitized.length === 0) return void 0;
332
+ if (sanitized.length !== 1) return sanitized;
333
+ const only = sanitized[0];
334
+ return only.type === "text" ? only.text : only;
335
+ }
336
+ function reportToolCalls(args) {
337
+ const message2 = reportingAssistantMessageSchema.safeParse(args.message);
338
+ if (!message2.success) return void 0;
339
+ const calls = message2.data.content.flatMap((part) => {
340
+ const call = reportingToolCallPartSchema.safeParse(part);
341
+ if (!call.success) return [];
342
+ return [
343
+ {
344
+ toolCallId: call.data.id,
345
+ name: call.data.name,
346
+ ...args.canExposePayload && call.data.arguments !== void 0 ? { input: call.data.arguments } : {}
347
+ }
348
+ ];
349
+ });
350
+ return calls.length > 0 ? { type: "tool_calls", calls } : void 0;
351
+ }
352
+ function reportToolResult(args) {
353
+ const message2 = reportingToolResultMessageSchema.safeParse(args.message);
354
+ if (!message2.success) return void 0;
355
+ const details = reportingToolResultDetailsSchema.safeParse(
356
+ message2.data.details
357
+ );
358
+ const outcome = message2.data.isError === true || details.success && (details.data.ok === false || details.data.status === "error") ? "error" : "completed";
359
+ const output = modelVisibleToolOutput(message2.data.content);
360
+ return {
361
+ type: "tool_result",
362
+ toolCallId: message2.data.toolCallId,
363
+ outcome,
364
+ ...args.canExposePayload && output !== void 0 ? { output } : {}
365
+ };
366
+ }
275
367
  function reportEventData(args) {
276
368
  const { data } = args;
277
369
  switch (data.type) {
@@ -280,6 +372,7 @@ function reportEventData(args) {
280
372
  type: "message",
281
373
  messageId: data.messageId,
282
374
  role: data.role,
375
+ ...typeof data.meta?.eventType === "string" ? { eventType: data.meta.eventType } : {},
283
376
  ...args.canExposePayload ? { text: data.text } : { redacted: true }
284
377
  };
285
378
  case "message_handled":
@@ -314,21 +407,28 @@ function reportEventData(args) {
314
407
  }
315
408
  function projectConversationReportEvents(args) {
316
409
  const subagentStarts = /* @__PURE__ */ new Map();
317
- const toolStarts = /* @__PURE__ */ new Map();
318
410
  const projected = [];
319
411
  for (const event of args.events) {
320
412
  let data;
321
- if (event.data.type === "tool_execution_started") {
322
- toolStarts.set(event.data.toolCallId, event.seq);
323
- data = { type: "tool_started", name: event.data.toolName };
413
+ if (event.data.type === "agent_step") {
414
+ const reportArgs = {
415
+ canExposePayload: args.canExposePayload,
416
+ message: event.data.message
417
+ };
418
+ data = reportToolCalls(reportArgs) ?? reportToolResult(reportArgs);
419
+ } else if (event.data.type === "tool_execution_started") {
420
+ data = {
421
+ type: "tool_started",
422
+ toolCallId: event.data.toolCallId,
423
+ name: event.data.toolName
424
+ };
324
425
  } else if (event.data.type === "subagent_started") {
325
426
  subagentStarts.set(event.data.subagentInvocationId, event.seq);
326
- const toolStartedSeq = event.data.parentToolCallId ? toolStarts.get(event.data.parentToolCallId) : void 0;
327
427
  data = {
328
428
  type: "subagent_started",
329
429
  childConversationId: event.data.childConversationId,
330
430
  subagentKind: event.data.subagentKind,
331
- ...toolStartedSeq === void 0 ? {} : { toolStartedSeq }
431
+ ...event.data.parentToolCallId ? { parentToolCallId: event.data.parentToolCallId } : {}
332
432
  };
333
433
  } else if (event.data.type === "subagent_ended") {
334
434
  const startedSeq = subagentStarts.get(event.data.subagentInvocationId);
@@ -340,10 +440,12 @@ function projectConversationReportEvents(args) {
340
440
  };
341
441
  }
342
442
  } else if (event.data.type === "handoff") {
343
- const toolStartedSeq = event.data.triggeringToolCallId ? toolStarts.get(event.data.triggeringToolCallId) : void 0;
344
443
  data = {
345
444
  type: "handoff",
346
- ...toolStartedSeq === void 0 ? {} : { toolStartedSeq }
445
+ modelProfile: event.data.modelProfile,
446
+ modelId: event.data.modelId,
447
+ ...event.data.reasoningLevel ? { reasoningLevel: event.data.reasoningLevel } : {},
448
+ ...event.data.triggeringToolCallId ? { triggeringToolCallId: event.data.triggeringToolCallId } : {}
347
449
  };
348
450
  } else {
349
451
  data = reportEventData({
@@ -364,7 +466,7 @@ function projectConversationReportEvents(args) {
364
466
  }
365
467
 
366
468
  // src/api/conversations/list.ts
367
- import { and as and3, asc, desc, eq as eq3, isNull } from "drizzle-orm";
469
+ import { and as and4, asc, desc, eq as eq5, isNull as isNull2 } from "drizzle-orm";
368
470
 
369
471
  // src/api/conversations/shared.ts
370
472
  function slackStatsLocationLabel(input) {
@@ -435,16 +537,13 @@ function titleFromConversation(args) {
435
537
  threadId: args.conversation.conversationId,
436
538
  channelName: effectiveChannelName
437
539
  });
438
- const privateLabel = resolveConversationPrivacy({
439
- conversationId: args.conversation.conversationId,
440
- visibility: args.conversation.visibility
441
- }) !== "public" ? privateConversationLabel(slackConversation) : void 0;
540
+ const privateLabel = args.canViewPrivateContent ? void 0 : privateConversationLabel(slackConversation);
442
541
  return privateLabel ?? args.conversation.title ?? slackStatsLocationLabel({
443
542
  channel: slackThread?.channelId,
444
543
  channelName: effectiveChannelName
445
544
  }) ?? surfaceFallbackLabel(args.surface);
446
545
  }
447
- function channelNameFromConversation(conversation) {
546
+ function channelNameFromConversation(conversation, canViewPrivateContent) {
448
547
  const effectiveChannelName = conversation.channelName;
449
548
  const slackThread = parseSlackThreadId(conversation.conversationId);
450
549
  if (!effectiveChannelName && !slackThread) {
@@ -454,39 +553,45 @@ function channelNameFromConversation(conversation) {
454
553
  threadId: conversation.conversationId,
455
554
  channelName: effectiveChannelName
456
555
  });
457
- if (resolveConversationPrivacy({
458
- conversationId: conversation.conversationId,
459
- visibility: conversation.visibility
460
- }) !== "public") {
556
+ if (!canViewPrivateContent) {
461
557
  return privateConversationLabel(slackConversation);
462
558
  }
463
559
  return effectiveChannelName;
464
560
  }
465
- function channelNameRedactedFromConversation(conversation) {
561
+ function channelNameRedactedFromConversation(conversation, canViewPrivateContent) {
466
562
  const effectiveChannelName = conversation.channelName;
467
563
  const slackThread = parseSlackThreadId(conversation.conversationId);
468
564
  if (!effectiveChannelName && !slackThread) {
469
565
  return false;
470
566
  }
471
- return resolveConversationPrivacy({
472
- conversationId: conversation.conversationId,
473
- visibility: conversation.visibility
474
- }) !== "public";
567
+ return !canViewPrivateContent;
475
568
  }
476
569
  function conversationSummaryFromStoredConversation(args) {
477
- const { conversation, durationMs, usage: usage2 } = args;
570
+ const { conversation, durationMs, usage: usage3 } = args;
571
+ const canViewPrivateContent = args.access?.canViewPrivateContent ?? false;
478
572
  const surface = surfaceFromSource(
479
573
  conversation.source,
480
574
  conversation.conversationId
481
575
  );
482
576
  const actorIdentity = actorIdentityReport(conversation.actor);
483
577
  const slackThread = parseSlackThreadId(conversation.conversationId);
484
- const channelName = channelNameFromConversation(conversation);
485
- const channelNameRedacted = channelNameRedactedFromConversation(conversation);
578
+ const channelName = channelNameFromConversation(
579
+ conversation,
580
+ canViewPrivateContent
581
+ );
582
+ const channelNameRedacted = channelNameRedactedFromConversation(
583
+ conversation,
584
+ canViewPrivateContent
585
+ );
486
586
  return {
487
587
  conversationId: conversation.conversationId,
488
588
  cumulativeDurationMs: durationMs,
489
- displayTitle: titleFromConversation({ conversation, surface }),
589
+ displayTitle: titleFromConversation({
590
+ canViewPrivateContent,
591
+ conversation,
592
+ surface
593
+ }),
594
+ isParticipant: args.access?.isParticipant ?? false,
490
595
  lastProgressAt: new Date(
491
596
  conversation.execution.updatedAtMs ?? conversation.updatedAtMs
492
597
  ).toISOString(),
@@ -494,7 +599,7 @@ function conversationSummaryFromStoredConversation(args) {
494
599
  startedAt: new Date(conversation.createdAtMs).toISOString(),
495
600
  status: statusFromConversation(conversation),
496
601
  surface,
497
- ...usage2 ? { cumulativeUsage: usage2 } : {},
602
+ ...usage3 ? { cumulativeUsage: usage3 } : {},
498
603
  ...actorIdentity ? { actorIdentity } : {},
499
604
  ...conversation.archivedAtMs ? { archivedAt: new Date(conversation.archivedAtMs).toISOString() } : {},
500
605
  ...slackThread ? { channel: slackThread.channelId } : {},
@@ -504,6 +609,163 @@ function conversationSummaryFromStoredConversation(args) {
504
609
  };
505
610
  }
506
611
 
612
+ // src/api/conversations/access.ts
613
+ import { eq as eq3, inArray } from "drizzle-orm";
614
+ import { alias } from "drizzle-orm/pg-core";
615
+ var rootConversation = alias(juniorConversations, "access_root_conversation");
616
+ var rootDestination = alias(juniorDestinations, "access_root_destination");
617
+ var rootIdentity = alias(juniorIdentities, "access_root_identity");
618
+ async function readConversationAccessFromSql(db, conversationIds, verifiedViewerEmail) {
619
+ if (conversationIds.length === 0) return /* @__PURE__ */ new Map();
620
+ const normalizedViewerEmail = verifiedViewerEmail?.trim().toLowerCase() || void 0;
621
+ const rows = await db.select({
622
+ conversationId: juniorConversations.conversationId,
623
+ parentConversationId: juniorConversations.parentConversationId,
624
+ storedRootConversationId: juniorConversations.rootConversationId,
625
+ rootConversationId: rootConversation.conversationId,
626
+ rootParentConversationId: rootConversation.parentConversationId,
627
+ rootRootConversationId: rootConversation.rootConversationId,
628
+ visibility: rootDestination.visibility,
629
+ rootEmailNormalized: rootIdentity.emailNormalized,
630
+ rootEmailVerified: rootIdentity.emailVerified
631
+ }).from(juniorConversations).leftJoin(
632
+ rootConversation,
633
+ eq3(
634
+ rootConversation.conversationId,
635
+ juniorConversations.rootConversationId
636
+ )
637
+ ).leftJoin(
638
+ rootDestination,
639
+ eq3(rootDestination.id, rootConversation.destinationId)
640
+ ).leftJoin(
641
+ rootIdentity,
642
+ eq3(rootIdentity.id, rootConversation.actorIdentityId)
643
+ ).where(inArray(juniorConversations.conversationId, [...conversationIds]));
644
+ return new Map(
645
+ rows.map((row) => {
646
+ const hasValidRoot = row.rootConversationId !== null && row.rootConversationId === row.storedRootConversationId && row.rootRootConversationId === row.rootConversationId && row.rootParentConversationId === null && (row.parentConversationId !== null || row.storedRootConversationId === row.conversationId);
647
+ const validRootConversationId = hasValidRoot ? row.rootConversationId ?? void 0 : void 0;
648
+ const visibility = validRootConversationId === void 0 ? null : row.visibility;
649
+ const isParticipant = validRootConversationId !== void 0 && normalizedViewerEmail !== void 0 && row.rootEmailVerified === true && row.rootEmailNormalized === normalizedViewerEmail;
650
+ const canViewPrivateContent = isParticipant || validRootConversationId !== void 0 && canExposeConversationPayload({
651
+ conversationId: validRootConversationId,
652
+ ...visibility === "public" || visibility === "private" ? { visibility } : {}
653
+ });
654
+ return [
655
+ row.conversationId,
656
+ {
657
+ canViewPrivateContent,
658
+ isParticipant,
659
+ visibility
660
+ }
661
+ ];
662
+ })
663
+ );
664
+ }
665
+
666
+ // src/api/conversations/usage.ts
667
+ import { and as and3, eq as eq4, inArray as inArray2, isNull, sql as sql2 } from "drizzle-orm";
668
+ import { alias as alias2 } from "drizzle-orm/pg-core";
669
+ var usage2 = juniorConversations.usage;
670
+ var metricRootConversation = alias2(
671
+ juniorConversations,
672
+ "metric_root_conversation"
673
+ );
674
+ var componentFields = [
675
+ "inputTokens",
676
+ "outputTokens",
677
+ "cachedInputTokens",
678
+ "cacheCreationTokens"
679
+ ];
680
+ function summedUsageNumber(field) {
681
+ return sql2`sum((${usage2}->>${field})::double precision)::double precision`;
682
+ }
683
+ function summedCostNumber(field) {
684
+ return sql2`round(
685
+ sum((${usage2}->'cost'->>${field})::numeric),
686
+ 12
687
+ )::double precision`;
688
+ }
689
+ function usageFromAggregate(row) {
690
+ const components = {};
691
+ let componentTotal = 0;
692
+ for (const field of componentFields) {
693
+ const value = row[field];
694
+ if (value === null) continue;
695
+ components[field] = value;
696
+ componentTotal += value;
697
+ }
698
+ const cost2 = {
699
+ ...row.costInput === null ? {} : { input: row.costInput },
700
+ ...row.costOutput === null ? {} : { output: row.costOutput },
701
+ ...row.costCacheRead === null ? {} : { cacheRead: row.costCacheRead },
702
+ ...row.costCacheWrite === null ? {} : { cacheWrite: row.costCacheWrite },
703
+ ...row.costTotal === null ? {} : { total: row.costTotal }
704
+ };
705
+ const result = {
706
+ ...row.totalOnlyTokens === null ? components : { totalTokens: row.totalOnlyTokens + componentTotal },
707
+ ...row.reasoningTokens === null ? {} : { reasoningTokens: row.reasoningTokens },
708
+ ...Object.keys(cost2).length === 0 ? {} : { cost: cost2 }
709
+ };
710
+ return hasAgentTurnUsage(result) ? result : void 0;
711
+ }
712
+ async function readRootConversationMetricsFromSql(db, rootConversationIds) {
713
+ if (rootConversationIds.length === 0) return /* @__PURE__ */ new Map();
714
+ const hasComponents = sql2`coalesce(
715
+ ${usage2}->>'inputTokens',
716
+ ${usage2}->>'outputTokens',
717
+ ${usage2}->>'cachedInputTokens',
718
+ ${usage2}->>'cacheCreationTokens'
719
+ ) is not null`;
720
+ const rows = await db.select({
721
+ rootConversationId: juniorConversations.rootConversationId,
722
+ durationMs: sql2`coalesce(sum(${juniorConversations.durationMs}), 0)::double precision`,
723
+ inputTokens: summedUsageNumber("inputTokens"),
724
+ outputTokens: summedUsageNumber("outputTokens"),
725
+ cachedInputTokens: summedUsageNumber("cachedInputTokens"),
726
+ cacheCreationTokens: summedUsageNumber("cacheCreationTokens"),
727
+ totalOnlyTokens: sql2`sum(
728
+ case
729
+ when not (${hasComponents})
730
+ then (${usage2}->>'totalTokens')::double precision
731
+ end
732
+ )::double precision`,
733
+ reasoningTokens: summedUsageNumber("reasoningTokens"),
734
+ costInput: summedCostNumber("input"),
735
+ costOutput: summedCostNumber("output"),
736
+ costCacheRead: summedCostNumber("cacheRead"),
737
+ costCacheWrite: summedCostNumber("cacheWrite"),
738
+ costTotal: summedCostNumber("total")
739
+ }).from(juniorConversations).innerJoin(
740
+ metricRootConversation,
741
+ and3(
742
+ eq4(
743
+ metricRootConversation.conversationId,
744
+ juniorConversations.rootConversationId
745
+ ),
746
+ isNull(metricRootConversation.parentConversationId),
747
+ eq4(
748
+ metricRootConversation.rootConversationId,
749
+ metricRootConversation.conversationId
750
+ )
751
+ )
752
+ ).where(
753
+ inArray2(juniorConversations.rootConversationId, [...rootConversationIds])
754
+ ).groupBy(juniorConversations.rootConversationId);
755
+ return new Map(
756
+ rows.flatMap((row) => {
757
+ if (!row.rootConversationId) return [];
758
+ const usage3 = usageFromAggregate(row);
759
+ return [
760
+ [
761
+ row.rootConversationId,
762
+ { durationMs: row.durationMs, ...usage3 ? { usage: usage3 } : {} }
763
+ ]
764
+ ];
765
+ })
766
+ );
767
+ }
768
+
507
769
  // src/api/conversations/list.ts
508
770
  var CONVERSATION_FEED_LIMIT = 50;
509
771
  async function conversationRows(db, limit, actorEmail) {
@@ -520,17 +782,17 @@ async function conversationRows(db, limit, actorEmail) {
520
782
  userDisplayName: juniorUsers.displayName
521
783
  }).from(juniorConversations).leftJoin(
522
784
  juniorDestinations,
523
- eq3(juniorDestinations.id, juniorConversations.destinationId)
785
+ eq5(juniorDestinations.id, juniorConversations.destinationId)
524
786
  ).leftJoin(
525
787
  juniorIdentities,
526
- eq3(juniorIdentities.id, juniorConversations.actorIdentityId)
527
- ).leftJoin(juniorUsers, eq3(juniorUsers.id, juniorIdentities.userId)).where(
528
- and3(
529
- isNull(juniorConversations.parentConversationId),
530
- isNull(juniorConversations.archivedAt),
531
- actorEmail ? and3(
532
- eq3(juniorIdentities.emailNormalized, actorEmail),
533
- eq3(juniorIdentities.emailVerified, true)
788
+ eq5(juniorIdentities.id, juniorConversations.actorIdentityId)
789
+ ).leftJoin(juniorUsers, eq5(juniorUsers.id, juniorIdentities.userId)).where(
790
+ and4(
791
+ isNull2(juniorConversations.parentConversationId),
792
+ isNull2(juniorConversations.archivedAt),
793
+ actorEmail ? and4(
794
+ eq5(juniorIdentities.emailNormalized, actorEmail),
795
+ eq5(juniorIdentities.emailVerified, true)
534
796
  ) : void 0
535
797
  )
536
798
  ).orderBy(
@@ -565,10 +827,7 @@ function conversationFromRow(row) {
565
827
  ...value.channelName ? { channelName: value.channelName } : {},
566
828
  ...value.source ? { source: value.source } : {},
567
829
  ...value.title ? { title: value.title } : {},
568
- ...value.transcriptPurgedAt ? { transcriptPurgedAtMs: value.transcriptPurgedAt.getTime() } : {},
569
- ...row.destinationVisibility ? {
570
- visibility: row.destinationVisibility === "public" ? "public" : "private"
571
- } : {}
830
+ ...value.transcriptPurgedAt ? { transcriptPurgedAtMs: value.transcriptPurgedAt.getTime() } : {}
572
831
  };
573
832
  }
574
833
  async function readConversationRecordFromSql(conversationId) {
@@ -586,76 +845,110 @@ async function readConversationRecordFromSql(conversationId) {
586
845
  userDisplayName: juniorUsers.displayName
587
846
  }).from(juniorConversations).leftJoin(
588
847
  juniorDestinations,
589
- eq3(juniorDestinations.id, juniorConversations.destinationId)
848
+ eq5(juniorDestinations.id, juniorConversations.destinationId)
590
849
  ).leftJoin(
591
850
  juniorIdentities,
592
- eq3(juniorIdentities.id, juniorConversations.actorIdentityId)
593
- ).leftJoin(juniorUsers, eq3(juniorUsers.id, juniorIdentities.userId)).where(eq3(juniorConversations.conversationId, conversationId)).limit(1);
851
+ eq5(juniorIdentities.id, juniorConversations.actorIdentityId)
852
+ ).leftJoin(juniorUsers, eq5(juniorUsers.id, juniorIdentities.userId)).where(eq5(juniorConversations.conversationId, conversationId)).limit(1);
594
853
  const row = rows[0];
595
854
  return row ? {
596
855
  conversation: conversationFromRow(row),
597
856
  durationMs: row.conversation.durationMs,
598
857
  ...row.destinationVisibility === "public" && row.destinationId ? { locationId: row.destinationId } : {},
599
- usage: row.conversation.usage
858
+ usage: row.conversation.usage,
859
+ rootConversationId: row.conversation.rootConversationId
600
860
  } : void 0;
601
861
  }
602
862
  async function readConversationFeedFromSql(options = {}) {
603
863
  const nowMs = Date.now();
864
+ const db = getDb();
604
865
  const rows = await conversationRows(
605
- getDb(),
866
+ db,
606
867
  options.limit ?? CONVERSATION_FEED_LIMIT,
607
868
  options.actorEmail
608
869
  );
870
+ const conversationIds = rows.map((row) => row.conversation.conversationId);
871
+ const [accessByConversation, metricsByRoot] = await Promise.all([
872
+ readConversationAccessFromSql(
873
+ db,
874
+ conversationIds,
875
+ options.verifiedViewerEmail
876
+ ),
877
+ readRootConversationMetricsFromSql(db, conversationIds)
878
+ ]);
609
879
  return {
610
- conversations: rows.map(
611
- (row) => conversationSummaryFromStoredConversation({
880
+ conversations: rows.map((row) => {
881
+ const metrics = metricsByRoot.get(row.conversation.conversationId);
882
+ return conversationSummaryFromStoredConversation({
612
883
  conversation: conversationFromRow(row),
613
- durationMs: row.conversation.durationMs,
884
+ access: accessByConversation.get(row.conversation.conversationId),
885
+ durationMs: metrics?.durationMs ?? row.conversation.durationMs,
614
886
  ...row.destinationVisibility === "public" && row.destinationId ? { locationId: row.destinationId } : {},
615
- usage: row.conversation.usage ?? void 0
616
- })
617
- ),
887
+ usage: metrics?.usage ?? row.conversation.usage ?? void 0
888
+ });
889
+ }),
618
890
  generatedAt: new Date(nowMs).toISOString(),
619
891
  source: "conversation_index"
620
892
  };
621
893
  }
622
894
  async function readConversationFeed(options = {}) {
623
895
  return conversationFeedSchema.parse(
624
- await readConversationFeedFromSql({ actorEmail: options.actorEmail })
896
+ await readConversationFeedFromSql(options)
625
897
  );
626
898
  }
627
- var list_default = {
899
+ var list_default = defineApiRoute({
628
900
  method: "get",
629
901
  path: "/",
902
+ responseSchema: conversationFeedSchema,
630
903
  handler: async (c) => {
631
904
  const { actorEmail } = parseQuery(
632
905
  conversationFeedQuerySchema,
633
906
  c.req.query()
634
907
  );
635
- return Response.json(await readConversationFeed({ actorEmail }));
908
+ const verifiedViewerEmail = c.get("verifiedViewerEmail");
909
+ return readConversationFeed({
910
+ ...actorEmail ? { actorEmail } : {},
911
+ ...verifiedViewerEmail ? { verifiedViewerEmail } : {}
912
+ });
636
913
  }
637
- };
914
+ });
638
915
 
639
916
  // src/api/conversations/detail.ts
917
+ var conversationEventColumns = getTableColumns(juniorConversationEvents);
918
+ async function readConversationReportEventRows(executor, conversationId) {
919
+ return executor.db().select({
920
+ ...conversationEventColumns,
921
+ // Replacement history is model context, never dashboard report data.
922
+ payload: sql3`case
923
+ when ${juniorConversationEvents.type} in ('compaction', 'handoff')
924
+ then jsonb_set(
925
+ ${juniorConversationEvents.payload},
926
+ '{replacementHistory}',
927
+ '[]'::jsonb
928
+ )
929
+ else ${juniorConversationEvents.payload}
930
+ end`
931
+ }).from(juniorConversationEvents).where(
932
+ and5(
933
+ eq6(juniorConversationEvents.conversationId, conversationId),
934
+ inArray3(juniorConversationEvents.type, [
935
+ ...conversationReportSourceEventTypes
936
+ ])
937
+ )
938
+ ).orderBy(asc2(juniorConversationEvents.seq));
939
+ }
640
940
  function projectConversationDetail(args) {
641
941
  const { conversation } = args;
642
942
  const conversationId = conversation.conversationId;
643
943
  const transcriptPurgedAtMs = conversation.transcriptPurgedAtMs;
644
- const { visibility: _storedVisibility, ...conversationWithoutVisibility } = conversation;
645
- const authorizedConversation = {
646
- ...conversationWithoutVisibility,
647
- ...args.effectiveVisibility ? { visibility: args.effectiveVisibility } : {}
648
- };
649
- const canExposePayload = canExposeConversationPayload({
650
- conversationId: args.privacyConversationId ?? conversationId,
651
- visibility: args.effectiveVisibility
652
- });
944
+ const canExposePayload = args.access?.canViewPrivateContent ?? false;
653
945
  const events = transcriptPurgedAtMs === void 0 ? args.events : [];
654
946
  const modelUsage = transcriptPurgedAtMs === void 0 ? args.modelUsage : [];
655
947
  const sentryConversationUrl = buildSentryConversationUrl(conversationId);
656
948
  return {
657
949
  ...conversationSummaryFromStoredConversation({
658
- conversation: authorizedConversation,
950
+ access: args.access,
951
+ conversation,
659
952
  durationMs: args.durationMs,
660
953
  ...args.locationId ? { locationId: args.locationId } : {},
661
954
  usage: args.usage
@@ -673,19 +966,28 @@ function projectConversationDetail(args) {
673
966
  ...sentryConversationUrl ? { sentryConversationUrl } : {}
674
967
  };
675
968
  }
676
- async function readConversationDetailFromSql(conversationId) {
969
+ async function readConversationDetailFromSql(conversationId, options) {
677
970
  const record = await readConversationRecordFromSql(conversationId);
678
971
  if (!record) return void 0;
679
972
  const executor = getSqlExecutor();
680
- const [snapshot, modelUsage] = await Promise.all([
681
- readConversationEventPrivacySnapshot(executor, {
973
+ const includeDescendantMetrics = record.rootConversationId === conversationId;
974
+ const [accessByConversation, eventRows, modelUsage, metricsByRoot] = await Promise.all([
975
+ readConversationAccessFromSql(
976
+ getDb(),
977
+ [conversationId],
978
+ options.verifiedViewerEmail
979
+ ),
980
+ readConversationReportEventRows(executor, conversationId),
981
+ record.conversation.transcriptPurgedAtMs === void 0 ? readConversationModelUsageFromSql(executor, {
682
982
  conversationId,
683
- eventTypes: conversationReportSourceEventTypes
684
- }),
685
- record.conversation.transcriptPurgedAtMs === void 0 ? readConversationModelUsageFromSql(executor, conversationId) : Promise.resolve([])
983
+ includeDescendants: includeDescendantMetrics
984
+ }) : Promise.resolve([]),
985
+ readRootConversationMetricsFromSql(
986
+ getDb(),
987
+ includeDescendantMetrics ? [conversationId] : []
988
+ )
686
989
  ]);
687
- if (!snapshot) return void 0;
688
- const events = snapshot.events.map(
990
+ const events = eventRows.map(
689
991
  (row) => decodeStoredConversationEvent({
690
992
  schemaVersion: row.schemaVersion,
691
993
  seq: row.seq,
@@ -696,106 +998,121 @@ async function readConversationDetailFromSql(conversationId) {
696
998
  payload: row.payload
697
999
  })
698
1000
  );
699
- const effectiveVisibility = snapshot.visibility === "public" || snapshot.visibility === "private" ? snapshot.visibility : void 0;
1001
+ const metrics = metricsByRoot.get(conversationId);
700
1002
  return projectConversationDetail({
701
1003
  ...record,
702
- effectiveVisibility,
1004
+ access: accessByConversation.get(conversationId),
1005
+ durationMs: metrics?.durationMs ?? record.durationMs,
703
1006
  events,
704
1007
  modelUsage,
705
- privacyConversationId: snapshot.rootConversationId,
706
- usage: record.usage ?? void 0
1008
+ usage: metrics?.usage ?? record.usage ?? void 0
707
1009
  });
708
1010
  }
709
- async function readConversationDetail(conversationId) {
710
- const report = await readConversationDetailFromSql(conversationId);
1011
+ async function readConversationDetail(conversationId, options = {}) {
1012
+ const report = await readConversationDetailFromSql(conversationId, options);
711
1013
  return report ? conversationDetailReportSchema.parse(report) : void 0;
712
1014
  }
713
- var detail_default = {
1015
+ var detail_default = defineApiRoute({
714
1016
  method: "get",
715
1017
  path: "/:conversationId",
1018
+ responseSchema: conversationDetailReportSchema,
716
1019
  handler: async (c) => {
717
1020
  const { conversationId } = parseParams(
718
1021
  conversationParamsSchema,
719
1022
  c.req.param()
720
1023
  );
721
- const report = await readConversationDetail(conversationId);
722
- return report ? Response.json(report) : Response.json({ error: "Conversation not found." }, { status: 404 });
1024
+ const verifiedViewerEmail = c.get("verifiedViewerEmail");
1025
+ const report = await readConversationDetail(
1026
+ conversationId,
1027
+ verifiedViewerEmail ? { verifiedViewerEmail } : {}
1028
+ );
1029
+ if (!report) throwApiError(404, "Conversation not found.");
1030
+ return report;
723
1031
  }
724
- };
1032
+ });
725
1033
 
726
1034
  // src/api/conversations/stats.query.ts
727
- import { and as and4, eq as eq4, gte, isNull as isNull2, lte, sql as sql3 } from "drizzle-orm";
1035
+ import { and as and6, eq as eq7, gte, isNull as isNull3, lte, sql as sql5 } from "drizzle-orm";
1036
+ import { alias as alias3 } from "drizzle-orm/pg-core";
728
1037
 
729
1038
  // src/api/conversations/aggregate.ts
730
- import { sql as sql2 } from "drizzle-orm";
731
- function tokenValue() {
732
- return sql2`
1039
+ import { sql as sql4 } from "drizzle-orm";
1040
+ function tokenValue(source) {
1041
+ return sql4`
733
1042
  CASE
734
- WHEN ${juniorConversations.usage}->>'totalTokens' IS NOT NULL
735
- THEN (${juniorConversations.usage}->>'totalTokens')::double precision
736
1043
  WHEN COALESCE(
737
- ${juniorConversations.usage}->>'inputTokens',
738
- ${juniorConversations.usage}->>'outputTokens',
739
- ${juniorConversations.usage}->>'cachedInputTokens',
740
- ${juniorConversations.usage}->>'cacheCreationTokens'
1044
+ ${source.usage}->>'inputTokens',
1045
+ ${source.usage}->>'outputTokens',
1046
+ ${source.usage}->>'cachedInputTokens',
1047
+ ${source.usage}->>'cacheCreationTokens'
741
1048
  ) IS NOT NULL
742
- THEN COALESCE((${juniorConversations.usage}->>'inputTokens')::double precision, 0)
743
- + COALESCE((${juniorConversations.usage}->>'outputTokens')::double precision, 0)
744
- + COALESCE((${juniorConversations.usage}->>'cachedInputTokens')::double precision, 0)
745
- + COALESCE((${juniorConversations.usage}->>'cacheCreationTokens')::double precision, 0)
1049
+ THEN COALESCE((${source.usage}->>'inputTokens')::double precision, 0)
1050
+ + COALESCE((${source.usage}->>'outputTokens')::double precision, 0)
1051
+ + COALESCE((${source.usage}->>'cachedInputTokens')::double precision, 0)
1052
+ + COALESCE((${source.usage}->>'cacheCreationTokens')::double precision, 0)
1053
+ WHEN ${source.usage}->>'totalTokens' IS NOT NULL
1054
+ THEN (${source.usage}->>'totalTokens')::double precision
746
1055
  ELSE NULL
747
1056
  END
748
1057
  `;
749
1058
  }
750
- function costValue() {
751
- return sql2`
1059
+ function costValue(source) {
1060
+ return sql4`
752
1061
  CASE
753
- WHEN ${juniorConversations.usage}->'cost'->>'total' IS NOT NULL
754
- THEN (${juniorConversations.usage}->'cost'->>'total')::double precision
1062
+ WHEN ${source.usage}->'cost'->>'total' IS NOT NULL
1063
+ THEN (${source.usage}->'cost'->>'total')::double precision
755
1064
  WHEN COALESCE(
756
- ${juniorConversations.usage}->'cost'->>'input',
757
- ${juniorConversations.usage}->'cost'->>'output',
758
- ${juniorConversations.usage}->'cost'->>'cacheRead',
759
- ${juniorConversations.usage}->'cost'->>'cacheWrite'
1065
+ ${source.usage}->'cost'->>'input',
1066
+ ${source.usage}->'cost'->>'output',
1067
+ ${source.usage}->'cost'->>'cacheRead',
1068
+ ${source.usage}->'cost'->>'cacheWrite'
760
1069
  ) IS NOT NULL
761
- THEN COALESCE((${juniorConversations.usage}->'cost'->>'input')::double precision, 0)
762
- + COALESCE((${juniorConversations.usage}->'cost'->>'output')::double precision, 0)
763
- + COALESCE((${juniorConversations.usage}->'cost'->>'cacheRead')::double precision, 0)
764
- + COALESCE((${juniorConversations.usage}->'cost'->>'cacheWrite')::double precision, 0)
1070
+ THEN COALESCE((${source.usage}->'cost'->>'input')::double precision, 0)
1071
+ + COALESCE((${source.usage}->'cost'->>'output')::double precision, 0)
1072
+ + COALESCE((${source.usage}->'cost'->>'cacheRead')::double precision, 0)
1073
+ + COALESCE((${source.usage}->'cost'->>'cacheWrite')::double precision, 0)
765
1074
  ELSE NULL
766
1075
  END
767
1076
  `;
768
1077
  }
769
- function conversationAggregateColumns() {
1078
+ function conversationAggregateColumns(sources) {
1079
+ const metrics = sources?.metrics ?? juniorConversations;
1080
+ const roots = sources?.roots ?? juniorConversations;
1081
+ const conversationCount = sources ? sql4`COUNT(DISTINCT ${roots.conversationId})` : sql4`COUNT(*)`;
770
1082
  return {
771
- active: sql2`COUNT(*) FILTER (
772
- WHERE ${juniorConversations.executionStatus} NOT IN ('idle', 'failed')
1083
+ active: sql4`${conversationCount} FILTER (
1084
+ WHERE ${roots.executionStatus} NOT IN ('idle', 'failed')
773
1085
  )::integer`,
774
- conversations: sql2`COUNT(*)::integer`,
775
- costUsd: sql2`SUM(${costValue()})::double precision`,
776
- durationMs: sql2`COALESCE(SUM(${juniorConversations.durationMs}), 0)::double precision`,
777
- failed: sql2`COUNT(*) FILTER (
778
- WHERE ${juniorConversations.executionStatus} = 'failed'
1086
+ conversations: sql4`${conversationCount}::integer`,
1087
+ costUsd: sql4`SUM(${costValue(metrics)})::double precision`,
1088
+ durationMs: sql4`COALESCE(SUM(${metrics.durationMs}), 0)::double precision`,
1089
+ failed: sql4`${conversationCount} FILTER (
1090
+ WHERE ${roots.executionStatus} = 'failed'
779
1091
  )::integer`,
780
- tokens: sql2`SUM(${tokenValue()})::double precision`
1092
+ tokens: sql4`SUM(${tokenValue(metrics)})::double precision`
781
1093
  };
782
1094
  }
783
1095
  function conversationRangeColumns() {
784
1096
  return {
785
- firstSeenAt: sql2`MIN(${juniorConversations.createdAt})`.mapWith(
1097
+ firstSeenAt: sql4`MIN(${juniorConversations.createdAt})`.mapWith(
786
1098
  juniorConversations.createdAt
787
1099
  ),
788
- lastSeenAt: sql2`MAX(${juniorConversations.lastActivityAt})`.mapWith(
1100
+ lastSeenAt: sql4`MAX(${juniorConversations.lastActivityAt})`.mapWith(
789
1101
  juniorConversations.lastActivityAt
790
1102
  )
791
1103
  };
792
1104
  }
793
1105
  function conversationActiveDaysColumn() {
794
- return sql2`COUNT(DISTINCT (${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC')::date)::integer`;
1106
+ return sql4`COUNT(DISTINCT (${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC')::date)::integer`;
795
1107
  }
796
1108
 
797
1109
  // src/api/conversations/stats.query.ts
798
1110
  var WINDOW_DAYS = 90;
1111
+ var treeConversation = alias3(juniorConversations, "stats_tree_conversation");
1112
+ var treeAggregateColumns = conversationAggregateColumns({
1113
+ metrics: treeConversation,
1114
+ roots: juniorConversations
1115
+ });
799
1116
  function emptyStatsItem(label) {
800
1117
  return {
801
1118
  active: 0,
@@ -850,8 +1167,8 @@ function statsItems(map) {
850
1167
  );
851
1168
  }
852
1169
  function statsWhere(start, end) {
853
- return and4(
854
- isNull2(juniorConversations.parentConversationId),
1170
+ return and6(
1171
+ isNull3(juniorConversations.parentConversationId),
855
1172
  gte(juniorConversations.lastActivityAt, start),
856
1173
  lte(juniorConversations.lastActivityAt, end)
857
1174
  );
@@ -884,12 +1201,18 @@ function metricDays(rows, endMs) {
884
1201
  }
885
1202
  async function aggregateStats(db, start, end) {
886
1203
  const where = statsWhere(start, end);
887
- const activityDate = sql3`TO_CHAR(
1204
+ const activityDate = sql5`TO_CHAR(
888
1205
  ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
889
1206
  'YYYY-MM-DD'
890
1207
  )`;
891
1208
  const [totalsRows, actorRows, locationRows, metricRows] = await Promise.all([
892
- db.select(conversationAggregateColumns()).from(juniorConversations).where(where),
1209
+ db.select(treeAggregateColumns).from(juniorConversations).innerJoin(
1210
+ treeConversation,
1211
+ eq7(
1212
+ treeConversation.rootConversationId,
1213
+ juniorConversations.conversationId
1214
+ )
1215
+ ).where(where),
893
1216
  db.select({
894
1217
  identityDisplayName: juniorIdentities.displayName,
895
1218
  identityEmail: juniorIdentities.emailNormalized,
@@ -897,11 +1220,17 @@ async function aggregateStats(db, start, end) {
897
1220
  identitySubjectId: juniorIdentities.providerSubjectId,
898
1221
  userDisplayName: juniorUsers.displayName,
899
1222
  userEmail: juniorUsers.primaryEmailNormalized,
900
- ...conversationAggregateColumns()
901
- }).from(juniorConversations).leftJoin(
1223
+ ...treeAggregateColumns
1224
+ }).from(juniorConversations).innerJoin(
1225
+ treeConversation,
1226
+ eq7(
1227
+ treeConversation.rootConversationId,
1228
+ juniorConversations.conversationId
1229
+ )
1230
+ ).leftJoin(
902
1231
  juniorIdentities,
903
- eq4(juniorIdentities.id, juniorConversations.actorIdentityId)
904
- ).leftJoin(juniorUsers, eq4(juniorUsers.id, juniorIdentities.userId)).where(where).groupBy(
1232
+ eq7(juniorIdentities.id, juniorConversations.actorIdentityId)
1233
+ ).leftJoin(juniorUsers, eq7(juniorUsers.id, juniorIdentities.userId)).where(where).groupBy(
905
1234
  juniorIdentities.displayName,
906
1235
  juniorIdentities.emailNormalized,
907
1236
  juniorIdentities.handle,
@@ -916,10 +1245,16 @@ async function aggregateStats(db, start, end) {
916
1245
  destinationProvider: juniorDestinations.provider,
917
1246
  destinationVisibility: juniorDestinations.visibility,
918
1247
  source: juniorConversations.source,
919
- ...conversationAggregateColumns()
920
- }).from(juniorConversations).leftJoin(
1248
+ ...treeAggregateColumns
1249
+ }).from(juniorConversations).innerJoin(
1250
+ treeConversation,
1251
+ eq7(
1252
+ treeConversation.rootConversationId,
1253
+ juniorConversations.conversationId
1254
+ )
1255
+ ).leftJoin(
921
1256
  juniorDestinations,
922
- eq4(juniorDestinations.id, juniorConversations.destinationId)
1257
+ eq7(juniorDestinations.id, juniorConversations.destinationId)
923
1258
  ).where(where).groupBy(
924
1259
  juniorConversations.channelName,
925
1260
  juniorConversations.source,
@@ -929,11 +1264,17 @@ async function aggregateStats(db, start, end) {
929
1264
  juniorDestinations.visibility
930
1265
  ),
931
1266
  db.select({
932
- costUsd: conversationAggregateColumns().costUsd,
1267
+ costUsd: treeAggregateColumns.costUsd,
933
1268
  date: activityDate,
934
- durationMs: conversationAggregateColumns().durationMs,
935
- tokens: conversationAggregateColumns().tokens
936
- }).from(juniorConversations).where(where).groupBy(activityDate)
1269
+ durationMs: treeAggregateColumns.durationMs,
1270
+ tokens: treeAggregateColumns.tokens
1271
+ }).from(juniorConversations).innerJoin(
1272
+ treeConversation,
1273
+ eq7(
1274
+ treeConversation.rootConversationId,
1275
+ juniorConversations.conversationId
1276
+ )
1277
+ ).where(where).groupBy(activityDate)
937
1278
  ]);
938
1279
  return { actorRows, locationRows, metricRows, totals: totalsRows[0] };
939
1280
  }
@@ -976,17 +1317,18 @@ async function readConversationStats() {
976
1317
  await readConversationStatsFromSql()
977
1318
  );
978
1319
  }
979
- var stats_default = {
1320
+ var stats_default = defineApiRoute({
980
1321
  method: "get",
981
1322
  path: "/stats",
982
- handler: async () => Response.json(await readConversationStats())
983
- };
1323
+ responseSchema: conversationStatsReportSchema,
1324
+ handler: readConversationStats
1325
+ });
984
1326
 
985
1327
  // src/api/conversations/routes.ts
986
1328
  var routes = [list_default, stats_default, archive_default, detail_default];
987
1329
  function createConversationRoutes() {
988
1330
  const app = new Hono();
989
- for (const route of routes) app.on(route.method, route.path, route.handler);
1331
+ registerApiRoutes(app, routes);
990
1332
  return app;
991
1333
  }
992
1334
 
@@ -994,28 +1336,10 @@ function createConversationRoutes() {
994
1336
  import { Hono as Hono2 } from "hono";
995
1337
 
996
1338
  // src/api/locations/query.ts
997
- import { and as and5, asc as asc2, desc as desc2, eq as eq5, gte as gte2, isNull as isNull3, sql as sql4 } from "drizzle-orm";
1339
+ import { and as and7, asc as asc3, desc as desc2, eq as eq8, gte as gte2, isNull as isNull4, sql as sql6 } from "drizzle-orm";
1340
+ import { alias as alias4 } from "drizzle-orm/pg-core";
998
1341
 
999
1342
  // src/api/conversations/reporting.ts
1000
- var PRIVATE_CONVERSATION_LABEL2 = "Private Conversation";
1001
- function channelFromConversationId(conversationId) {
1002
- const [provider, channel] = conversationId.split(":");
1003
- return provider === "slack" && channel ? channel : void 0;
1004
- }
1005
- function surfaceFromRow(row) {
1006
- if (row.source === "api" || row.source === "scheduler" || row.source === "slack") {
1007
- return row.source;
1008
- }
1009
- if (row.conversationId.startsWith("slack:")) return "slack";
1010
- if (row.conversationId.startsWith("scheduler:")) return "scheduler";
1011
- if (row.conversationId.startsWith("api:")) return "api";
1012
- return "internal";
1013
- }
1014
- function statusFromRow(row) {
1015
- if (row.executionStatus === "failed") return "failed";
1016
- if (row.executionStatus === "idle") return "completed";
1017
- return "active";
1018
- }
1019
1343
  function surfaceLabel2(surface) {
1020
1344
  if (surface === "scheduler") return "Scheduler";
1021
1345
  if (surface === "api") return "API";
@@ -1037,22 +1361,6 @@ function slackLocationLabel(args) {
1037
1361
  }
1038
1362
  return name || channelId;
1039
1363
  }
1040
- function channelNameFromRow(row) {
1041
- if (row.destinationVisibility !== "public") {
1042
- return PRIVATE_CONVERSATION_LABEL2;
1043
- }
1044
- return row.channelName ?? void 0;
1045
- }
1046
- function titleFromRow(row, surface) {
1047
- if (row.destinationVisibility !== "public") {
1048
- return PRIVATE_CONVERSATION_LABEL2;
1049
- }
1050
- const channel = channelFromConversationId(row.conversationId);
1051
- return row.title ?? slackLocationLabel({
1052
- channel,
1053
- channelName: row.channelName ?? void 0
1054
- }) ?? surfaceLabel2(surface);
1055
- }
1056
1364
  function actorFromRow(row) {
1057
1365
  const actor = {
1058
1366
  ...row.email ? { email: row.email } : {},
@@ -1062,34 +1370,42 @@ function actorFromRow(row) {
1062
1370
  };
1063
1371
  return Object.keys(actor).length ? actor : void 0;
1064
1372
  }
1065
- function summaryFromRow(row) {
1066
- const surface = surfaceFromRow(row);
1067
- const channel = channelFromConversationId(row.conversationId);
1068
- const channelName = channelNameFromRow(row);
1069
- const channelNameRedacted = row.destinationVisibility !== "public";
1070
- const actorIdentity = actorFromRow(row);
1071
- return {
1373
+ function summaryFromRow(row, options = {}) {
1374
+ const actor = actorFromRow(row);
1375
+ const conversation = {
1072
1376
  conversationId: row.conversationId,
1073
- cumulativeDurationMs: row.durationMs,
1074
- displayTitle: titleFromRow(row, surface),
1075
- lastProgressAt: new Date(
1076
- row.executionUpdatedAt ?? row.updatedAt
1077
- ).toISOString(),
1078
- lastSeenAt: row.lastActivityAt.toISOString(),
1079
- startedAt: row.createdAt.toISOString(),
1080
- status: statusFromRow(row),
1081
- surface,
1082
- ...actorIdentity ? { actorIdentity } : {},
1083
- ...channel ? { channel } : {},
1084
- ...channelName ? { channelName } : {},
1085
- ...channelNameRedacted ? { channelNameRedacted: true } : {},
1086
- ...row.destinationVisibility === "public" && row.destinationId ? { locationId: row.destinationId } : {}
1377
+ createdAtMs: row.createdAt.getTime(),
1378
+ lastActivityAtMs: row.lastActivityAt.getTime(),
1379
+ updatedAtMs: row.updatedAt.getTime(),
1380
+ execution: {
1381
+ status: row.executionStatus,
1382
+ ...row.executionUpdatedAt ? { updatedAtMs: row.executionUpdatedAt.getTime() } : {}
1383
+ },
1384
+ ...actor ? { actor } : {},
1385
+ ...row.channelName ? { channelName: row.channelName } : {},
1386
+ ...row.source ? { source: row.source } : {},
1387
+ ...row.title ? { title: row.title } : {}
1087
1388
  };
1389
+ return conversationSummaryFromStoredConversation({
1390
+ access: options.access,
1391
+ conversation,
1392
+ durationMs: options.metrics?.durationMs ?? row.durationMs,
1393
+ ...options.access?.visibility === "public" && row.destinationId ? { locationId: row.destinationId } : {},
1394
+ usage: options.metrics?.usage ?? row.usage ?? void 0
1395
+ });
1088
1396
  }
1089
1397
 
1090
1398
  // src/api/locations/query.ts
1091
1399
  var RECENT_LIMIT = 25;
1092
1400
  var ACTIVITY_DAYS = 90;
1401
+ var treeConversation2 = alias4(
1402
+ juniorConversations,
1403
+ "location_tree_conversation"
1404
+ );
1405
+ var treeAggregateColumns2 = conversationAggregateColumns({
1406
+ metrics: treeConversation2,
1407
+ roots: juniorConversations
1408
+ });
1093
1409
  function emptyMetrics() {
1094
1410
  return {
1095
1411
  active: 0,
@@ -1125,13 +1441,13 @@ function emptyActor(actor) {
1125
1441
  };
1126
1442
  }
1127
1443
  function topLevelWhere() {
1128
- return isNull3(juniorConversations.parentConversationId);
1444
+ return isNull4(juniorConversations.parentConversationId);
1129
1445
  }
1130
1446
  function publicLocationWhere(destinationId) {
1131
- return and5(
1447
+ return and7(
1132
1448
  topLevelWhere(),
1133
- eq5(juniorDestinations.id, destinationId),
1134
- eq5(juniorDestinations.visibility, "public")
1449
+ eq8(juniorDestinations.id, destinationId),
1450
+ eq8(juniorDestinations.visibility, "public")
1135
1451
  );
1136
1452
  }
1137
1453
  function locationColumns() {
@@ -1178,26 +1494,32 @@ function locationFromAggregate(row) {
1178
1494
  async function directoryRows(db) {
1179
1495
  return db.select({
1180
1496
  ...locationColumns(),
1181
- ...conversationAggregateColumns(),
1497
+ ...treeAggregateColumns2,
1182
1498
  ...conversationRangeColumns()
1183
- }).from(juniorConversations).leftJoin(
1499
+ }).from(juniorConversations).innerJoin(
1500
+ treeConversation2,
1501
+ eq8(
1502
+ treeConversation2.rootConversationId,
1503
+ juniorConversations.conversationId
1504
+ )
1505
+ ).leftJoin(
1184
1506
  juniorDestinations,
1185
- eq5(juniorDestinations.id, juniorConversations.destinationId)
1507
+ eq8(juniorDestinations.id, juniorConversations.destinationId)
1186
1508
  ).where(topLevelWhere()).groupBy(...locationGroupBy());
1187
1509
  }
1188
1510
  async function directoryActivityRows(db, start) {
1189
- const date = sql4`TO_CHAR(
1511
+ const date = sql6`TO_CHAR(
1190
1512
  ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
1191
1513
  'YYYY-MM-DD'
1192
1514
  )`;
1193
1515
  return db.select({
1194
- conversations: sql4`COUNT(*)::integer`,
1516
+ conversations: sql6`COUNT(*)::integer`,
1195
1517
  date,
1196
1518
  visibility: juniorDestinations.visibility
1197
1519
  }).from(juniorConversations).leftJoin(
1198
1520
  juniorDestinations,
1199
- eq5(juniorDestinations.id, juniorConversations.destinationId)
1200
- ).where(and5(topLevelWhere(), gte2(juniorConversations.lastActivityAt, start))).groupBy(date, juniorDestinations.visibility);
1521
+ eq8(juniorDestinations.id, juniorConversations.destinationId)
1522
+ ).where(and7(topLevelWhere(), gte2(juniorConversations.lastActivityAt, start))).groupBy(date, juniorDestinations.visibility);
1201
1523
  }
1202
1524
  function directoryActivityDays(rows, nowMs) {
1203
1525
  const days = /* @__PURE__ */ new Map();
@@ -1269,9 +1591,8 @@ async function recentLocationRows(db, locationId) {
1269
1591
  conversationId: juniorConversations.conversationId,
1270
1592
  createdAt: juniorConversations.createdAt,
1271
1593
  destinationId: juniorDestinations.id,
1272
- destinationVisibility: juniorDestinations.visibility,
1273
1594
  durationMs: juniorConversations.durationMs,
1274
- email: sql4`COALESCE(
1595
+ email: sql6`COALESCE(
1275
1596
  ${juniorUsers.primaryEmailNormalized},
1276
1597
  ${juniorIdentities.email}
1277
1598
  )`,
@@ -1280,7 +1601,7 @@ async function recentLocationRows(db, locationId) {
1280
1601
  fullName: juniorUsers.displayName,
1281
1602
  handle: juniorIdentities.handle,
1282
1603
  lastActivityAt: juniorConversations.lastActivityAt,
1283
- providerSubjectId: sql4`CASE
1604
+ providerSubjectId: sql6`CASE
1284
1605
  WHEN ${juniorIdentities.provider} = 'slack'
1285
1606
  THEN ${juniorIdentities.providerSubjectId}
1286
1607
  ELSE NULL
@@ -1291,44 +1612,59 @@ async function recentLocationRows(db, locationId) {
1291
1612
  usage: juniorConversations.usage
1292
1613
  }).from(juniorConversations).innerJoin(
1293
1614
  juniorDestinations,
1294
- eq5(juniorDestinations.id, juniorConversations.destinationId)
1615
+ eq8(juniorDestinations.id, juniorConversations.destinationId)
1295
1616
  ).leftJoin(
1296
1617
  juniorIdentities,
1297
- eq5(juniorIdentities.id, juniorConversations.actorIdentityId)
1298
- ).leftJoin(juniorUsers, eq5(juniorUsers.id, juniorIdentities.userId)).where(
1299
- and5(
1618
+ eq8(juniorIdentities.id, juniorConversations.actorIdentityId)
1619
+ ).leftJoin(juniorUsers, eq8(juniorUsers.id, juniorIdentities.userId)).where(
1620
+ and7(
1300
1621
  publicLocationWhere(locationId),
1301
- isNull3(juniorConversations.archivedAt)
1622
+ isNull4(juniorConversations.archivedAt)
1302
1623
  )
1303
1624
  ).orderBy(
1304
1625
  desc2(juniorConversations.lastActivityAt),
1305
- asc2(juniorConversations.conversationId)
1626
+ asc3(juniorConversations.conversationId)
1306
1627
  ).limit(RECENT_LIMIT);
1307
1628
  }
1308
- async function readLocationDetailFromSql(locationId) {
1629
+ async function readLocationDetailFromSql(locationId, options = {}) {
1309
1630
  const nowMs = Date.now();
1310
1631
  const end = new Date(nowMs);
1311
1632
  end.setUTCHours(0, 0, 0, 0);
1312
1633
  const start = new Date(end);
1313
1634
  start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS - 1));
1314
1635
  const where = publicLocationWhere(locationId);
1315
- const activityDate = sql4`TO_CHAR(
1636
+ const activityDate = sql6`TO_CHAR(
1316
1637
  ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
1317
1638
  'YYYY-MM-DD'
1318
1639
  )`;
1319
1640
  const [locationRows, dayRows, actorRows, recentRows] = await Promise.all([
1320
1641
  getDb().select({
1321
1642
  ...locationColumns(),
1322
- ...conversationAggregateColumns(),
1643
+ ...treeAggregateColumns2,
1323
1644
  ...conversationRangeColumns()
1324
1645
  }).from(juniorConversations).innerJoin(
1646
+ treeConversation2,
1647
+ eq8(
1648
+ treeConversation2.rootConversationId,
1649
+ juniorConversations.conversationId
1650
+ )
1651
+ ).innerJoin(
1325
1652
  juniorDestinations,
1326
- eq5(juniorDestinations.id, juniorConversations.destinationId)
1653
+ eq8(juniorDestinations.id, juniorConversations.destinationId)
1327
1654
  ).where(where).groupBy(...locationGroupBy()),
1328
- getDb().select({ date: activityDate, ...conversationAggregateColumns() }).from(juniorConversations).innerJoin(
1655
+ getDb().select({
1656
+ date: activityDate,
1657
+ ...treeAggregateColumns2
1658
+ }).from(juniorConversations).innerJoin(
1659
+ treeConversation2,
1660
+ eq8(
1661
+ treeConversation2.rootConversationId,
1662
+ juniorConversations.conversationId
1663
+ )
1664
+ ).innerJoin(
1329
1665
  juniorDestinations,
1330
- eq5(juniorDestinations.id, juniorConversations.destinationId)
1331
- ).where(and5(where, gte2(juniorConversations.lastActivityAt, start))).groupBy(activityDate),
1666
+ eq8(juniorDestinations.id, juniorConversations.destinationId)
1667
+ ).where(and7(where, gte2(juniorConversations.lastActivityAt, start))).groupBy(activityDate),
1332
1668
  getDb().select({
1333
1669
  actorIdentityId: juniorConversations.actorIdentityId,
1334
1670
  email: juniorUsers.primaryEmailNormalized,
@@ -1337,14 +1673,20 @@ async function readLocationDetailFromSql(locationId) {
1337
1673
  identityEmail: juniorIdentities.email,
1338
1674
  identityProvider: juniorIdentities.provider,
1339
1675
  providerSubjectId: juniorIdentities.providerSubjectId,
1340
- ...conversationAggregateColumns()
1676
+ ...treeAggregateColumns2
1341
1677
  }).from(juniorConversations).innerJoin(
1678
+ treeConversation2,
1679
+ eq8(
1680
+ treeConversation2.rootConversationId,
1681
+ juniorConversations.conversationId
1682
+ )
1683
+ ).innerJoin(
1342
1684
  juniorDestinations,
1343
- eq5(juniorDestinations.id, juniorConversations.destinationId)
1685
+ eq8(juniorDestinations.id, juniorConversations.destinationId)
1344
1686
  ).leftJoin(
1345
1687
  juniorIdentities,
1346
- eq5(juniorIdentities.id, juniorConversations.actorIdentityId)
1347
- ).leftJoin(juniorUsers, eq5(juniorUsers.id, juniorIdentities.userId)).where(where).groupBy(
1688
+ eq8(juniorIdentities.id, juniorConversations.actorIdentityId)
1689
+ ).leftJoin(juniorUsers, eq8(juniorUsers.id, juniorIdentities.userId)).where(where).groupBy(
1348
1690
  juniorConversations.actorIdentityId,
1349
1691
  juniorUsers.primaryEmailNormalized,
1350
1692
  juniorUsers.displayName,
@@ -1384,6 +1726,15 @@ async function readLocationDetailFromSql(locationId) {
1384
1726
  actors.push(item);
1385
1727
  }
1386
1728
  const activity = activityDays(days, nowMs, ACTIVITY_DAYS);
1729
+ const recentConversationIds = recentRows.map((row) => row.conversationId);
1730
+ const [accessByConversation, metricsByRoot] = await Promise.all([
1731
+ readConversationAccessFromSql(
1732
+ getDb(),
1733
+ recentConversationIds,
1734
+ options.verifiedViewerEmail
1735
+ ),
1736
+ readRootConversationMetricsFromSql(getDb(), recentConversationIds)
1737
+ ]);
1387
1738
  return {
1388
1739
  ...location,
1389
1740
  activityDays: activity,
@@ -1391,7 +1742,12 @@ async function readLocationDetailFromSql(locationId) {
1391
1742
  (left, right) => right.conversations - left.conversations || left.label.localeCompare(right.label)
1392
1743
  ),
1393
1744
  generatedAt: new Date(nowMs).toISOString(),
1394
- recentConversations: recentRows.map(summaryFromRow),
1745
+ recentConversations: recentRows.map(
1746
+ (row) => summaryFromRow(row, {
1747
+ access: accessByConversation.get(row.conversationId),
1748
+ metrics: metricsByRoot.get(row.conversationId)
1749
+ })
1750
+ ),
1395
1751
  source: "conversation_index",
1396
1752
  windowEnd: end.toISOString(),
1397
1753
  windowStart: activity[0] ? `${activity[0].date}T00:00:00.000Z` : end.toISOString()
@@ -1399,19 +1755,25 @@ async function readLocationDetailFromSql(locationId) {
1399
1755
  }
1400
1756
 
1401
1757
  // src/api/locations/detail.ts
1402
- async function readLocationDetail(locationId) {
1403
- const report = await readLocationDetailFromSql(locationId);
1758
+ async function readLocationDetail(locationId, options = {}) {
1759
+ const report = await readLocationDetailFromSql(locationId, options);
1404
1760
  return report ? locationDetailReportSchema.parse(report) : void 0;
1405
1761
  }
1406
- var detail_default2 = {
1762
+ var detail_default2 = defineApiRoute({
1407
1763
  method: "get",
1408
1764
  path: "/:locationId",
1765
+ responseSchema: locationDetailReportSchema,
1409
1766
  handler: async (c) => {
1410
1767
  const { locationId } = parseParams(locationParamsSchema, c.req.param());
1411
- const report = await readLocationDetail(locationId);
1412
- return report ? Response.json(report) : Response.json({ error: "Location not found." }, { status: 404 });
1768
+ const verifiedViewerEmail = c.get("verifiedViewerEmail");
1769
+ const report = await readLocationDetail(
1770
+ locationId,
1771
+ verifiedViewerEmail ? { verifiedViewerEmail } : {}
1772
+ );
1773
+ if (!report) throwApiError(404, "Location not found.");
1774
+ return report;
1413
1775
  }
1414
- };
1776
+ });
1415
1777
 
1416
1778
  // src/api/locations/list.ts
1417
1779
  async function readLocationDirectory() {
@@ -1419,17 +1781,18 @@ async function readLocationDirectory() {
1419
1781
  await readLocationDirectoryFromSql()
1420
1782
  );
1421
1783
  }
1422
- var list_default2 = {
1784
+ var list_default2 = defineApiRoute({
1423
1785
  method: "get",
1424
1786
  path: "/",
1425
- handler: async () => Response.json(await readLocationDirectory())
1426
- };
1787
+ responseSchema: locationDirectoryReportSchema,
1788
+ handler: readLocationDirectory
1789
+ });
1427
1790
 
1428
1791
  // src/api/locations/routes.ts
1429
1792
  var routes2 = [list_default2, detail_default2];
1430
1793
  function createLocationRoutes() {
1431
1794
  const app = new Hono2();
1432
- for (const route of routes2) app.on(route.method, route.path, route.handler);
1795
+ registerApiRoutes(app, routes2);
1433
1796
  return app;
1434
1797
  }
1435
1798
 
@@ -1437,12 +1800,68 @@ function createLocationRoutes() {
1437
1800
  import { Hono as Hono3 } from "hono";
1438
1801
 
1439
1802
  // src/api/people/list.query.ts
1440
- import { and as and7, eq as eq7, gte as gte3, sql as sql6 } from "drizzle-orm";
1803
+ import { and as and9, eq as eq10, gte as gte3, sql as sql8 } from "drizzle-orm";
1441
1804
 
1442
1805
  // src/api/people/shared.ts
1443
- import { and as and6, asc as asc3, desc as desc3, eq as eq6, isNull as isNull4, sql as sql5 } from "drizzle-orm";
1806
+ import { and as and8, asc as asc4, desc as desc3, eq as eq9, isNull as isNull5, notExists, or, sql as sql7 } from "drizzle-orm";
1807
+ import { alias as alias5 } from "drizzle-orm/pg-core";
1444
1808
  var RECENT_LIMIT2 = 25;
1445
1809
  var ACTIVITY_DAYS2 = 365;
1810
+ var peopleTreeConversation = alias5(
1811
+ juniorConversations,
1812
+ "people_tree_conversation"
1813
+ );
1814
+ var peopleRootConversation = alias5(
1815
+ juniorConversations,
1816
+ "people_root_conversation"
1817
+ );
1818
+ var peopleRootIdentity = alias5(juniorIdentities, "people_root_identity");
1819
+ var peopleTreeAggregateColumns = conversationAggregateColumns({
1820
+ metrics: peopleTreeConversation,
1821
+ roots: juniorConversations
1822
+ });
1823
+ function peopleTreeMetricsJoin() {
1824
+ const rootOwnedByActor = getDb().select({ conversationId: peopleRootConversation.conversationId }).from(peopleRootConversation).innerJoin(
1825
+ peopleRootIdentity,
1826
+ eq9(peopleRootIdentity.id, peopleRootConversation.actorIdentityId)
1827
+ ).where(
1828
+ and8(
1829
+ eq9(
1830
+ peopleRootConversation.conversationId,
1831
+ juniorConversations.rootConversationId
1832
+ ),
1833
+ isNull5(peopleRootConversation.parentConversationId),
1834
+ eq9(
1835
+ peopleRootConversation.rootConversationId,
1836
+ peopleRootConversation.conversationId
1837
+ ),
1838
+ eq9(
1839
+ peopleRootIdentity.emailNormalized,
1840
+ juniorUsers.primaryEmailNormalized
1841
+ ),
1842
+ eq9(peopleRootIdentity.emailVerified, true)
1843
+ )
1844
+ );
1845
+ return and8(
1846
+ eq9(
1847
+ peopleTreeConversation.rootConversationId,
1848
+ juniorConversations.rootConversationId
1849
+ ),
1850
+ or(
1851
+ eq9(
1852
+ juniorConversations.rootConversationId,
1853
+ juniorConversations.conversationId
1854
+ ),
1855
+ and8(
1856
+ eq9(
1857
+ peopleTreeConversation.conversationId,
1858
+ juniorConversations.conversationId
1859
+ ),
1860
+ notExists(rootOwnedByActor)
1861
+ )
1862
+ )
1863
+ );
1864
+ }
1446
1865
  function normalizeEmail(email) {
1447
1866
  const normalized = email?.trim().toLowerCase();
1448
1867
  return normalized || void 0;
@@ -1484,11 +1903,11 @@ function statsItems2(map) {
1484
1903
  }
1485
1904
  function verifiedActorWhere(email) {
1486
1905
  const normalizedEmail = normalizeEmail(email);
1487
- return and6(
1488
- eq6(juniorIdentities.provider, "slack"),
1489
- eq6(juniorIdentities.emailVerified, true),
1490
- sql5`${juniorUsers.primaryEmailNormalized} IS NOT NULL`,
1491
- normalizedEmail ? eq6(juniorUsers.primaryEmailNormalized, normalizedEmail) : void 0
1906
+ return and8(
1907
+ eq9(juniorIdentities.provider, "slack"),
1908
+ eq9(juniorIdentities.emailVerified, true),
1909
+ sql7`${juniorUsers.primaryEmailNormalized} IS NOT NULL`,
1910
+ normalizedEmail ? eq9(juniorUsers.primaryEmailNormalized, normalizedEmail) : void 0
1492
1911
  );
1493
1912
  }
1494
1913
  async function recentActorRows(email) {
@@ -1497,7 +1916,6 @@ async function recentActorRows(email) {
1497
1916
  conversationId: juniorConversations.conversationId,
1498
1917
  createdAt: juniorConversations.createdAt,
1499
1918
  destinationId: juniorDestinations.id,
1500
- destinationVisibility: juniorDestinations.visibility,
1501
1919
  durationMs: juniorConversations.durationMs,
1502
1920
  email: juniorUsers.primaryEmailNormalized,
1503
1921
  executionStatus: juniorConversations.executionStatus,
@@ -1512,15 +1930,15 @@ async function recentActorRows(email) {
1512
1930
  usage: juniorConversations.usage
1513
1931
  }).from(juniorConversations).innerJoin(
1514
1932
  juniorIdentities,
1515
- eq6(juniorIdentities.id, juniorConversations.actorIdentityId)
1516
- ).innerJoin(juniorUsers, eq6(juniorUsers.id, juniorIdentities.userId)).leftJoin(
1933
+ eq9(juniorIdentities.id, juniorConversations.actorIdentityId)
1934
+ ).innerJoin(juniorUsers, eq9(juniorUsers.id, juniorIdentities.userId)).leftJoin(
1517
1935
  juniorDestinations,
1518
- eq6(juniorDestinations.id, juniorConversations.destinationId)
1936
+ eq9(juniorDestinations.id, juniorConversations.destinationId)
1519
1937
  ).where(
1520
- and6(verifiedActorWhere(email), isNull4(juniorConversations.archivedAt))
1938
+ and8(verifiedActorWhere(email), isNull5(juniorConversations.archivedAt))
1521
1939
  ).orderBy(
1522
1940
  desc3(juniorConversations.lastActivityAt),
1523
- asc3(juniorConversations.conversationId)
1941
+ asc4(juniorConversations.conversationId)
1524
1942
  ).limit(RECENT_LIMIT2);
1525
1943
  }
1526
1944
 
@@ -1546,7 +1964,7 @@ function directoryActivityDays2(rows, nowMs) {
1546
1964
  async function readPeopleListFromSql() {
1547
1965
  const nowMs = Date.now();
1548
1966
  const { end, start } = activityWindow(nowMs);
1549
- const activityDate = sql6`TO_CHAR(
1967
+ const activityDate = sql8`TO_CHAR(
1550
1968
  ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
1551
1969
  'YYYY-MM-DD'
1552
1970
  )`;
@@ -1554,24 +1972,24 @@ async function readPeopleListFromSql() {
1554
1972
  getDb().select({
1555
1973
  email: juniorUsers.primaryEmailNormalized,
1556
1974
  fullName: juniorUsers.displayName,
1557
- slackUserId: sql6`MAX(${juniorIdentities.providerSubjectId})`,
1558
- slackUserName: sql6`MAX(${juniorIdentities.handle})`,
1975
+ slackUserId: sql8`MAX(${juniorIdentities.providerSubjectId})`,
1976
+ slackUserName: sql8`MAX(${juniorIdentities.handle})`,
1559
1977
  activeDays: conversationActiveDaysColumn(),
1560
- ...conversationAggregateColumns(),
1978
+ ...peopleTreeAggregateColumns,
1561
1979
  ...conversationRangeColumns()
1562
1980
  }).from(juniorConversations).innerJoin(
1563
1981
  juniorIdentities,
1564
- eq7(juniorIdentities.id, juniorConversations.actorIdentityId)
1565
- ).innerJoin(juniorUsers, eq7(juniorUsers.id, juniorIdentities.userId)).where(verifiedActorWhere()).groupBy(juniorUsers.primaryEmailNormalized, juniorUsers.displayName),
1982
+ eq10(juniorIdentities.id, juniorConversations.actorIdentityId)
1983
+ ).innerJoin(juniorUsers, eq10(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, peopleTreeMetricsJoin()).where(verifiedActorWhere()).groupBy(juniorUsers.primaryEmailNormalized, juniorUsers.displayName),
1566
1984
  getDb().select({
1567
- activePeople: sql6`COUNT(DISTINCT ${juniorUsers.id})::int`,
1568
- conversations: sql6`COUNT(*)::int`,
1985
+ activePeople: sql8`COUNT(DISTINCT ${juniorUsers.id})::int`,
1986
+ conversations: sql8`COUNT(*)::int`,
1569
1987
  date: activityDate
1570
1988
  }).from(juniorConversations).innerJoin(
1571
1989
  juniorIdentities,
1572
- eq7(juniorIdentities.id, juniorConversations.actorIdentityId)
1573
- ).innerJoin(juniorUsers, eq7(juniorUsers.id, juniorIdentities.userId)).where(
1574
- and7(
1990
+ eq10(juniorIdentities.id, juniorConversations.actorIdentityId)
1991
+ ).innerJoin(juniorUsers, eq10(juniorUsers.id, juniorIdentities.userId)).where(
1992
+ and9(
1575
1993
  verifiedActorWhere(),
1576
1994
  gte3(juniorConversations.lastActivityAt, start)
1577
1995
  )
@@ -1612,14 +2030,15 @@ async function readPeopleListFromSql() {
1612
2030
  async function readPeopleList() {
1613
2031
  return actorDirectoryReportSchema.parse(await readPeopleListFromSql());
1614
2032
  }
1615
- var list_default3 = {
2033
+ var list_default3 = defineApiRoute({
1616
2034
  method: "get",
1617
2035
  path: "/",
1618
- handler: async () => Response.json(await readPeopleList())
1619
- };
2036
+ responseSchema: actorDirectoryReportSchema,
2037
+ handler: readPeopleList
2038
+ });
1620
2039
 
1621
2040
  // src/api/people/profile.query.ts
1622
- import { and as and8, eq as eq8, gte as gte4, sql as sql7 } from "drizzle-orm";
2041
+ import { and as and10, eq as eq11, gte as gte4, sql as sql9 } from "drizzle-orm";
1623
2042
  function emptyProfile(email, nowMs) {
1624
2043
  const end = new Date(nowMs);
1625
2044
  end.setUTCHours(0, 0, 0, 0);
@@ -1654,7 +2073,7 @@ function addAggregate2(map, label, row) {
1654
2073
  map.set(label, item);
1655
2074
  }
1656
2075
  function surfaceExpression() {
1657
- return sql7`CASE
2076
+ return sql9`CASE
1658
2077
  WHEN ${juniorConversations.source} IN ('api', 'scheduler', 'slack')
1659
2078
  THEN ${juniorConversations.source}
1660
2079
  WHEN ${juniorConversations.conversationId} LIKE 'slack:%' THEN 'slack'
@@ -1672,7 +2091,7 @@ function locationLabel2(row) {
1672
2091
  channelName: row.channelName ?? void 0
1673
2092
  }) ?? "Conversation";
1674
2093
  }
1675
- async function readPeopleProfileFromSql(email) {
2094
+ async function readPeopleProfileFromSql(email, options = {}) {
1676
2095
  const nowMs = Date.now();
1677
2096
  const normalizedEmail = normalizeEmail(email);
1678
2097
  if (!normalizedEmail) return emptyProfile("", nowMs);
@@ -1681,53 +2100,54 @@ async function readPeopleProfileFromSql(email) {
1681
2100
  const start = new Date(end);
1682
2101
  start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS2 - 1));
1683
2102
  const where = verifiedActorWhere(normalizedEmail);
2103
+ const metricsJoin = peopleTreeMetricsJoin();
1684
2104
  const surface = surfaceExpression();
1685
- const activityDate = sql7`TO_CHAR(
2105
+ const activityDate = sql9`TO_CHAR(
1686
2106
  ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
1687
2107
  'YYYY-MM-DD'
1688
2108
  )`;
1689
- const channel = sql7`SPLIT_PART(${juniorConversations.conversationId}, ':', 2)`;
2109
+ const channel = sql9`SPLIT_PART(${juniorConversations.conversationId}, ':', 2)`;
1690
2110
  const [totalsRows, dayRows, locationRows, surfaceRows, recentRows] = await Promise.all([
1691
2111
  getDb().select({
1692
2112
  email: juniorUsers.primaryEmailNormalized,
1693
2113
  fullName: juniorUsers.displayName,
1694
- slackUserId: sql7`MAX(${juniorIdentities.providerSubjectId})`,
1695
- slackUserName: sql7`MAX(${juniorIdentities.handle})`,
2114
+ slackUserId: sql9`MAX(${juniorIdentities.providerSubjectId})`,
2115
+ slackUserName: sql9`MAX(${juniorIdentities.handle})`,
1696
2116
  activeDays: conversationActiveDaysColumn(),
1697
- ...conversationAggregateColumns()
2117
+ ...peopleTreeAggregateColumns
1698
2118
  }).from(juniorConversations).innerJoin(
1699
2119
  juniorIdentities,
1700
- eq8(juniorIdentities.id, juniorConversations.actorIdentityId)
1701
- ).innerJoin(juniorUsers, eq8(juniorUsers.id, juniorIdentities.userId)).where(where).groupBy(juniorUsers.primaryEmailNormalized, juniorUsers.displayName),
2120
+ eq11(juniorIdentities.id, juniorConversations.actorIdentityId)
2121
+ ).innerJoin(juniorUsers, eq11(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(where).groupBy(juniorUsers.primaryEmailNormalized, juniorUsers.displayName),
1702
2122
  getDb().select({
1703
2123
  date: activityDate,
1704
- ...conversationAggregateColumns()
2124
+ ...peopleTreeAggregateColumns
1705
2125
  }).from(juniorConversations).innerJoin(
1706
2126
  juniorIdentities,
1707
- eq8(juniorIdentities.id, juniorConversations.actorIdentityId)
1708
- ).innerJoin(juniorUsers, eq8(juniorUsers.id, juniorIdentities.userId)).where(and8(where, gte4(juniorConversations.lastActivityAt, start))).groupBy(activityDate),
2127
+ eq11(juniorIdentities.id, juniorConversations.actorIdentityId)
2128
+ ).innerJoin(juniorUsers, eq11(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(and10(where, gte4(juniorConversations.lastActivityAt, start))).groupBy(activityDate),
1709
2129
  getDb().select({
1710
2130
  channel,
1711
2131
  channelName: juniorConversations.channelName,
1712
2132
  destinationVisibility: juniorDestinations.visibility,
1713
2133
  surface,
1714
- ...conversationAggregateColumns()
2134
+ ...peopleTreeAggregateColumns
1715
2135
  }).from(juniorConversations).innerJoin(
1716
2136
  juniorIdentities,
1717
- eq8(juniorIdentities.id, juniorConversations.actorIdentityId)
1718
- ).innerJoin(juniorUsers, eq8(juniorUsers.id, juniorIdentities.userId)).leftJoin(
2137
+ eq11(juniorIdentities.id, juniorConversations.actorIdentityId)
2138
+ ).innerJoin(juniorUsers, eq11(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).leftJoin(
1719
2139
  juniorDestinations,
1720
- eq8(juniorDestinations.id, juniorConversations.destinationId)
2140
+ eq11(juniorDestinations.id, juniorConversations.destinationId)
1721
2141
  ).where(where).groupBy(
1722
2142
  channel,
1723
2143
  juniorConversations.channelName,
1724
2144
  juniorDestinations.visibility,
1725
2145
  surface
1726
2146
  ),
1727
- getDb().select({ surface, ...conversationAggregateColumns() }).from(juniorConversations).innerJoin(
2147
+ getDb().select({ surface, ...peopleTreeAggregateColumns }).from(juniorConversations).innerJoin(
1728
2148
  juniorIdentities,
1729
- eq8(juniorIdentities.id, juniorConversations.actorIdentityId)
1730
- ).innerJoin(juniorUsers, eq8(juniorUsers.id, juniorIdentities.userId)).where(where).groupBy(surface),
2149
+ eq11(juniorIdentities.id, juniorConversations.actorIdentityId)
2150
+ ).innerJoin(juniorUsers, eq11(juniorUsers.id, juniorIdentities.userId)).leftJoin(peopleTreeConversation, metricsJoin).where(where).groupBy(surface),
1731
2151
  recentActorRows(normalizedEmail)
1732
2152
  ]);
1733
2153
  const totalsRow = totalsRows[0];
@@ -1761,11 +2181,25 @@ async function readPeopleProfileFromSql(email) {
1761
2181
  row
1762
2182
  );
1763
2183
  }
2184
+ const recentConversationIds = recentRows.map((row) => row.conversationId);
2185
+ const [accessByConversation, metricsByRoot] = await Promise.all([
2186
+ readConversationAccessFromSql(
2187
+ getDb(),
2188
+ recentConversationIds,
2189
+ options.verifiedViewerEmail
2190
+ ),
2191
+ readRootConversationMetricsFromSql(getDb(), recentConversationIds)
2192
+ ]);
1764
2193
  return {
1765
2194
  activityDays: activityDays2(days, nowMs),
1766
2195
  generatedAt: new Date(nowMs).toISOString(),
1767
2196
  locations: statsItems2(locations),
1768
- recentConversations: recentRows.map(summaryFromRow),
2197
+ recentConversations: recentRows.map(
2198
+ (row) => summaryFromRow(row, {
2199
+ access: accessByConversation.get(row.conversationId),
2200
+ metrics: metricsByRoot.get(row.conversationId)
2201
+ })
2202
+ ),
1769
2203
  actor,
1770
2204
  source: "conversation_index",
1771
2205
  surfaces: statsItems2(surfaces),
@@ -1783,23 +2217,30 @@ async function readPeopleProfileFromSql(email) {
1783
2217
  }
1784
2218
 
1785
2219
  // src/api/people/profile.ts
1786
- async function readPeopleProfile(email) {
1787
- return actorProfileReportSchema.parse(await readPeopleProfileFromSql(email));
2220
+ async function readPeopleProfile(email, options = {}) {
2221
+ return actorProfileReportSchema.parse(
2222
+ await readPeopleProfileFromSql(email, options)
2223
+ );
1788
2224
  }
1789
- var profile_default = {
2225
+ var profile_default = defineApiRoute({
1790
2226
  method: "get",
1791
2227
  path: "/:email",
2228
+ responseSchema: actorProfileReportSchema,
1792
2229
  handler: async (c) => {
1793
2230
  const { email } = parseParams(personParamsSchema, c.req.param());
1794
- return Response.json(await readPeopleProfile(email));
2231
+ const verifiedViewerEmail = c.get("verifiedViewerEmail");
2232
+ return readPeopleProfile(
2233
+ email,
2234
+ verifiedViewerEmail ? { verifiedViewerEmail } : {}
2235
+ );
1795
2236
  }
1796
- };
2237
+ });
1797
2238
 
1798
2239
  // src/api/people/routes.ts
1799
2240
  var routes3 = [list_default3, profile_default];
1800
2241
  function createPeopleRoutes() {
1801
2242
  const app = new Hono3();
1802
- for (const route of routes3) app.on(route.method, route.path, route.handler);
2243
+ registerApiRoutes(app, routes3);
1803
2244
  return app;
1804
2245
  }
1805
2246
 
@@ -1815,7 +2256,7 @@ function readDescriptionText(home) {
1815
2256
  }
1816
2257
  }
1817
2258
  async function readSkillReports() {
1818
- const { discoverSkills } = await import("./skills-ITNHBARB.js");
2259
+ const { discoverSkills } = await import("./skills-AR4AF32L.js");
1819
2260
  const skills = await discoverSkills();
1820
2261
  return skillReportsSchema.parse(
1821
2262
  skills.map((skill) => ({
@@ -1825,7 +2266,7 @@ async function readSkillReports() {
1825
2266
  );
1826
2267
  }
1827
2268
  async function readPluginReports() {
1828
- const { pluginCatalogRuntime } = await import("./catalog-runtime-FOFNPR3P.js");
2269
+ const { pluginCatalogRuntime } = await import("./catalog-runtime-4GI22PXK.js");
1829
2270
  return pluginReportsSchema.parse(
1830
2271
  pluginCatalogRuntime.getProviders().map((plugin) => ({
1831
2272
  name: plugin.manifest.name
@@ -1835,7 +2276,7 @@ async function readPluginReports() {
1835
2276
  async function readRuntimeInfoReport() {
1836
2277
  const [{ homeDir }, { pluginCatalogRuntime }, plugins, skills] = await Promise.all([
1837
2278
  import("./discovery-RMHYSRL7.js"),
1838
- import("./catalog-runtime-FOFNPR3P.js"),
2279
+ import("./catalog-runtime-4GI22PXK.js"),
1839
2280
  readPluginReports(),
1840
2281
  readSkillReports()
1841
2282
  ]);
@@ -1851,7 +2292,7 @@ async function readRuntimeInfoReport() {
1851
2292
  }
1852
2293
  async function readPluginOperationalReportFeed() {
1853
2294
  const nowMs = Date.now();
1854
- const { getPluginOperationalReports } = await import("./agent-hooks-NU5HK3PS.js");
2295
+ const { getPluginOperationalReports } = await import("./agent-hooks-ELT4ODFF.js");
1855
2296
  return pluginOperationalReportFeedSchema.parse({
1856
2297
  source: "plugins",
1857
2298
  generatedAt: new Date(nowMs).toISOString(),
@@ -1860,28 +2301,56 @@ async function readPluginOperationalReportFeed() {
1860
2301
  }
1861
2302
 
1862
2303
  // src/api.ts
2304
+ var routes4 = [
2305
+ defineApiRoute({
2306
+ method: "get",
2307
+ path: "/api/health",
2308
+ responseSchema: healthReportSchema,
2309
+ handler: readHealthReport
2310
+ }),
2311
+ defineApiRoute({
2312
+ method: "get",
2313
+ path: "/api/runtime",
2314
+ responseSchema: runtimeInfoReportSchema,
2315
+ handler: readRuntimeInfoReport
2316
+ }),
2317
+ defineApiRoute({
2318
+ method: "get",
2319
+ path: "/api/plugins",
2320
+ responseSchema: pluginReportsSchema,
2321
+ handler: readPluginReports
2322
+ }),
2323
+ defineApiRoute({
2324
+ method: "get",
2325
+ path: "/api/skills",
2326
+ responseSchema: skillReportsSchema,
2327
+ handler: readSkillReports
2328
+ }),
2329
+ defineApiRoute({
2330
+ method: "get",
2331
+ path: "/api/plugin-reports",
2332
+ responseSchema: pluginOperationalReportFeedSchema,
2333
+ handler: readPluginOperationalReportFeed
2334
+ })
2335
+ ];
1863
2336
  function createJuniorApi() {
1864
2337
  const app = new Hono4();
1865
- app.get("/api/health", async () => {
1866
- return Response.json(await readHealthReport());
1867
- });
1868
- app.get("/api/runtime", async () => {
1869
- return Response.json(await readRuntimeInfoReport());
1870
- });
1871
- app.get("/api/plugins", async () => {
1872
- return Response.json(await readPluginReports());
1873
- });
1874
- app.get("/api/skills", async () => {
1875
- return Response.json(await readSkillReports());
1876
- });
1877
- app.get("/api/plugin-reports", async () => {
1878
- return Response.json(await readPluginOperationalReportFeed());
1879
- });
2338
+ registerApiRoutes(app, routes4);
1880
2339
  app.route("/api/conversations", createConversationRoutes());
1881
2340
  app.route("/api/people", createPeopleRoutes());
1882
2341
  app.route("/api/locations", createLocationRoutes());
2342
+ app.notFound(
2343
+ () => jsonResponse(
2344
+ apiErrorSchema,
2345
+ { error: "Resource not found." },
2346
+ {
2347
+ status: 404
2348
+ }
2349
+ )
2350
+ );
1883
2351
  return app;
1884
2352
  }
1885
2353
  export {
1886
- createJuniorApi
2354
+ createJuniorApi,
2355
+ jsonResponse
1887
2356
  };