@sentry/junior 0.109.0 → 0.111.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 (123) hide show
  1. package/dist/{agent-hooks-FHHVLNPA.js → agent-hooks-QQ2IWKOP.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 +1 -1
  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} +31 -3
  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 +767 -384
  35. package/dist/app.js +591 -447
  36. package/dist/build/virtual-config.d.ts +2 -0
  37. package/dist/catalog-runtime-D22W6QEJ.js +16 -0
  38. package/dist/chat/agent/request.d.ts +3 -6
  39. package/dist/chat/agent/resume.d.ts +11 -12
  40. package/dist/chat/agent/sandbox.d.ts +36 -11
  41. package/dist/chat/agent/tools.d.ts +5 -8
  42. package/dist/chat/config.d.ts +8 -1
  43. package/dist/chat/conversations/history.d.ts +3 -0
  44. package/dist/chat/conversations/sql/privacy.d.ts +3 -15
  45. package/dist/chat/pi/messages.d.ts +2 -2
  46. package/dist/chat/pi/sdk.d.ts +1 -1
  47. package/dist/chat/pi/sql-model-usage.d.ts +4 -1
  48. package/dist/chat/plugins/agent-hooks.d.ts +2 -2
  49. package/dist/chat/runtime/agent-continue-runner.d.ts +7 -3
  50. package/dist/chat/runtime/slack-runtime.d.ts +1 -2
  51. package/dist/chat/runtime/thread-state.d.ts +3 -7
  52. package/dist/chat/runtime/turn-preparation.d.ts +2 -2
  53. package/dist/chat/sandbox/command-result.d.ts +91 -0
  54. package/dist/chat/sandbox/errors.d.ts +3 -1
  55. package/dist/chat/sandbox/ref.d.ts +5 -0
  56. package/dist/chat/sandbox/sandbox.d.ts +19 -46
  57. package/dist/chat/sandbox/session.d.ts +16 -20
  58. package/dist/chat/sandbox/skill-sync.d.ts +2 -2
  59. package/dist/chat/sandbox/workspace.d.ts +14 -12
  60. package/dist/chat/services/auth-pause.d.ts +4 -0
  61. package/dist/chat/services/context-compaction.d.ts +1 -0
  62. package/dist/chat/services/turn-result.d.ts +3 -4
  63. package/dist/chat/task-execution/queue-signing.d.ts +10 -8
  64. package/dist/chat/task-execution/queue.d.ts +9 -7
  65. package/dist/chat/task-execution/slack-work.d.ts +9 -7
  66. package/dist/chat/task-execution/state.d.ts +75 -22
  67. package/dist/chat/task-execution/store.d.ts +34 -3
  68. package/dist/chat/task-execution/worker.d.ts +0 -1
  69. package/dist/chat/tool-support/normalize-result.d.ts +2 -2
  70. package/dist/chat/tool-support/pi-tool-adapter.d.ts +2 -2
  71. package/dist/chat/tool-support/structured-result.d.ts +1 -2
  72. package/dist/chat/tools/sandbox/file-uploads.d.ts +1 -1
  73. package/dist/chat/{runtime → tools/sandbox}/generated-artifacts.d.ts +3 -3
  74. package/dist/chat/tools/types.d.ts +1 -1
  75. package/dist/chat/tools/web/fetch-content.d.ts +5 -0
  76. package/dist/chat/tools/web/image-generate.d.ts +2 -1
  77. package/dist/chat/tools/web/search.d.ts +1 -1
  78. package/dist/{chunk-CEA3A3UA.js → chunk-2ZTTXTSZ.js} +24 -1
  79. package/dist/{chunk-IVVGFRQQ.js → chunk-42WRQLJ6.js} +10 -38
  80. package/dist/{chunk-O3JMSZLI.js → chunk-6Y5L4BOM.js} +3 -5
  81. package/dist/{chunk-HO5NT6OE.js → chunk-B45FPP2A.js} +306 -31
  82. package/dist/{chunk-WHMOBKT7.js → chunk-BTRNMS6A.js} +2 -2
  83. package/dist/{chunk-PLB7MRCQ.js → chunk-C4MZ3GP5.js} +1 -1
  84. package/dist/{chunk-HHVY4SMP.js → chunk-FPK7XY3F.js} +98 -92
  85. package/dist/{chunk-RGX4V75M.js → chunk-IFAHK54X.js} +3 -3
  86. package/dist/{chunk-ISMWX44P.js → chunk-J7APPQIJ.js} +2 -2
  87. package/dist/{chunk-TMBRFULF.js → chunk-JQFJO5WH.js} +7 -1
  88. package/dist/{chunk-FWJRXN4F.js → chunk-L6JPKANW.js} +3 -139
  89. package/dist/{chunk-7Q6WCISF.js → chunk-MIAKQNYX.js} +43 -31
  90. package/dist/chunk-MPVES2IC.js +44 -0
  91. package/dist/{chunk-JNAVF45E.js → chunk-NQLWOTY4.js} +47 -19
  92. package/dist/{chunk-IXKSQGIO.js → chunk-OOLU34J4.js} +8860 -9137
  93. package/dist/{chunk-OUHNWH46.js → chunk-PYKKM4MM.js} +1 -1
  94. package/dist/{chunk-MXW4GL53.js → chunk-PZELBSHC.js} +8 -9
  95. package/dist/{chunk-QY636HZU.js → chunk-QQIVLZW4.js} +96 -17
  96. package/dist/{chunk-72OMGPDS.js → chunk-SL3ZKNLQ.js} +19 -18
  97. package/dist/{chunk-3SXUV423.js → chunk-WSRBFHPX.js} +1 -1
  98. package/dist/{chunk-TT6GMXNA.js → chunk-XQAYFRWT.js} +2 -2
  99. package/dist/{chunk-MGUMR35T.js → chunk-Z2W4SJZ7.js} +1 -1
  100. package/dist/{chunk-OCD3OZRK.js → chunk-Z4G3WCP3.js} +1 -1
  101. package/dist/{chunk-FENLIFJD.js → chunk-ZRZ4SVWG.js} +69 -10
  102. package/dist/cli/chat.js +23 -24
  103. package/dist/cli/check.js +8 -8
  104. package/dist/cli/plugins.js +15 -16
  105. package/dist/cli/snapshot-warmup.js +11 -11
  106. package/dist/cli/upgrade.js +9 -9
  107. package/dist/db/schema/conversations.d.ts +17 -0
  108. package/dist/db/schema.d.ts +17 -0
  109. package/dist/{db-V3NU72O2.js → db-WXQOQESG.js} +8 -9
  110. package/dist/instrumentation.js +4 -4
  111. package/dist/nitro.js +15 -10
  112. package/dist/reporting-schema.d.ts +46 -0
  113. package/dist/{runner-JYBCQBLK.js → runner-4QY4XH4B.js} +23 -33
  114. package/dist/{skills-U6MDAST7.js → skills-P5MU4EBX.js} +8 -8
  115. package/dist/{validation-72N3JVRU.js → validation-PQF5OP6T.js} +8 -8
  116. package/dist/version.js +1 -1
  117. package/migrations/0007_thick_sugar_man.sql +23 -0
  118. package/migrations/meta/0007_snapshot.json +1013 -0
  119. package/migrations/meta/_journal.json +7 -0
  120. package/package.json +6 -6
  121. package/dist/catalog-runtime-DMGJSLV2.js +0 -16
  122. package/dist/chunk-OB42YVAE.js +0 -16
  123. package/dist/chunk-SYAQIR5Z.js +0 -53
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,53 +15,51 @@ import {
12
15
  locationDirectoryReportSchema,
13
16
  locationParamsSchema,
14
17
  personParamsSchema
15
- } from "./chunk-HHVY4SMP.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-FWJRXN4F.js";
24
+ } from "./chunk-L6JPKANW.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-RGX4V75M.js";
37
+ import "./chunk-IFAHK54X.js";
35
38
  import {
36
39
  decodeStoredConversationEvent,
37
40
  getDb,
38
41
  getSqlExecutor
39
- } from "./chunk-FENLIFJD.js";
42
+ } from "./chunk-ZRZ4SVWG.js";
40
43
  import {
41
44
  juniorConversationEvents,
42
45
  juniorConversations,
43
46
  juniorIdentities,
44
47
  juniorUsers
45
- } from "./chunk-TMBRFULF.js";
48
+ } from "./chunk-JQFJO5WH.js";
46
49
  import "./chunk-4ZNGQH7C.js";
47
- import "./chunk-JNAVF45E.js";
50
+ import "./chunk-NQLWOTY4.js";
48
51
  import {
49
52
  juniorDestinations
50
53
  } from "./chunk-VFUK3X5B.js";
51
54
  import {
52
55
  canExposeConversationPayload,
53
- parseSlackThreadId,
54
- resolveConversationPrivacy
55
- } from "./chunk-O3JMSZLI.js";
56
- import "./chunk-SYAQIR5Z.js";
57
- import "./chunk-ISMWX44P.js";
58
- import "./chunk-OB42YVAE.js";
56
+ parseSlackThreadId
57
+ } from "./chunk-6Y5L4BOM.js";
58
+ import "./chunk-J7APPQIJ.js";
59
+ import "./chunk-42WRQLJ6.js";
59
60
  import "./chunk-6GWA276C.js";
60
- import "./chunk-IVVGFRQQ.js";
61
- import "./chunk-OCD3OZRK.js";
61
+ import "./chunk-MPVES2IC.js";
62
+ import "./chunk-Z4G3WCP3.js";
62
63
  import "./chunk-MLKGABMK.js";
63
64
 
64
65
  // src/api.ts
@@ -67,32 +68,50 @@ import { Hono as Hono4 } from "hono";
67
68
  // src/api/conversations/routes.ts
68
69
  import { Hono } from "hono";
69
70
 
70
- // src/api/conversations/archive.ts
71
- import { and, eq, isNotNull } from "drizzle-orm";
72
- import { HTTPException as HTTPException2 } from "hono/http-exception";
73
- import { z } from "zod";
74
-
75
71
  // src/api/http.ts
76
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
+ }
77
83
  function parseParams(schema, params) {
78
84
  const result = schema.safeParse(params);
79
85
  if (result.success) return result.data;
80
- throw new HTTPException(400, {
81
- cause: result.error,
82
- message: "Invalid route parameters."
83
- });
86
+ return throwApiError(400, "Invalid route parameters.", result.error);
84
87
  }
85
88
  function parseQuery(schema, query) {
86
89
  const result = schema.safeParse(query);
87
90
  if (result.success) return result.data;
88
- throw new HTTPException(400, {
89
- cause: result.error,
90
- message: "Invalid query parameters."
91
- });
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
+ }
92
111
  }
93
112
 
94
113
  // src/api/conversations/archive.ts
95
- var archiveBodySchema = z.object({ archived: z.boolean(), lastSeenAt: z.string().datetime() }).strict();
114
+ import { and, eq, isNotNull } from "drizzle-orm";
96
115
  async function archiveIfUnchanged(args) {
97
116
  const db = getDb();
98
117
  const rows = await db.update(juniorConversations).set({ archivedAt: args.archived ? /* @__PURE__ */ new Date() : null }).where(
@@ -110,9 +129,10 @@ async function archiveIfUnchanged(args) {
110
129
  if (!args.archived && existing.archivedAt === null) return "updated";
111
130
  return "conflict";
112
131
  }
113
- var archive_default = {
132
+ var archive_default = defineApiRoute({
114
133
  method: "patch",
115
134
  path: "/:conversationId/archive",
135
+ responseSchema: archiveConversationResponseSchema,
116
136
  handler: async (c) => {
117
137
  const { conversationId } = parseParams(
118
138
  conversationParamsSchema,
@@ -122,35 +142,22 @@ var archive_default = {
122
142
  try {
123
143
  input = await c.req.json();
124
144
  } catch (error) {
125
- throw new HTTPException2(400, {
126
- cause: error,
127
- message: "Invalid request body."
128
- });
129
- }
130
- const parsed = archiveBodySchema.safeParse(input);
131
- if (!parsed.success) {
132
- throw new HTTPException2(400, {
133
- cause: parsed.error,
134
- message: "Invalid request body."
135
- });
145
+ throwApiError(400, "Invalid request body.", error);
136
146
  }
137
- const body = parsed.data;
147
+ const body = parseBody(archiveConversationBodySchema, input);
138
148
  const result = await archiveIfUnchanged({ ...body, conversationId });
139
149
  if (result === "not_found") {
140
- return Response.json(
141
- { error: "Conversation not found." },
142
- { status: 404 }
143
- );
150
+ throwApiError(404, "Conversation not found.");
144
151
  }
145
152
  if (result === "conflict") {
146
- return Response.json(
147
- { error: "Conversation received new activity." },
148
- { status: 409 }
149
- );
153
+ throwApiError(409, "Conversation received new activity.");
150
154
  }
151
- return Response.json({ archived: body.archived });
155
+ return { archived: body.archived };
152
156
  }
153
- };
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";
154
161
 
155
162
  // src/chat/pi/sql-model-usage.ts
156
163
  import { and as and2, eq as eq2, sql } from "drizzle-orm";
@@ -219,7 +226,7 @@ function usageFromRow(row) {
219
226
  };
220
227
  return hasAgentTurnUsage(result) ? agentTurnUsageSchema.parse(result) : void 0;
221
228
  }
222
- async function readConversationModelUsageFromSql(executor, conversationId) {
229
+ async function readConversationModelUsageFromSql(executor, options) {
223
230
  const modelId = sql`concat(${message}->>'provider', '/', ${message}->>'model')`;
224
231
  const inputTokens = token("input", "inputTokens");
225
232
  const outputTokens = token("output", "outputTokens");
@@ -245,9 +252,15 @@ async function readConversationModelUsageFromSql(executor, conversationId) {
245
252
  costCacheRead: summedCost(nonnegativeNumber(cost, "cacheRead")),
246
253
  costCacheWrite: summedCost(nonnegativeNumber(cost, "cacheWrite")),
247
254
  costTotal: summedCost(nonnegativeNumber(cost, "total"))
248
- }).from(juniorConversationEvents).where(
255
+ }).from(juniorConversationEvents).innerJoin(
256
+ juniorConversations,
257
+ eq2(
258
+ juniorConversations.conversationId,
259
+ juniorConversationEvents.conversationId
260
+ )
261
+ ).where(
249
262
  and2(
250
- eq2(juniorConversationEvents.conversationId, conversationId),
263
+ options.includeDescendants ? eq2(juniorConversations.rootConversationId, options.conversationId) : eq2(juniorConversationEvents.conversationId, options.conversationId),
251
264
  eq2(juniorConversationEvents.type, "agent_step"),
252
265
  sql`${message}->>'role' = 'assistant'`,
253
266
  sql`jsonb_typeof(${message}->'provider') = 'string'`,
@@ -263,7 +276,7 @@ async function readConversationModelUsageFromSql(executor, conversationId) {
263
276
  }
264
277
 
265
278
  // src/api/conversations/events.ts
266
- import { z as z2 } from "zod";
279
+ import { z } from "zod";
267
280
  var conversationReportSourceEventTypes = [
268
281
  "message",
269
282
  "message_handled",
@@ -277,31 +290,31 @@ var conversationReportSourceEventTypes = [
277
290
  "subagent_started",
278
291
  "subagent_ended"
279
292
  ];
280
- var reportingAssistantMessageSchema = z2.object({
281
- role: z2.literal("assistant"),
282
- content: z2.array(z2.unknown())
293
+ var reportingAssistantMessageSchema = z.object({
294
+ role: z.literal("assistant"),
295
+ content: z.array(z.unknown())
283
296
  }).passthrough();
284
- var reportingToolCallPartSchema = z2.object({
285
- type: z2.literal("toolCall"),
286
- id: z2.string().min(1),
287
- name: z2.string().min(1),
288
- arguments: z2.unknown()
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()
289
302
  }).passthrough();
290
- var reportingToolResultMessageSchema = z2.object({
291
- role: z2.literal("toolResult"),
292
- toolCallId: z2.string().min(1),
293
- content: z2.array(z2.unknown()),
294
- details: z2.unknown().optional(),
295
- isError: z2.boolean().optional()
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()
296
309
  }).passthrough();
297
- var reportingToolResultDetailsSchema = z2.object({
298
- ok: z2.boolean().optional(),
299
- status: z2.string().optional()
310
+ var reportingToolResultDetailsSchema = z.object({
311
+ ok: z.boolean().optional(),
312
+ status: z.string().optional()
300
313
  }).passthrough();
301
- var reportingTextPartSchema = z2.object({ type: z2.literal("text"), text: z2.string() }).passthrough();
302
- var reportingMediaPartSchema = z2.object({
303
- type: z2.enum(["image", "audio"]),
304
- mimeType: z2.string().min(1).optional()
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()
305
318
  }).passthrough();
306
319
  function sanitizeModelContentPart(value) {
307
320
  const text = reportingTextPartSchema.safeParse(value);
@@ -429,6 +442,9 @@ function projectConversationReportEvents(args) {
429
442
  } else if (event.data.type === "handoff") {
430
443
  data = {
431
444
  type: "handoff",
445
+ modelProfile: event.data.modelProfile,
446
+ modelId: event.data.modelId,
447
+ ...event.data.reasoningLevel ? { reasoningLevel: event.data.reasoningLevel } : {},
432
448
  ...event.data.triggeringToolCallId ? { triggeringToolCallId: event.data.triggeringToolCallId } : {}
433
449
  };
434
450
  } else {
@@ -450,7 +466,7 @@ function projectConversationReportEvents(args) {
450
466
  }
451
467
 
452
468
  // src/api/conversations/list.ts
453
- 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";
454
470
 
455
471
  // src/api/conversations/shared.ts
456
472
  function slackStatsLocationLabel(input) {
@@ -521,16 +537,13 @@ function titleFromConversation(args) {
521
537
  threadId: args.conversation.conversationId,
522
538
  channelName: effectiveChannelName
523
539
  });
524
- const privateLabel = resolveConversationPrivacy({
525
- conversationId: args.conversation.conversationId,
526
- visibility: args.conversation.visibility
527
- }) !== "public" ? privateConversationLabel(slackConversation) : void 0;
540
+ const privateLabel = args.canViewPrivateContent ? void 0 : privateConversationLabel(slackConversation);
528
541
  return privateLabel ?? args.conversation.title ?? slackStatsLocationLabel({
529
542
  channel: slackThread?.channelId,
530
543
  channelName: effectiveChannelName
531
544
  }) ?? surfaceFallbackLabel(args.surface);
532
545
  }
533
- function channelNameFromConversation(conversation) {
546
+ function channelNameFromConversation(conversation, canViewPrivateContent) {
534
547
  const effectiveChannelName = conversation.channelName;
535
548
  const slackThread = parseSlackThreadId(conversation.conversationId);
536
549
  if (!effectiveChannelName && !slackThread) {
@@ -540,39 +553,45 @@ function channelNameFromConversation(conversation) {
540
553
  threadId: conversation.conversationId,
541
554
  channelName: effectiveChannelName
542
555
  });
543
- if (resolveConversationPrivacy({
544
- conversationId: conversation.conversationId,
545
- visibility: conversation.visibility
546
- }) !== "public") {
556
+ if (!canViewPrivateContent) {
547
557
  return privateConversationLabel(slackConversation);
548
558
  }
549
559
  return effectiveChannelName;
550
560
  }
551
- function channelNameRedactedFromConversation(conversation) {
561
+ function channelNameRedactedFromConversation(conversation, canViewPrivateContent) {
552
562
  const effectiveChannelName = conversation.channelName;
553
563
  const slackThread = parseSlackThreadId(conversation.conversationId);
554
564
  if (!effectiveChannelName && !slackThread) {
555
565
  return false;
556
566
  }
557
- return resolveConversationPrivacy({
558
- conversationId: conversation.conversationId,
559
- visibility: conversation.visibility
560
- }) !== "public";
567
+ return !canViewPrivateContent;
561
568
  }
562
569
  function conversationSummaryFromStoredConversation(args) {
563
- const { conversation, durationMs, usage: usage2 } = args;
570
+ const { conversation, durationMs, usage: usage3 } = args;
571
+ const canViewPrivateContent = args.access?.canViewPrivateContent ?? false;
564
572
  const surface = surfaceFromSource(
565
573
  conversation.source,
566
574
  conversation.conversationId
567
575
  );
568
576
  const actorIdentity = actorIdentityReport(conversation.actor);
569
577
  const slackThread = parseSlackThreadId(conversation.conversationId);
570
- const channelName = channelNameFromConversation(conversation);
571
- const channelNameRedacted = channelNameRedactedFromConversation(conversation);
578
+ const channelName = channelNameFromConversation(
579
+ conversation,
580
+ canViewPrivateContent
581
+ );
582
+ const channelNameRedacted = channelNameRedactedFromConversation(
583
+ conversation,
584
+ canViewPrivateContent
585
+ );
572
586
  return {
573
587
  conversationId: conversation.conversationId,
574
588
  cumulativeDurationMs: durationMs,
575
- displayTitle: titleFromConversation({ conversation, surface }),
589
+ displayTitle: titleFromConversation({
590
+ canViewPrivateContent,
591
+ conversation,
592
+ surface
593
+ }),
594
+ isParticipant: args.access?.isParticipant ?? false,
576
595
  lastProgressAt: new Date(
577
596
  conversation.execution.updatedAtMs ?? conversation.updatedAtMs
578
597
  ).toISOString(),
@@ -580,7 +599,7 @@ function conversationSummaryFromStoredConversation(args) {
580
599
  startedAt: new Date(conversation.createdAtMs).toISOString(),
581
600
  status: statusFromConversation(conversation),
582
601
  surface,
583
- ...usage2 ? { cumulativeUsage: usage2 } : {},
602
+ ...usage3 ? { cumulativeUsage: usage3 } : {},
584
603
  ...actorIdentity ? { actorIdentity } : {},
585
604
  ...conversation.archivedAtMs ? { archivedAt: new Date(conversation.archivedAtMs).toISOString() } : {},
586
605
  ...slackThread ? { channel: slackThread.channelId } : {},
@@ -590,6 +609,163 @@ function conversationSummaryFromStoredConversation(args) {
590
609
  };
591
610
  }
592
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
+
593
769
  // src/api/conversations/list.ts
594
770
  var CONVERSATION_FEED_LIMIT = 50;
595
771
  async function conversationRows(db, limit, actorEmail) {
@@ -606,17 +782,17 @@ async function conversationRows(db, limit, actorEmail) {
606
782
  userDisplayName: juniorUsers.displayName
607
783
  }).from(juniorConversations).leftJoin(
608
784
  juniorDestinations,
609
- eq3(juniorDestinations.id, juniorConversations.destinationId)
785
+ eq5(juniorDestinations.id, juniorConversations.destinationId)
610
786
  ).leftJoin(
611
787
  juniorIdentities,
612
- eq3(juniorIdentities.id, juniorConversations.actorIdentityId)
613
- ).leftJoin(juniorUsers, eq3(juniorUsers.id, juniorIdentities.userId)).where(
614
- and3(
615
- isNull(juniorConversations.parentConversationId),
616
- isNull(juniorConversations.archivedAt),
617
- actorEmail ? and3(
618
- eq3(juniorIdentities.emailNormalized, actorEmail),
619
- 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)
620
796
  ) : void 0
621
797
  )
622
798
  ).orderBy(
@@ -651,10 +827,7 @@ function conversationFromRow(row) {
651
827
  ...value.channelName ? { channelName: value.channelName } : {},
652
828
  ...value.source ? { source: value.source } : {},
653
829
  ...value.title ? { title: value.title } : {},
654
- ...value.transcriptPurgedAt ? { transcriptPurgedAtMs: value.transcriptPurgedAt.getTime() } : {},
655
- ...row.destinationVisibility ? {
656
- visibility: row.destinationVisibility === "public" ? "public" : "private"
657
- } : {}
830
+ ...value.transcriptPurgedAt ? { transcriptPurgedAtMs: value.transcriptPurgedAt.getTime() } : {}
658
831
  };
659
832
  }
660
833
  async function readConversationRecordFromSql(conversationId) {
@@ -672,76 +845,110 @@ async function readConversationRecordFromSql(conversationId) {
672
845
  userDisplayName: juniorUsers.displayName
673
846
  }).from(juniorConversations).leftJoin(
674
847
  juniorDestinations,
675
- eq3(juniorDestinations.id, juniorConversations.destinationId)
848
+ eq5(juniorDestinations.id, juniorConversations.destinationId)
676
849
  ).leftJoin(
677
850
  juniorIdentities,
678
- eq3(juniorIdentities.id, juniorConversations.actorIdentityId)
679
- ).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);
680
853
  const row = rows[0];
681
854
  return row ? {
682
855
  conversation: conversationFromRow(row),
683
856
  durationMs: row.conversation.durationMs,
684
857
  ...row.destinationVisibility === "public" && row.destinationId ? { locationId: row.destinationId } : {},
685
- usage: row.conversation.usage
858
+ usage: row.conversation.usage,
859
+ rootConversationId: row.conversation.rootConversationId
686
860
  } : void 0;
687
861
  }
688
862
  async function readConversationFeedFromSql(options = {}) {
689
863
  const nowMs = Date.now();
864
+ const db = getDb();
690
865
  const rows = await conversationRows(
691
- getDb(),
866
+ db,
692
867
  options.limit ?? CONVERSATION_FEED_LIMIT,
693
868
  options.actorEmail
694
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
+ ]);
695
879
  return {
696
- conversations: rows.map(
697
- (row) => conversationSummaryFromStoredConversation({
880
+ conversations: rows.map((row) => {
881
+ const metrics = metricsByRoot.get(row.conversation.conversationId);
882
+ return conversationSummaryFromStoredConversation({
698
883
  conversation: conversationFromRow(row),
699
- durationMs: row.conversation.durationMs,
884
+ access: accessByConversation.get(row.conversation.conversationId),
885
+ durationMs: metrics?.durationMs ?? row.conversation.durationMs,
700
886
  ...row.destinationVisibility === "public" && row.destinationId ? { locationId: row.destinationId } : {},
701
- usage: row.conversation.usage ?? void 0
702
- })
703
- ),
887
+ usage: metrics?.usage ?? row.conversation.usage ?? void 0
888
+ });
889
+ }),
704
890
  generatedAt: new Date(nowMs).toISOString(),
705
891
  source: "conversation_index"
706
892
  };
707
893
  }
708
894
  async function readConversationFeed(options = {}) {
709
895
  return conversationFeedSchema.parse(
710
- await readConversationFeedFromSql({ actorEmail: options.actorEmail })
896
+ await readConversationFeedFromSql(options)
711
897
  );
712
898
  }
713
- var list_default = {
899
+ var list_default = defineApiRoute({
714
900
  method: "get",
715
901
  path: "/",
902
+ responseSchema: conversationFeedSchema,
716
903
  handler: async (c) => {
717
904
  const { actorEmail } = parseQuery(
718
905
  conversationFeedQuerySchema,
719
906
  c.req.query()
720
907
  );
721
- return Response.json(await readConversationFeed({ actorEmail }));
908
+ const verifiedViewerEmail = c.get("verifiedViewerEmail");
909
+ return readConversationFeed({
910
+ ...actorEmail ? { actorEmail } : {},
911
+ ...verifiedViewerEmail ? { verifiedViewerEmail } : {}
912
+ });
722
913
  }
723
- };
914
+ });
724
915
 
725
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
+ }
726
940
  function projectConversationDetail(args) {
727
941
  const { conversation } = args;
728
942
  const conversationId = conversation.conversationId;
729
943
  const transcriptPurgedAtMs = conversation.transcriptPurgedAtMs;
730
- const { visibility: _storedVisibility, ...conversationWithoutVisibility } = conversation;
731
- const authorizedConversation = {
732
- ...conversationWithoutVisibility,
733
- ...args.effectiveVisibility ? { visibility: args.effectiveVisibility } : {}
734
- };
735
- const canExposePayload = canExposeConversationPayload({
736
- conversationId: args.privacyConversationId ?? conversationId,
737
- visibility: args.effectiveVisibility
738
- });
944
+ const canExposePayload = args.access?.canViewPrivateContent ?? false;
739
945
  const events = transcriptPurgedAtMs === void 0 ? args.events : [];
740
946
  const modelUsage = transcriptPurgedAtMs === void 0 ? args.modelUsage : [];
741
947
  const sentryConversationUrl = buildSentryConversationUrl(conversationId);
742
948
  return {
743
949
  ...conversationSummaryFromStoredConversation({
744
- conversation: authorizedConversation,
950
+ access: args.access,
951
+ conversation,
745
952
  durationMs: args.durationMs,
746
953
  ...args.locationId ? { locationId: args.locationId } : {},
747
954
  usage: args.usage
@@ -759,19 +966,28 @@ function projectConversationDetail(args) {
759
966
  ...sentryConversationUrl ? { sentryConversationUrl } : {}
760
967
  };
761
968
  }
762
- async function readConversationDetailFromSql(conversationId) {
969
+ async function readConversationDetailFromSql(conversationId, options) {
763
970
  const record = await readConversationRecordFromSql(conversationId);
764
971
  if (!record) return void 0;
765
972
  const executor = getSqlExecutor();
766
- const [snapshot, modelUsage] = await Promise.all([
767
- 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, {
768
982
  conversationId,
769
- eventTypes: conversationReportSourceEventTypes
770
- }),
771
- record.conversation.transcriptPurgedAtMs === void 0 ? readConversationModelUsageFromSql(executor, conversationId) : Promise.resolve([])
983
+ includeDescendants: includeDescendantMetrics
984
+ }) : Promise.resolve([]),
985
+ readRootConversationMetricsFromSql(
986
+ getDb(),
987
+ includeDescendantMetrics ? [conversationId] : []
988
+ )
772
989
  ]);
773
- if (!snapshot) return void 0;
774
- const events = snapshot.events.map(
990
+ const events = eventRows.map(
775
991
  (row) => decodeStoredConversationEvent({
776
992
  schemaVersion: row.schemaVersion,
777
993
  seq: row.seq,
@@ -782,106 +998,121 @@ async function readConversationDetailFromSql(conversationId) {
782
998
  payload: row.payload
783
999
  })
784
1000
  );
785
- const effectiveVisibility = snapshot.visibility === "public" || snapshot.visibility === "private" ? snapshot.visibility : void 0;
1001
+ const metrics = metricsByRoot.get(conversationId);
786
1002
  return projectConversationDetail({
787
1003
  ...record,
788
- effectiveVisibility,
1004
+ access: accessByConversation.get(conversationId),
1005
+ durationMs: metrics?.durationMs ?? record.durationMs,
789
1006
  events,
790
1007
  modelUsage,
791
- privacyConversationId: snapshot.rootConversationId,
792
- usage: record.usage ?? void 0
1008
+ usage: metrics?.usage ?? record.usage ?? void 0
793
1009
  });
794
1010
  }
795
- async function readConversationDetail(conversationId) {
796
- const report = await readConversationDetailFromSql(conversationId);
1011
+ async function readConversationDetail(conversationId, options = {}) {
1012
+ const report = await readConversationDetailFromSql(conversationId, options);
797
1013
  return report ? conversationDetailReportSchema.parse(report) : void 0;
798
1014
  }
799
- var detail_default = {
1015
+ var detail_default = defineApiRoute({
800
1016
  method: "get",
801
1017
  path: "/:conversationId",
1018
+ responseSchema: conversationDetailReportSchema,
802
1019
  handler: async (c) => {
803
1020
  const { conversationId } = parseParams(
804
1021
  conversationParamsSchema,
805
1022
  c.req.param()
806
1023
  );
807
- const report = await readConversationDetail(conversationId);
808
- 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;
809
1031
  }
810
- };
1032
+ });
811
1033
 
812
1034
  // src/api/conversations/stats.query.ts
813
- 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";
814
1037
 
815
1038
  // src/api/conversations/aggregate.ts
816
- import { sql as sql2 } from "drizzle-orm";
817
- function tokenValue() {
818
- return sql2`
1039
+ import { sql as sql4 } from "drizzle-orm";
1040
+ function tokenValue(source) {
1041
+ return sql4`
819
1042
  CASE
820
- WHEN ${juniorConversations.usage}->>'totalTokens' IS NOT NULL
821
- THEN (${juniorConversations.usage}->>'totalTokens')::double precision
822
1043
  WHEN COALESCE(
823
- ${juniorConversations.usage}->>'inputTokens',
824
- ${juniorConversations.usage}->>'outputTokens',
825
- ${juniorConversations.usage}->>'cachedInputTokens',
826
- ${juniorConversations.usage}->>'cacheCreationTokens'
1044
+ ${source.usage}->>'inputTokens',
1045
+ ${source.usage}->>'outputTokens',
1046
+ ${source.usage}->>'cachedInputTokens',
1047
+ ${source.usage}->>'cacheCreationTokens'
827
1048
  ) IS NOT NULL
828
- THEN COALESCE((${juniorConversations.usage}->>'inputTokens')::double precision, 0)
829
- + COALESCE((${juniorConversations.usage}->>'outputTokens')::double precision, 0)
830
- + COALESCE((${juniorConversations.usage}->>'cachedInputTokens')::double precision, 0)
831
- + 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
832
1055
  ELSE NULL
833
1056
  END
834
1057
  `;
835
1058
  }
836
- function costValue() {
837
- return sql2`
1059
+ function costValue(source) {
1060
+ return sql4`
838
1061
  CASE
839
- WHEN ${juniorConversations.usage}->'cost'->>'total' IS NOT NULL
840
- THEN (${juniorConversations.usage}->'cost'->>'total')::double precision
1062
+ WHEN ${source.usage}->'cost'->>'total' IS NOT NULL
1063
+ THEN (${source.usage}->'cost'->>'total')::double precision
841
1064
  WHEN COALESCE(
842
- ${juniorConversations.usage}->'cost'->>'input',
843
- ${juniorConversations.usage}->'cost'->>'output',
844
- ${juniorConversations.usage}->'cost'->>'cacheRead',
845
- ${juniorConversations.usage}->'cost'->>'cacheWrite'
1065
+ ${source.usage}->'cost'->>'input',
1066
+ ${source.usage}->'cost'->>'output',
1067
+ ${source.usage}->'cost'->>'cacheRead',
1068
+ ${source.usage}->'cost'->>'cacheWrite'
846
1069
  ) IS NOT NULL
847
- THEN COALESCE((${juniorConversations.usage}->'cost'->>'input')::double precision, 0)
848
- + COALESCE((${juniorConversations.usage}->'cost'->>'output')::double precision, 0)
849
- + COALESCE((${juniorConversations.usage}->'cost'->>'cacheRead')::double precision, 0)
850
- + 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)
851
1074
  ELSE NULL
852
1075
  END
853
1076
  `;
854
1077
  }
855
- 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(*)`;
856
1082
  return {
857
- active: sql2`COUNT(*) FILTER (
858
- WHERE ${juniorConversations.executionStatus} NOT IN ('idle', 'failed')
1083
+ active: sql4`${conversationCount} FILTER (
1084
+ WHERE ${roots.executionStatus} NOT IN ('idle', 'failed')
859
1085
  )::integer`,
860
- conversations: sql2`COUNT(*)::integer`,
861
- costUsd: sql2`SUM(${costValue()})::double precision`,
862
- durationMs: sql2`COALESCE(SUM(${juniorConversations.durationMs}), 0)::double precision`,
863
- failed: sql2`COUNT(*) FILTER (
864
- 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'
865
1091
  )::integer`,
866
- tokens: sql2`SUM(${tokenValue()})::double precision`
1092
+ tokens: sql4`SUM(${tokenValue(metrics)})::double precision`
867
1093
  };
868
1094
  }
869
1095
  function conversationRangeColumns() {
870
1096
  return {
871
- firstSeenAt: sql2`MIN(${juniorConversations.createdAt})`.mapWith(
1097
+ firstSeenAt: sql4`MIN(${juniorConversations.createdAt})`.mapWith(
872
1098
  juniorConversations.createdAt
873
1099
  ),
874
- lastSeenAt: sql2`MAX(${juniorConversations.lastActivityAt})`.mapWith(
1100
+ lastSeenAt: sql4`MAX(${juniorConversations.lastActivityAt})`.mapWith(
875
1101
  juniorConversations.lastActivityAt
876
1102
  )
877
1103
  };
878
1104
  }
879
1105
  function conversationActiveDaysColumn() {
880
- 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`;
881
1107
  }
882
1108
 
883
1109
  // src/api/conversations/stats.query.ts
884
1110
  var WINDOW_DAYS = 90;
1111
+ var treeConversation = alias3(juniorConversations, "stats_tree_conversation");
1112
+ var treeAggregateColumns = conversationAggregateColumns({
1113
+ metrics: treeConversation,
1114
+ roots: juniorConversations
1115
+ });
885
1116
  function emptyStatsItem(label) {
886
1117
  return {
887
1118
  active: 0,
@@ -936,8 +1167,8 @@ function statsItems(map) {
936
1167
  );
937
1168
  }
938
1169
  function statsWhere(start, end) {
939
- return and4(
940
- isNull2(juniorConversations.parentConversationId),
1170
+ return and6(
1171
+ isNull3(juniorConversations.parentConversationId),
941
1172
  gte(juniorConversations.lastActivityAt, start),
942
1173
  lte(juniorConversations.lastActivityAt, end)
943
1174
  );
@@ -970,12 +1201,18 @@ function metricDays(rows, endMs) {
970
1201
  }
971
1202
  async function aggregateStats(db, start, end) {
972
1203
  const where = statsWhere(start, end);
973
- const activityDate = sql3`TO_CHAR(
1204
+ const activityDate = sql5`TO_CHAR(
974
1205
  ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
975
1206
  'YYYY-MM-DD'
976
1207
  )`;
977
1208
  const [totalsRows, actorRows, locationRows, metricRows] = await Promise.all([
978
- 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),
979
1216
  db.select({
980
1217
  identityDisplayName: juniorIdentities.displayName,
981
1218
  identityEmail: juniorIdentities.emailNormalized,
@@ -983,11 +1220,17 @@ async function aggregateStats(db, start, end) {
983
1220
  identitySubjectId: juniorIdentities.providerSubjectId,
984
1221
  userDisplayName: juniorUsers.displayName,
985
1222
  userEmail: juniorUsers.primaryEmailNormalized,
986
- ...conversationAggregateColumns()
987
- }).from(juniorConversations).leftJoin(
1223
+ ...treeAggregateColumns
1224
+ }).from(juniorConversations).innerJoin(
1225
+ treeConversation,
1226
+ eq7(
1227
+ treeConversation.rootConversationId,
1228
+ juniorConversations.conversationId
1229
+ )
1230
+ ).leftJoin(
988
1231
  juniorIdentities,
989
- eq4(juniorIdentities.id, juniorConversations.actorIdentityId)
990
- ).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(
991
1234
  juniorIdentities.displayName,
992
1235
  juniorIdentities.emailNormalized,
993
1236
  juniorIdentities.handle,
@@ -1002,10 +1245,16 @@ async function aggregateStats(db, start, end) {
1002
1245
  destinationProvider: juniorDestinations.provider,
1003
1246
  destinationVisibility: juniorDestinations.visibility,
1004
1247
  source: juniorConversations.source,
1005
- ...conversationAggregateColumns()
1006
- }).from(juniorConversations).leftJoin(
1248
+ ...treeAggregateColumns
1249
+ }).from(juniorConversations).innerJoin(
1250
+ treeConversation,
1251
+ eq7(
1252
+ treeConversation.rootConversationId,
1253
+ juniorConversations.conversationId
1254
+ )
1255
+ ).leftJoin(
1007
1256
  juniorDestinations,
1008
- eq4(juniorDestinations.id, juniorConversations.destinationId)
1257
+ eq7(juniorDestinations.id, juniorConversations.destinationId)
1009
1258
  ).where(where).groupBy(
1010
1259
  juniorConversations.channelName,
1011
1260
  juniorConversations.source,
@@ -1015,11 +1264,17 @@ async function aggregateStats(db, start, end) {
1015
1264
  juniorDestinations.visibility
1016
1265
  ),
1017
1266
  db.select({
1018
- costUsd: conversationAggregateColumns().costUsd,
1267
+ costUsd: treeAggregateColumns.costUsd,
1019
1268
  date: activityDate,
1020
- durationMs: conversationAggregateColumns().durationMs,
1021
- tokens: conversationAggregateColumns().tokens
1022
- }).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)
1023
1278
  ]);
1024
1279
  return { actorRows, locationRows, metricRows, totals: totalsRows[0] };
1025
1280
  }
@@ -1062,17 +1317,18 @@ async function readConversationStats() {
1062
1317
  await readConversationStatsFromSql()
1063
1318
  );
1064
1319
  }
1065
- var stats_default = {
1320
+ var stats_default = defineApiRoute({
1066
1321
  method: "get",
1067
1322
  path: "/stats",
1068
- handler: async () => Response.json(await readConversationStats())
1069
- };
1323
+ responseSchema: conversationStatsReportSchema,
1324
+ handler: readConversationStats
1325
+ });
1070
1326
 
1071
1327
  // src/api/conversations/routes.ts
1072
1328
  var routes = [list_default, stats_default, archive_default, detail_default];
1073
1329
  function createConversationRoutes() {
1074
1330
  const app = new Hono();
1075
- for (const route of routes) app.on(route.method, route.path, route.handler);
1331
+ registerApiRoutes(app, routes);
1076
1332
  return app;
1077
1333
  }
1078
1334
 
@@ -1080,28 +1336,10 @@ function createConversationRoutes() {
1080
1336
  import { Hono as Hono2 } from "hono";
1081
1337
 
1082
1338
  // src/api/locations/query.ts
1083
- 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";
1084
1341
 
1085
1342
  // src/api/conversations/reporting.ts
1086
- var PRIVATE_CONVERSATION_LABEL2 = "Private Conversation";
1087
- function channelFromConversationId(conversationId) {
1088
- const [provider, channel] = conversationId.split(":");
1089
- return provider === "slack" && channel ? channel : void 0;
1090
- }
1091
- function surfaceFromRow(row) {
1092
- if (row.source === "api" || row.source === "scheduler" || row.source === "slack") {
1093
- return row.source;
1094
- }
1095
- if (row.conversationId.startsWith("slack:")) return "slack";
1096
- if (row.conversationId.startsWith("scheduler:")) return "scheduler";
1097
- if (row.conversationId.startsWith("api:")) return "api";
1098
- return "internal";
1099
- }
1100
- function statusFromRow(row) {
1101
- if (row.executionStatus === "failed") return "failed";
1102
- if (row.executionStatus === "idle") return "completed";
1103
- return "active";
1104
- }
1105
1343
  function surfaceLabel2(surface) {
1106
1344
  if (surface === "scheduler") return "Scheduler";
1107
1345
  if (surface === "api") return "API";
@@ -1123,22 +1361,6 @@ function slackLocationLabel(args) {
1123
1361
  }
1124
1362
  return name || channelId;
1125
1363
  }
1126
- function channelNameFromRow(row) {
1127
- if (row.destinationVisibility !== "public") {
1128
- return PRIVATE_CONVERSATION_LABEL2;
1129
- }
1130
- return row.channelName ?? void 0;
1131
- }
1132
- function titleFromRow(row, surface) {
1133
- if (row.destinationVisibility !== "public") {
1134
- return PRIVATE_CONVERSATION_LABEL2;
1135
- }
1136
- const channel = channelFromConversationId(row.conversationId);
1137
- return row.title ?? slackLocationLabel({
1138
- channel,
1139
- channelName: row.channelName ?? void 0
1140
- }) ?? surfaceLabel2(surface);
1141
- }
1142
1364
  function actorFromRow(row) {
1143
1365
  const actor = {
1144
1366
  ...row.email ? { email: row.email } : {},
@@ -1148,34 +1370,42 @@ function actorFromRow(row) {
1148
1370
  };
1149
1371
  return Object.keys(actor).length ? actor : void 0;
1150
1372
  }
1151
- function summaryFromRow(row) {
1152
- const surface = surfaceFromRow(row);
1153
- const channel = channelFromConversationId(row.conversationId);
1154
- const channelName = channelNameFromRow(row);
1155
- const channelNameRedacted = row.destinationVisibility !== "public";
1156
- const actorIdentity = actorFromRow(row);
1157
- return {
1373
+ function summaryFromRow(row, options = {}) {
1374
+ const actor = actorFromRow(row);
1375
+ const conversation = {
1158
1376
  conversationId: row.conversationId,
1159
- cumulativeDurationMs: row.durationMs,
1160
- displayTitle: titleFromRow(row, surface),
1161
- lastProgressAt: new Date(
1162
- row.executionUpdatedAt ?? row.updatedAt
1163
- ).toISOString(),
1164
- lastSeenAt: row.lastActivityAt.toISOString(),
1165
- startedAt: row.createdAt.toISOString(),
1166
- status: statusFromRow(row),
1167
- surface,
1168
- ...actorIdentity ? { actorIdentity } : {},
1169
- ...channel ? { channel } : {},
1170
- ...channelName ? { channelName } : {},
1171
- ...channelNameRedacted ? { channelNameRedacted: true } : {},
1172
- ...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 } : {}
1173
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
+ });
1174
1396
  }
1175
1397
 
1176
1398
  // src/api/locations/query.ts
1177
1399
  var RECENT_LIMIT = 25;
1178
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
+ });
1179
1409
  function emptyMetrics() {
1180
1410
  return {
1181
1411
  active: 0,
@@ -1211,13 +1441,13 @@ function emptyActor(actor) {
1211
1441
  };
1212
1442
  }
1213
1443
  function topLevelWhere() {
1214
- return isNull3(juniorConversations.parentConversationId);
1444
+ return isNull4(juniorConversations.parentConversationId);
1215
1445
  }
1216
1446
  function publicLocationWhere(destinationId) {
1217
- return and5(
1447
+ return and7(
1218
1448
  topLevelWhere(),
1219
- eq5(juniorDestinations.id, destinationId),
1220
- eq5(juniorDestinations.visibility, "public")
1449
+ eq8(juniorDestinations.id, destinationId),
1450
+ eq8(juniorDestinations.visibility, "public")
1221
1451
  );
1222
1452
  }
1223
1453
  function locationColumns() {
@@ -1264,26 +1494,32 @@ function locationFromAggregate(row) {
1264
1494
  async function directoryRows(db) {
1265
1495
  return db.select({
1266
1496
  ...locationColumns(),
1267
- ...conversationAggregateColumns(),
1497
+ ...treeAggregateColumns2,
1268
1498
  ...conversationRangeColumns()
1269
- }).from(juniorConversations).leftJoin(
1499
+ }).from(juniorConversations).innerJoin(
1500
+ treeConversation2,
1501
+ eq8(
1502
+ treeConversation2.rootConversationId,
1503
+ juniorConversations.conversationId
1504
+ )
1505
+ ).leftJoin(
1270
1506
  juniorDestinations,
1271
- eq5(juniorDestinations.id, juniorConversations.destinationId)
1507
+ eq8(juniorDestinations.id, juniorConversations.destinationId)
1272
1508
  ).where(topLevelWhere()).groupBy(...locationGroupBy());
1273
1509
  }
1274
1510
  async function directoryActivityRows(db, start) {
1275
- const date = sql4`TO_CHAR(
1511
+ const date = sql6`TO_CHAR(
1276
1512
  ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
1277
1513
  'YYYY-MM-DD'
1278
1514
  )`;
1279
1515
  return db.select({
1280
- conversations: sql4`COUNT(*)::integer`,
1516
+ conversations: sql6`COUNT(*)::integer`,
1281
1517
  date,
1282
1518
  visibility: juniorDestinations.visibility
1283
1519
  }).from(juniorConversations).leftJoin(
1284
1520
  juniorDestinations,
1285
- eq5(juniorDestinations.id, juniorConversations.destinationId)
1286
- ).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);
1287
1523
  }
1288
1524
  function directoryActivityDays(rows, nowMs) {
1289
1525
  const days = /* @__PURE__ */ new Map();
@@ -1355,9 +1591,8 @@ async function recentLocationRows(db, locationId) {
1355
1591
  conversationId: juniorConversations.conversationId,
1356
1592
  createdAt: juniorConversations.createdAt,
1357
1593
  destinationId: juniorDestinations.id,
1358
- destinationVisibility: juniorDestinations.visibility,
1359
1594
  durationMs: juniorConversations.durationMs,
1360
- email: sql4`COALESCE(
1595
+ email: sql6`COALESCE(
1361
1596
  ${juniorUsers.primaryEmailNormalized},
1362
1597
  ${juniorIdentities.email}
1363
1598
  )`,
@@ -1366,7 +1601,7 @@ async function recentLocationRows(db, locationId) {
1366
1601
  fullName: juniorUsers.displayName,
1367
1602
  handle: juniorIdentities.handle,
1368
1603
  lastActivityAt: juniorConversations.lastActivityAt,
1369
- providerSubjectId: sql4`CASE
1604
+ providerSubjectId: sql6`CASE
1370
1605
  WHEN ${juniorIdentities.provider} = 'slack'
1371
1606
  THEN ${juniorIdentities.providerSubjectId}
1372
1607
  ELSE NULL
@@ -1377,44 +1612,59 @@ async function recentLocationRows(db, locationId) {
1377
1612
  usage: juniorConversations.usage
1378
1613
  }).from(juniorConversations).innerJoin(
1379
1614
  juniorDestinations,
1380
- eq5(juniorDestinations.id, juniorConversations.destinationId)
1615
+ eq8(juniorDestinations.id, juniorConversations.destinationId)
1381
1616
  ).leftJoin(
1382
1617
  juniorIdentities,
1383
- eq5(juniorIdentities.id, juniorConversations.actorIdentityId)
1384
- ).leftJoin(juniorUsers, eq5(juniorUsers.id, juniorIdentities.userId)).where(
1385
- and5(
1618
+ eq8(juniorIdentities.id, juniorConversations.actorIdentityId)
1619
+ ).leftJoin(juniorUsers, eq8(juniorUsers.id, juniorIdentities.userId)).where(
1620
+ and7(
1386
1621
  publicLocationWhere(locationId),
1387
- isNull3(juniorConversations.archivedAt)
1622
+ isNull4(juniorConversations.archivedAt)
1388
1623
  )
1389
1624
  ).orderBy(
1390
1625
  desc2(juniorConversations.lastActivityAt),
1391
- asc2(juniorConversations.conversationId)
1626
+ asc3(juniorConversations.conversationId)
1392
1627
  ).limit(RECENT_LIMIT);
1393
1628
  }
1394
- async function readLocationDetailFromSql(locationId) {
1629
+ async function readLocationDetailFromSql(locationId, options = {}) {
1395
1630
  const nowMs = Date.now();
1396
1631
  const end = new Date(nowMs);
1397
1632
  end.setUTCHours(0, 0, 0, 0);
1398
1633
  const start = new Date(end);
1399
1634
  start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS - 1));
1400
1635
  const where = publicLocationWhere(locationId);
1401
- const activityDate = sql4`TO_CHAR(
1636
+ const activityDate = sql6`TO_CHAR(
1402
1637
  ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
1403
1638
  'YYYY-MM-DD'
1404
1639
  )`;
1405
1640
  const [locationRows, dayRows, actorRows, recentRows] = await Promise.all([
1406
1641
  getDb().select({
1407
1642
  ...locationColumns(),
1408
- ...conversationAggregateColumns(),
1643
+ ...treeAggregateColumns2,
1409
1644
  ...conversationRangeColumns()
1410
1645
  }).from(juniorConversations).innerJoin(
1646
+ treeConversation2,
1647
+ eq8(
1648
+ treeConversation2.rootConversationId,
1649
+ juniorConversations.conversationId
1650
+ )
1651
+ ).innerJoin(
1411
1652
  juniorDestinations,
1412
- eq5(juniorDestinations.id, juniorConversations.destinationId)
1653
+ eq8(juniorDestinations.id, juniorConversations.destinationId)
1413
1654
  ).where(where).groupBy(...locationGroupBy()),
1414
- 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(
1415
1665
  juniorDestinations,
1416
- eq5(juniorDestinations.id, juniorConversations.destinationId)
1417
- ).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),
1418
1668
  getDb().select({
1419
1669
  actorIdentityId: juniorConversations.actorIdentityId,
1420
1670
  email: juniorUsers.primaryEmailNormalized,
@@ -1423,14 +1673,20 @@ async function readLocationDetailFromSql(locationId) {
1423
1673
  identityEmail: juniorIdentities.email,
1424
1674
  identityProvider: juniorIdentities.provider,
1425
1675
  providerSubjectId: juniorIdentities.providerSubjectId,
1426
- ...conversationAggregateColumns()
1676
+ ...treeAggregateColumns2
1427
1677
  }).from(juniorConversations).innerJoin(
1678
+ treeConversation2,
1679
+ eq8(
1680
+ treeConversation2.rootConversationId,
1681
+ juniorConversations.conversationId
1682
+ )
1683
+ ).innerJoin(
1428
1684
  juniorDestinations,
1429
- eq5(juniorDestinations.id, juniorConversations.destinationId)
1685
+ eq8(juniorDestinations.id, juniorConversations.destinationId)
1430
1686
  ).leftJoin(
1431
1687
  juniorIdentities,
1432
- eq5(juniorIdentities.id, juniorConversations.actorIdentityId)
1433
- ).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(
1434
1690
  juniorConversations.actorIdentityId,
1435
1691
  juniorUsers.primaryEmailNormalized,
1436
1692
  juniorUsers.displayName,
@@ -1470,6 +1726,15 @@ async function readLocationDetailFromSql(locationId) {
1470
1726
  actors.push(item);
1471
1727
  }
1472
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
+ ]);
1473
1738
  return {
1474
1739
  ...location,
1475
1740
  activityDays: activity,
@@ -1477,7 +1742,12 @@ async function readLocationDetailFromSql(locationId) {
1477
1742
  (left, right) => right.conversations - left.conversations || left.label.localeCompare(right.label)
1478
1743
  ),
1479
1744
  generatedAt: new Date(nowMs).toISOString(),
1480
- 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
+ ),
1481
1751
  source: "conversation_index",
1482
1752
  windowEnd: end.toISOString(),
1483
1753
  windowStart: activity[0] ? `${activity[0].date}T00:00:00.000Z` : end.toISOString()
@@ -1485,19 +1755,25 @@ async function readLocationDetailFromSql(locationId) {
1485
1755
  }
1486
1756
 
1487
1757
  // src/api/locations/detail.ts
1488
- async function readLocationDetail(locationId) {
1489
- const report = await readLocationDetailFromSql(locationId);
1758
+ async function readLocationDetail(locationId, options = {}) {
1759
+ const report = await readLocationDetailFromSql(locationId, options);
1490
1760
  return report ? locationDetailReportSchema.parse(report) : void 0;
1491
1761
  }
1492
- var detail_default2 = {
1762
+ var detail_default2 = defineApiRoute({
1493
1763
  method: "get",
1494
1764
  path: "/:locationId",
1765
+ responseSchema: locationDetailReportSchema,
1495
1766
  handler: async (c) => {
1496
1767
  const { locationId } = parseParams(locationParamsSchema, c.req.param());
1497
- const report = await readLocationDetail(locationId);
1498
- 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;
1499
1775
  }
1500
- };
1776
+ });
1501
1777
 
1502
1778
  // src/api/locations/list.ts
1503
1779
  async function readLocationDirectory() {
@@ -1505,17 +1781,18 @@ async function readLocationDirectory() {
1505
1781
  await readLocationDirectoryFromSql()
1506
1782
  );
1507
1783
  }
1508
- var list_default2 = {
1784
+ var list_default2 = defineApiRoute({
1509
1785
  method: "get",
1510
1786
  path: "/",
1511
- handler: async () => Response.json(await readLocationDirectory())
1512
- };
1787
+ responseSchema: locationDirectoryReportSchema,
1788
+ handler: readLocationDirectory
1789
+ });
1513
1790
 
1514
1791
  // src/api/locations/routes.ts
1515
1792
  var routes2 = [list_default2, detail_default2];
1516
1793
  function createLocationRoutes() {
1517
1794
  const app = new Hono2();
1518
- for (const route of routes2) app.on(route.method, route.path, route.handler);
1795
+ registerApiRoutes(app, routes2);
1519
1796
  return app;
1520
1797
  }
1521
1798
 
@@ -1523,12 +1800,68 @@ function createLocationRoutes() {
1523
1800
  import { Hono as Hono3 } from "hono";
1524
1801
 
1525
1802
  // src/api/people/list.query.ts
1526
- 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";
1527
1804
 
1528
1805
  // src/api/people/shared.ts
1529
- 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";
1530
1808
  var RECENT_LIMIT2 = 25;
1531
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
+ }
1532
1865
  function normalizeEmail(email) {
1533
1866
  const normalized = email?.trim().toLowerCase();
1534
1867
  return normalized || void 0;
@@ -1570,11 +1903,11 @@ function statsItems2(map) {
1570
1903
  }
1571
1904
  function verifiedActorWhere(email) {
1572
1905
  const normalizedEmail = normalizeEmail(email);
1573
- return and6(
1574
- eq6(juniorIdentities.provider, "slack"),
1575
- eq6(juniorIdentities.emailVerified, true),
1576
- sql5`${juniorUsers.primaryEmailNormalized} IS NOT NULL`,
1577
- 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
1578
1911
  );
1579
1912
  }
1580
1913
  async function recentActorRows(email) {
@@ -1583,7 +1916,6 @@ async function recentActorRows(email) {
1583
1916
  conversationId: juniorConversations.conversationId,
1584
1917
  createdAt: juniorConversations.createdAt,
1585
1918
  destinationId: juniorDestinations.id,
1586
- destinationVisibility: juniorDestinations.visibility,
1587
1919
  durationMs: juniorConversations.durationMs,
1588
1920
  email: juniorUsers.primaryEmailNormalized,
1589
1921
  executionStatus: juniorConversations.executionStatus,
@@ -1598,15 +1930,15 @@ async function recentActorRows(email) {
1598
1930
  usage: juniorConversations.usage
1599
1931
  }).from(juniorConversations).innerJoin(
1600
1932
  juniorIdentities,
1601
- eq6(juniorIdentities.id, juniorConversations.actorIdentityId)
1602
- ).innerJoin(juniorUsers, eq6(juniorUsers.id, juniorIdentities.userId)).leftJoin(
1933
+ eq9(juniorIdentities.id, juniorConversations.actorIdentityId)
1934
+ ).innerJoin(juniorUsers, eq9(juniorUsers.id, juniorIdentities.userId)).leftJoin(
1603
1935
  juniorDestinations,
1604
- eq6(juniorDestinations.id, juniorConversations.destinationId)
1936
+ eq9(juniorDestinations.id, juniorConversations.destinationId)
1605
1937
  ).where(
1606
- and6(verifiedActorWhere(email), isNull4(juniorConversations.archivedAt))
1938
+ and8(verifiedActorWhere(email), isNull5(juniorConversations.archivedAt))
1607
1939
  ).orderBy(
1608
1940
  desc3(juniorConversations.lastActivityAt),
1609
- asc3(juniorConversations.conversationId)
1941
+ asc4(juniorConversations.conversationId)
1610
1942
  ).limit(RECENT_LIMIT2);
1611
1943
  }
1612
1944
 
@@ -1632,7 +1964,7 @@ function directoryActivityDays2(rows, nowMs) {
1632
1964
  async function readPeopleListFromSql() {
1633
1965
  const nowMs = Date.now();
1634
1966
  const { end, start } = activityWindow(nowMs);
1635
- const activityDate = sql6`TO_CHAR(
1967
+ const activityDate = sql8`TO_CHAR(
1636
1968
  ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
1637
1969
  'YYYY-MM-DD'
1638
1970
  )`;
@@ -1640,24 +1972,24 @@ async function readPeopleListFromSql() {
1640
1972
  getDb().select({
1641
1973
  email: juniorUsers.primaryEmailNormalized,
1642
1974
  fullName: juniorUsers.displayName,
1643
- slackUserId: sql6`MAX(${juniorIdentities.providerSubjectId})`,
1644
- slackUserName: sql6`MAX(${juniorIdentities.handle})`,
1975
+ slackUserId: sql8`MAX(${juniorIdentities.providerSubjectId})`,
1976
+ slackUserName: sql8`MAX(${juniorIdentities.handle})`,
1645
1977
  activeDays: conversationActiveDaysColumn(),
1646
- ...conversationAggregateColumns(),
1978
+ ...peopleTreeAggregateColumns,
1647
1979
  ...conversationRangeColumns()
1648
1980
  }).from(juniorConversations).innerJoin(
1649
1981
  juniorIdentities,
1650
- eq7(juniorIdentities.id, juniorConversations.actorIdentityId)
1651
- ).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),
1652
1984
  getDb().select({
1653
- activePeople: sql6`COUNT(DISTINCT ${juniorUsers.id})::int`,
1654
- conversations: sql6`COUNT(*)::int`,
1985
+ activePeople: sql8`COUNT(DISTINCT ${juniorUsers.id})::int`,
1986
+ conversations: sql8`COUNT(*)::int`,
1655
1987
  date: activityDate
1656
1988
  }).from(juniorConversations).innerJoin(
1657
1989
  juniorIdentities,
1658
- eq7(juniorIdentities.id, juniorConversations.actorIdentityId)
1659
- ).innerJoin(juniorUsers, eq7(juniorUsers.id, juniorIdentities.userId)).where(
1660
- and7(
1990
+ eq10(juniorIdentities.id, juniorConversations.actorIdentityId)
1991
+ ).innerJoin(juniorUsers, eq10(juniorUsers.id, juniorIdentities.userId)).where(
1992
+ and9(
1661
1993
  verifiedActorWhere(),
1662
1994
  gte3(juniorConversations.lastActivityAt, start)
1663
1995
  )
@@ -1698,14 +2030,15 @@ async function readPeopleListFromSql() {
1698
2030
  async function readPeopleList() {
1699
2031
  return actorDirectoryReportSchema.parse(await readPeopleListFromSql());
1700
2032
  }
1701
- var list_default3 = {
2033
+ var list_default3 = defineApiRoute({
1702
2034
  method: "get",
1703
2035
  path: "/",
1704
- handler: async () => Response.json(await readPeopleList())
1705
- };
2036
+ responseSchema: actorDirectoryReportSchema,
2037
+ handler: readPeopleList
2038
+ });
1706
2039
 
1707
2040
  // src/api/people/profile.query.ts
1708
- 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";
1709
2042
  function emptyProfile(email, nowMs) {
1710
2043
  const end = new Date(nowMs);
1711
2044
  end.setUTCHours(0, 0, 0, 0);
@@ -1740,7 +2073,7 @@ function addAggregate2(map, label, row) {
1740
2073
  map.set(label, item);
1741
2074
  }
1742
2075
  function surfaceExpression() {
1743
- return sql7`CASE
2076
+ return sql9`CASE
1744
2077
  WHEN ${juniorConversations.source} IN ('api', 'scheduler', 'slack')
1745
2078
  THEN ${juniorConversations.source}
1746
2079
  WHEN ${juniorConversations.conversationId} LIKE 'slack:%' THEN 'slack'
@@ -1758,7 +2091,7 @@ function locationLabel2(row) {
1758
2091
  channelName: row.channelName ?? void 0
1759
2092
  }) ?? "Conversation";
1760
2093
  }
1761
- async function readPeopleProfileFromSql(email) {
2094
+ async function readPeopleProfileFromSql(email, options = {}) {
1762
2095
  const nowMs = Date.now();
1763
2096
  const normalizedEmail = normalizeEmail(email);
1764
2097
  if (!normalizedEmail) return emptyProfile("", nowMs);
@@ -1767,53 +2100,54 @@ async function readPeopleProfileFromSql(email) {
1767
2100
  const start = new Date(end);
1768
2101
  start.setUTCDate(start.getUTCDate() - (ACTIVITY_DAYS2 - 1));
1769
2102
  const where = verifiedActorWhere(normalizedEmail);
2103
+ const metricsJoin = peopleTreeMetricsJoin();
1770
2104
  const surface = surfaceExpression();
1771
- const activityDate = sql7`TO_CHAR(
2105
+ const activityDate = sql9`TO_CHAR(
1772
2106
  ${juniorConversations.lastActivityAt} AT TIME ZONE 'UTC',
1773
2107
  'YYYY-MM-DD'
1774
2108
  )`;
1775
- const channel = sql7`SPLIT_PART(${juniorConversations.conversationId}, ':', 2)`;
2109
+ const channel = sql9`SPLIT_PART(${juniorConversations.conversationId}, ':', 2)`;
1776
2110
  const [totalsRows, dayRows, locationRows, surfaceRows, recentRows] = await Promise.all([
1777
2111
  getDb().select({
1778
2112
  email: juniorUsers.primaryEmailNormalized,
1779
2113
  fullName: juniorUsers.displayName,
1780
- slackUserId: sql7`MAX(${juniorIdentities.providerSubjectId})`,
1781
- slackUserName: sql7`MAX(${juniorIdentities.handle})`,
2114
+ slackUserId: sql9`MAX(${juniorIdentities.providerSubjectId})`,
2115
+ slackUserName: sql9`MAX(${juniorIdentities.handle})`,
1782
2116
  activeDays: conversationActiveDaysColumn(),
1783
- ...conversationAggregateColumns()
2117
+ ...peopleTreeAggregateColumns
1784
2118
  }).from(juniorConversations).innerJoin(
1785
2119
  juniorIdentities,
1786
- eq8(juniorIdentities.id, juniorConversations.actorIdentityId)
1787
- ).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),
1788
2122
  getDb().select({
1789
2123
  date: activityDate,
1790
- ...conversationAggregateColumns()
2124
+ ...peopleTreeAggregateColumns
1791
2125
  }).from(juniorConversations).innerJoin(
1792
2126
  juniorIdentities,
1793
- eq8(juniorIdentities.id, juniorConversations.actorIdentityId)
1794
- ).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),
1795
2129
  getDb().select({
1796
2130
  channel,
1797
2131
  channelName: juniorConversations.channelName,
1798
2132
  destinationVisibility: juniorDestinations.visibility,
1799
2133
  surface,
1800
- ...conversationAggregateColumns()
2134
+ ...peopleTreeAggregateColumns
1801
2135
  }).from(juniorConversations).innerJoin(
1802
2136
  juniorIdentities,
1803
- eq8(juniorIdentities.id, juniorConversations.actorIdentityId)
1804
- ).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(
1805
2139
  juniorDestinations,
1806
- eq8(juniorDestinations.id, juniorConversations.destinationId)
2140
+ eq11(juniorDestinations.id, juniorConversations.destinationId)
1807
2141
  ).where(where).groupBy(
1808
2142
  channel,
1809
2143
  juniorConversations.channelName,
1810
2144
  juniorDestinations.visibility,
1811
2145
  surface
1812
2146
  ),
1813
- getDb().select({ surface, ...conversationAggregateColumns() }).from(juniorConversations).innerJoin(
2147
+ getDb().select({ surface, ...peopleTreeAggregateColumns }).from(juniorConversations).innerJoin(
1814
2148
  juniorIdentities,
1815
- eq8(juniorIdentities.id, juniorConversations.actorIdentityId)
1816
- ).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),
1817
2151
  recentActorRows(normalizedEmail)
1818
2152
  ]);
1819
2153
  const totalsRow = totalsRows[0];
@@ -1847,11 +2181,25 @@ async function readPeopleProfileFromSql(email) {
1847
2181
  row
1848
2182
  );
1849
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
+ ]);
1850
2193
  return {
1851
2194
  activityDays: activityDays2(days, nowMs),
1852
2195
  generatedAt: new Date(nowMs).toISOString(),
1853
2196
  locations: statsItems2(locations),
1854
- 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
+ ),
1855
2203
  actor,
1856
2204
  source: "conversation_index",
1857
2205
  surfaces: statsItems2(surfaces),
@@ -1869,23 +2217,30 @@ async function readPeopleProfileFromSql(email) {
1869
2217
  }
1870
2218
 
1871
2219
  // src/api/people/profile.ts
1872
- async function readPeopleProfile(email) {
1873
- return actorProfileReportSchema.parse(await readPeopleProfileFromSql(email));
2220
+ async function readPeopleProfile(email, options = {}) {
2221
+ return actorProfileReportSchema.parse(
2222
+ await readPeopleProfileFromSql(email, options)
2223
+ );
1874
2224
  }
1875
- var profile_default = {
2225
+ var profile_default = defineApiRoute({
1876
2226
  method: "get",
1877
2227
  path: "/:email",
2228
+ responseSchema: actorProfileReportSchema,
1878
2229
  handler: async (c) => {
1879
2230
  const { email } = parseParams(personParamsSchema, c.req.param());
1880
- return Response.json(await readPeopleProfile(email));
2231
+ const verifiedViewerEmail = c.get("verifiedViewerEmail");
2232
+ return readPeopleProfile(
2233
+ email,
2234
+ verifiedViewerEmail ? { verifiedViewerEmail } : {}
2235
+ );
1881
2236
  }
1882
- };
2237
+ });
1883
2238
 
1884
2239
  // src/api/people/routes.ts
1885
2240
  var routes3 = [list_default3, profile_default];
1886
2241
  function createPeopleRoutes() {
1887
2242
  const app = new Hono3();
1888
- for (const route of routes3) app.on(route.method, route.path, route.handler);
2243
+ registerApiRoutes(app, routes3);
1889
2244
  return app;
1890
2245
  }
1891
2246
 
@@ -1901,7 +2256,7 @@ function readDescriptionText(home) {
1901
2256
  }
1902
2257
  }
1903
2258
  async function readSkillReports() {
1904
- const { discoverSkills } = await import("./skills-U6MDAST7.js");
2259
+ const { discoverSkills } = await import("./skills-P5MU4EBX.js");
1905
2260
  const skills = await discoverSkills();
1906
2261
  return skillReportsSchema.parse(
1907
2262
  skills.map((skill) => ({
@@ -1911,7 +2266,7 @@ async function readSkillReports() {
1911
2266
  );
1912
2267
  }
1913
2268
  async function readPluginReports() {
1914
- const { pluginCatalogRuntime } = await import("./catalog-runtime-DMGJSLV2.js");
2269
+ const { pluginCatalogRuntime } = await import("./catalog-runtime-D22W6QEJ.js");
1915
2270
  return pluginReportsSchema.parse(
1916
2271
  pluginCatalogRuntime.getProviders().map((plugin) => ({
1917
2272
  name: plugin.manifest.name
@@ -1921,7 +2276,7 @@ async function readPluginReports() {
1921
2276
  async function readRuntimeInfoReport() {
1922
2277
  const [{ homeDir }, { pluginCatalogRuntime }, plugins, skills] = await Promise.all([
1923
2278
  import("./discovery-RMHYSRL7.js"),
1924
- import("./catalog-runtime-DMGJSLV2.js"),
2279
+ import("./catalog-runtime-D22W6QEJ.js"),
1925
2280
  readPluginReports(),
1926
2281
  readSkillReports()
1927
2282
  ]);
@@ -1937,7 +2292,7 @@ async function readRuntimeInfoReport() {
1937
2292
  }
1938
2293
  async function readPluginOperationalReportFeed() {
1939
2294
  const nowMs = Date.now();
1940
- const { getPluginOperationalReports } = await import("./agent-hooks-FHHVLNPA.js");
2295
+ const { getPluginOperationalReports } = await import("./agent-hooks-QQ2IWKOP.js");
1941
2296
  return pluginOperationalReportFeedSchema.parse({
1942
2297
  source: "plugins",
1943
2298
  generatedAt: new Date(nowMs).toISOString(),
@@ -1946,28 +2301,56 @@ async function readPluginOperationalReportFeed() {
1946
2301
  }
1947
2302
 
1948
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
+ ];
1949
2336
  function createJuniorApi() {
1950
2337
  const app = new Hono4();
1951
- app.get("/api/health", async () => {
1952
- return Response.json(await readHealthReport());
1953
- });
1954
- app.get("/api/runtime", async () => {
1955
- return Response.json(await readRuntimeInfoReport());
1956
- });
1957
- app.get("/api/plugins", async () => {
1958
- return Response.json(await readPluginReports());
1959
- });
1960
- app.get("/api/skills", async () => {
1961
- return Response.json(await readSkillReports());
1962
- });
1963
- app.get("/api/plugin-reports", async () => {
1964
- return Response.json(await readPluginOperationalReportFeed());
1965
- });
2338
+ registerApiRoutes(app, routes4);
1966
2339
  app.route("/api/conversations", createConversationRoutes());
1967
2340
  app.route("/api/people", createPeopleRoutes());
1968
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
+ );
1969
2351
  return app;
1970
2352
  }
1971
2353
  export {
1972
- createJuniorApi
2354
+ createJuniorApi,
2355
+ jsonResponse
1973
2356
  };