@sentry/junior 0.69.0 → 0.71.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.
@@ -1,12 +1,31 @@
1
1
  import { type AgentPluginCredentialResult, type AgentPluginGrant, type AgentPluginProviderAccount } from "@sentry/junior-plugin-api";
2
2
  import type { StoredTokens, UserTokenStore } from "@/chat/credentials/user-token-store";
3
3
  export interface EgressGrantInput {
4
+ bodyText?: string;
4
5
  method: string;
5
6
  provider: string;
6
7
  upstreamUrl: URL;
7
8
  }
8
9
  /** Ask a plugin which grant an outbound request needs. */
9
10
  export declare function selectPluginGrant(input: EgressGrantInput): Promise<AgentPluginGrant | undefined>;
11
+ export interface EgressResponseInput {
12
+ grant: AgentPluginGrant;
13
+ method: string;
14
+ provider: string;
15
+ response: {
16
+ headers: Headers;
17
+ readText(maxBytes: number): Promise<string | undefined>;
18
+ status: number;
19
+ };
20
+ upstreamUrl: URL;
21
+ }
22
+ export interface EgressResponseEffects {
23
+ permissionDenied?: {
24
+ message: string;
25
+ };
26
+ }
27
+ /** Let the owning plugin inspect an upstream response without changing pass-through behavior. */
28
+ export declare function onPluginEgressResponse(input: EgressResponseInput): Promise<EgressResponseEffects>;
10
29
  /** Return whether a plugin owns credential issuance for egress. */
11
30
  export declare function hasEgressCredentialHooks(provider: string): boolean;
12
31
  export interface IssueCredentialInput {
@@ -21,6 +21,7 @@ export declare class SandboxEgressCredentialNeededError extends Error {
21
21
  }
22
22
  /** Select the plugin-defined or default grant needed for one outbound request. */
23
23
  export declare function selectSandboxEgressGrant(input: {
24
+ bodyText?: string;
24
25
  method: string;
25
26
  provider: string;
26
27
  upstreamUrl: URL;
@@ -89,7 +89,7 @@ export declare const sandboxEgressPermissionDeniedSignalSchema: z.ZodObject<{
89
89
  provider: z.ZodString;
90
90
  source: z.ZodLiteral<"upstream">;
91
91
  sso: z.ZodOptional<z.ZodString>;
92
- status: z.ZodLiteral<403>;
92
+ status: z.ZodNumber;
93
93
  upstreamHost: z.ZodString;
94
94
  upstreamPath: z.ZodString;
95
95
  createdAtMs: z.ZodNumber;
@@ -0,0 +1,46 @@
1
+ import type { AgentTurnRequester, AgentTurnSurface } from "./turn-session";
2
+ export interface ConversationDetailsRecord {
3
+ conversationId: string;
4
+ /** Generated display title from the LLM. Absent until title has been produced. */
5
+ displayTitle?: string;
6
+ /** The message id used as input when generating the title. */
7
+ titleSourceMessageId?: string;
8
+ /** Slack channel name or equivalent location label. */
9
+ channelName?: string;
10
+ /** Surface on which the conversation was started. */
11
+ originSurface?: AgentTurnSurface;
12
+ /** Requester who initiated the conversation (first turn). */
13
+ originRequester?: AgentTurnRequester;
14
+ /** Timestamp of the first turn in the conversation. */
15
+ startedAtMs?: number;
16
+ }
17
+ /**
18
+ * Record the origin context for a conversation the first time it is seen and
19
+ * refresh the context TTL on later turns without changing the stored origin.
20
+ */
21
+ export declare function initConversationContext(conversationId: string, context: {
22
+ channelName?: string;
23
+ originSurface?: AgentTurnSurface;
24
+ originRequester?: AgentTurnRequester;
25
+ startedAtMs: number;
26
+ }): Promise<void>;
27
+ /**
28
+ * Persist or refresh the LLM-generated title for a conversation.
29
+ *
30
+ * Plain set — no read, no lock.
31
+ */
32
+ export declare function setConversationTitle(conversationId: string, title: {
33
+ displayTitle: string;
34
+ titleSourceMessageId?: string;
35
+ }): Promise<void>;
36
+ /**
37
+ * Read conversation details for a single conversation.
38
+ * Assembles the context and title records in parallel.
39
+ * Returns undefined only when neither context nor title details exist yet.
40
+ */
41
+ export declare function getConversationDetails(conversationId: string): Promise<ConversationDetailsRecord | undefined>;
42
+ /**
43
+ * Bulk-fetch conversation details for a set of conversation ids in parallel.
44
+ * Returns a map from conversationId → record (omits ids with no details).
45
+ */
46
+ export declare function getConversationDetailsForIds(conversationIds: Iterable<string>): Promise<Map<string, ConversationDetailsRecord>>;
@@ -12,7 +12,6 @@ export interface AgentTurnRequester {
12
12
  }
13
13
  export interface AgentTurnSessionRecord {
14
14
  channelName?: string;
15
- conversationTitle?: string;
16
15
  version: number;
17
16
  conversationId: string;
18
17
  cumulativeDurationMs: number;
@@ -39,7 +38,6 @@ export declare function getAgentTurnSessionRecord(conversationId: string, sessio
39
38
  /** Commit stable Pi session state and advance the turn session record. */
40
39
  export declare function upsertAgentTurnSessionRecord(args: {
41
40
  channelName?: string;
42
- conversationTitle?: string;
43
41
  conversationId: string;
44
42
  cumulativeDurationMs?: number;
45
43
  cumulativeUsage?: AgentTurnUsage;
@@ -61,7 +59,6 @@ export declare function upsertAgentTurnSessionRecord(args: {
61
59
  /** Record turn-session metadata without storing conversation messages. */
62
60
  export declare function recordAgentTurnSessionSummary(args: {
63
61
  channelName?: string;
64
- conversationTitle?: string;
65
62
  conversationId: string;
66
63
  cumulativeDurationMs?: number;
67
64
  cumulativeUsage?: AgentTurnUsage;
@@ -4,6 +4,7 @@ import type { ConversationWorkQueue } from "./queue";
4
4
  export declare const CONVERSATION_WORK_LEASE_TTL_MS = 90000;
5
5
  export declare const CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15000;
6
6
  export declare const CONVERSATION_WORK_STALE_ENQUEUE_MS = 60000;
7
+ export declare const CONVERSATION_WORK_MAX_CONSECUTIVE_FAILURES = 5;
7
8
  export type InboundMessageSource = "plugin" | "scheduler" | "slack";
8
9
  export interface AgentInputMessage {
9
10
  attachments?: unknown[];
@@ -28,13 +29,16 @@ export interface ConversationLease {
28
29
  leaseToken: string;
29
30
  }
30
31
  export interface ConversationWorkState {
32
+ consecutiveFailureCount: number;
31
33
  conversationId: string;
32
34
  destination: Destination;
33
35
  lastEnqueuedAtMs?: number;
36
+ lastFailureAtMs?: number;
34
37
  lease?: ConversationLease;
35
38
  messages: InboundMessageRecord[];
36
39
  needsRun: boolean;
37
40
  schemaVersion: 1;
41
+ terminallyFailedAtMs?: number;
38
42
  updatedAtMs: number;
39
43
  }
40
44
  export interface ConversationLeaseAcquired {
@@ -151,6 +155,25 @@ export declare function clearExpiredConversationLease(args: {
151
155
  nowMs?: number;
152
156
  state?: StateAdapter;
153
157
  }): Promise<boolean>;
158
+ export interface RecordConversationWorkFailureResult {
159
+ abandoned: boolean;
160
+ consecutiveFailureCount: number;
161
+ releasedLease: boolean;
162
+ }
163
+ /**
164
+ * Increment the durable failure counter after a caught worker error so
165
+ * deterministic poison work cannot churn the queue forever. When the counter
166
+ * crosses {@link CONVERSATION_WORK_MAX_CONSECUTIVE_FAILURES}, the conversation
167
+ * is marked terminally failed: the lease is cleared, pending mailbox messages
168
+ * are dropped, and the conversation drops out of the recovery index so neither
169
+ * the worker nor heartbeat will requeue it again. A later inbound message
170
+ * resets the counter and gives the conversation a fresh attempt.
171
+ */
172
+ export declare function recordConversationWorkFailure(args: {
173
+ conversationId: string;
174
+ nowMs?: number;
175
+ state?: StateAdapter;
176
+ }): Promise<RecordConversationWorkFailureResult>;
154
177
  /** List bounded conversation ids that may need heartbeat recovery. */
155
178
  export declare function listConversationWorkIds(args?: {
156
179
  limit?: number;
@@ -16,7 +16,7 @@ export interface ConversationWorkerResult {
16
16
  status: "completed" | "lost_lease" | "yielded";
17
17
  }
18
18
  export interface ConversationWorkProcessResult {
19
- status: "active" | "completed" | "lost_lease" | "no_work" | "pending_requeued" | "yielded";
19
+ status: "abandoned" | "active" | "completed" | "lost_lease" | "no_work" | "pending_requeued" | "yielded";
20
20
  }
21
21
  export interface ProcessConversationWorkOptions {
22
22
  checkInIntervalMs?: number;
@@ -29,6 +29,7 @@ import {
29
29
  logInfo,
30
30
  logWarn,
31
31
  soulPathCandidates,
32
+ toOptionalNumber,
32
33
  worldPathCandidates
33
34
  } from "./chunk-BBXYXOJW.js";
34
35
  import {
@@ -2174,7 +2175,6 @@ function parseAgentTurnSessionFields(parsed) {
2174
2175
  return void 0;
2175
2176
  }
2176
2177
  const channelName = typeof parsed.channelName === "string" && parsed.channelName.trim() ? parsed.channelName.trim() : void 0;
2177
- const conversationTitle = typeof parsed.conversationTitle === "string" && parsed.conversationTitle.trim() ? parsed.conversationTitle.trim() : void 0;
2178
2178
  const conversationId = parsed.conversationId;
2179
2179
  const sessionId = parsed.sessionId;
2180
2180
  const sliceId = toFiniteNonNegativeNumber(parsed.sliceId);
@@ -2194,7 +2194,6 @@ function parseAgentTurnSessionFields(parsed) {
2194
2194
  return {
2195
2195
  version,
2196
2196
  ...channelName ? { channelName } : {},
2197
- ...conversationTitle ? { conversationTitle } : {},
2198
2197
  conversationId,
2199
2198
  sessionId,
2200
2199
  sliceId,
@@ -2285,7 +2284,6 @@ function materializeAgentTurnSessionRecord(stored, piMessages) {
2285
2284
  return {
2286
2285
  version: stored.version,
2287
2286
  ...stored.channelName ? { channelName: stored.channelName } : {},
2288
- ...stored.conversationTitle ? { conversationTitle: stored.conversationTitle } : {},
2289
2287
  conversationId: stored.conversationId,
2290
2288
  sessionId: stored.sessionId,
2291
2289
  sliceId: stored.sliceId,
@@ -2347,7 +2345,6 @@ function buildStoredRecord(args) {
2347
2345
  return {
2348
2346
  version: (args.previousVersion ?? 0) + 1,
2349
2347
  ...args.channelName ? { channelName: args.channelName } : {},
2350
- ...args.conversationTitle ? { conversationTitle: args.conversationTitle } : {},
2351
2348
  conversationId: args.conversationId,
2352
2349
  sessionId: args.sessionId,
2353
2350
  sliceId: args.sliceId,
@@ -2408,7 +2405,6 @@ async function updateAgentTurnSessionState(args) {
2408
2405
  state: args.state,
2409
2406
  committedMessageCount: parsed.committedMessageCount,
2410
2407
  ...parsed.channelName ? { channelName: parsed.channelName } : {},
2411
- ...parsed.conversationTitle ? { conversationTitle: parsed.conversationTitle } : {},
2412
2408
  startedAtMs: parsed.startedAtMs,
2413
2409
  lastProgressAtMs: parsed.lastProgressAtMs,
2414
2410
  previousVersion: parsed.version,
@@ -2442,9 +2438,6 @@ async function upsertAgentTurnSessionRecord(args) {
2442
2438
  ttlMs,
2443
2439
  record: buildStoredRecord({
2444
2440
  ...args.channelName ?? existingRecord?.channelName ? { channelName: args.channelName ?? existingRecord?.channelName } : {},
2445
- ...args.conversationTitle ?? existingRecord?.conversationTitle ? {
2446
- conversationTitle: args.conversationTitle ?? existingRecord?.conversationTitle
2447
- } : {},
2448
2441
  conversationId: args.conversationId,
2449
2442
  sessionId: args.sessionId,
2450
2443
  sliceId: args.sliceId,
@@ -2478,9 +2471,6 @@ async function recordAgentTurnSessionSummary(args) {
2478
2471
  {
2479
2472
  version: existing?.version ?? 0,
2480
2473
  ...args.channelName ?? existing?.channelName ? { channelName: args.channelName ?? existing?.channelName } : {},
2481
- ...args.conversationTitle ?? existing?.conversationTitle ? {
2482
- conversationTitle: args.conversationTitle ?? existing?.conversationTitle
2483
- } : {},
2484
2474
  conversationId: args.conversationId,
2485
2475
  sessionId: args.sessionId,
2486
2476
  sliceId: args.sliceId,
@@ -2687,6 +2677,123 @@ function formatSlackConversationRedactedLabel(context) {
2687
2677
  return formatSlackConversationTypeLabel(context.type);
2688
2678
  }
2689
2679
 
2680
+ // src/chat/state/conversation-details.ts
2681
+ import { THREAD_STATE_TTL_MS as THREAD_STATE_TTL_MS2 } from "chat";
2682
+ var CONVERSATION_PREFIX = "junior:conversation";
2683
+ var CONVERSATION_DETAILS_TTL_MS = THREAD_STATE_TTL_MS2;
2684
+ function conversationContextKey(conversationId) {
2685
+ return `${CONVERSATION_PREFIX}:${conversationId}:context`;
2686
+ }
2687
+ function conversationTitleKey(conversationId) {
2688
+ return `${CONVERSATION_PREFIX}:${conversationId}:title`;
2689
+ }
2690
+ function parseAgentTurnRequester2(value) {
2691
+ if (!isRecord(value)) return void 0;
2692
+ const requester = {
2693
+ ...typeof value.email === "string" ? { email: value.email } : {},
2694
+ ...typeof value.fullName === "string" ? { fullName: value.fullName } : {},
2695
+ ...typeof value.slackUserId === "string" ? { slackUserId: value.slackUserId } : {},
2696
+ ...typeof value.slackUserName === "string" ? { slackUserName: value.slackUserName } : {}
2697
+ };
2698
+ return Object.keys(requester).length > 0 ? requester : void 0;
2699
+ }
2700
+ function parseOriginSurface(value) {
2701
+ if (value === "slack" || value === "api" || value === "scheduler" || value === "internal") {
2702
+ return value;
2703
+ }
2704
+ return void 0;
2705
+ }
2706
+ function storedContextFromInput(context) {
2707
+ return {
2708
+ ...context.channelName ? { channelName: context.channelName } : {},
2709
+ ...context.originSurface ? { originSurface: context.originSurface } : {},
2710
+ ...context.originRequester ? { originRequester: context.originRequester } : {},
2711
+ startedAtMs: context.startedAtMs
2712
+ };
2713
+ }
2714
+ function parseContext(value) {
2715
+ if (!isRecord(value)) return void 0;
2716
+ const startedAtMs = toOptionalNumber(value.startedAtMs);
2717
+ if (startedAtMs === void 0) return void 0;
2718
+ return {
2719
+ ...typeof value.channelName === "string" && value.channelName.trim() ? { channelName: value.channelName.trim() } : {},
2720
+ ...parseOriginSurface(value.originSurface) ? { originSurface: parseOriginSurface(value.originSurface) } : {},
2721
+ ...parseAgentTurnRequester2(value.originRequester) ? { originRequester: parseAgentTurnRequester2(value.originRequester) } : {},
2722
+ startedAtMs
2723
+ };
2724
+ }
2725
+ function parseTitle(value) {
2726
+ if (!isRecord(value)) return void 0;
2727
+ const displayTitle = typeof value.displayTitle === "string" && value.displayTitle.trim() ? value.displayTitle.trim() : void 0;
2728
+ if (!displayTitle) return void 0;
2729
+ return {
2730
+ displayTitle,
2731
+ ...typeof value.titleSourceMessageId === "string" ? { titleSourceMessageId: value.titleSourceMessageId } : {}
2732
+ };
2733
+ }
2734
+ async function initConversationContext(conversationId, context) {
2735
+ const stateAdapter = getStateAdapter();
2736
+ await stateAdapter.connect();
2737
+ const key2 = conversationContextKey(conversationId);
2738
+ const inserted = await stateAdapter.setIfNotExists(
2739
+ key2,
2740
+ storedContextFromInput(context),
2741
+ CONVERSATION_DETAILS_TTL_MS
2742
+ );
2743
+ if (inserted) return;
2744
+ const existing = parseContext(await stateAdapter.get(key2));
2745
+ if (!existing) {
2746
+ return;
2747
+ }
2748
+ await stateAdapter.set(key2, existing, CONVERSATION_DETAILS_TTL_MS);
2749
+ }
2750
+ async function setConversationTitle(conversationId, title) {
2751
+ const stateAdapter = getStateAdapter();
2752
+ await stateAdapter.connect();
2753
+ await stateAdapter.set(
2754
+ conversationTitleKey(conversationId),
2755
+ {
2756
+ displayTitle: title.displayTitle,
2757
+ ...title.titleSourceMessageId ? { titleSourceMessageId: title.titleSourceMessageId } : {}
2758
+ },
2759
+ CONVERSATION_DETAILS_TTL_MS
2760
+ );
2761
+ }
2762
+ async function getConversationDetails(conversationId) {
2763
+ const stateAdapter = getStateAdapter();
2764
+ await stateAdapter.connect();
2765
+ const [rawContext, rawTitle] = await Promise.all([
2766
+ stateAdapter.get(conversationContextKey(conversationId)),
2767
+ stateAdapter.get(conversationTitleKey(conversationId))
2768
+ ]);
2769
+ const context = parseContext(rawContext);
2770
+ const title = parseTitle(rawTitle);
2771
+ if (!context && !title) return void 0;
2772
+ return {
2773
+ conversationId,
2774
+ ...title?.displayTitle ? { displayTitle: title.displayTitle } : {},
2775
+ ...title?.titleSourceMessageId ? { titleSourceMessageId: title.titleSourceMessageId } : {},
2776
+ ...context?.channelName ? { channelName: context.channelName } : {},
2777
+ ...context?.originSurface ? { originSurface: context.originSurface } : {},
2778
+ ...context?.originRequester ? { originRequester: context.originRequester } : {},
2779
+ ...context?.startedAtMs !== void 0 ? { startedAtMs: context.startedAtMs } : {}
2780
+ };
2781
+ }
2782
+ async function getConversationDetailsForIds(conversationIds) {
2783
+ const uniqueIds = [...new Set(conversationIds)].filter(Boolean);
2784
+ const entries = await Promise.all(
2785
+ uniqueIds.map(async (id) => {
2786
+ const details = await getConversationDetails(id);
2787
+ return details ? [id, details] : void 0;
2788
+ })
2789
+ );
2790
+ const result = /* @__PURE__ */ new Map();
2791
+ for (const entry of entries) {
2792
+ if (entry) result.set(entry[0], entry[1]);
2793
+ }
2794
+ return result;
2795
+ }
2796
+
2690
2797
  export {
2691
2798
  createAgentPluginLogger,
2692
2799
  createPluginState,
@@ -2729,5 +2836,9 @@ export {
2729
2836
  resolveSlackChannelTypeFromMessage,
2730
2837
  resolveSlackConversationContext,
2731
2838
  resolveSlackConversationContextFromThreadId,
2732
- formatSlackConversationRedactedLabel
2839
+ formatSlackConversationRedactedLabel,
2840
+ initConversationContext,
2841
+ setConversationTitle,
2842
+ getConversationDetails,
2843
+ getConversationDetailsForIds
2733
2844
  };
package/dist/cli/init.js CHANGED
@@ -31,6 +31,17 @@ export const POST = (request: Request) => app.fetch(request);
31
31
  `
32
32
  );
33
33
  }
34
+ function writePluginsFile(targetDir) {
35
+ fs.writeFileSync(
36
+ path.join(targetDir, "plugins.ts"),
37
+ `import { defineJuniorPlugins } from "@sentry/junior";
38
+
39
+ export const plugins = defineJuniorPlugins([
40
+ "@sentry/junior-maintenance",
41
+ ]);
42
+ `
43
+ );
44
+ }
34
45
  function writeNitroConfig(targetDir) {
35
46
  fs.writeFileSync(
36
47
  path.join(targetDir, "nitro.config.ts"),
@@ -39,7 +50,11 @@ import { juniorNitro } from "@sentry/junior/nitro";
39
50
 
40
51
  export default defineConfig({
41
52
  preset: "vercel",
42
- modules: [juniorNitro()],
53
+ modules: [
54
+ juniorNitro({
55
+ plugins: "./plugins",
56
+ }),
57
+ ],
43
58
  routes: {
44
59
  "/**": { handler: "./server.ts" },
45
60
  },
@@ -129,6 +144,7 @@ async function runInit(dir, log = console.log) {
129
144
  },
130
145
  dependencies: {
131
146
  "@sentry/junior": "latest",
147
+ "@sentry/junior-maintenance": "latest",
132
148
  hono: "^4.12.0"
133
149
  },
134
150
  devDependencies: {
@@ -199,6 +215,7 @@ SENTRY_ORG_SLUG=
199
215
  );
200
216
  writeServerEntry(target);
201
217
  writeQueueConsumerEntry(target);
218
+ writePluginsFile(target);
202
219
  writeNitroConfig(target);
203
220
  writeViteConfig(target);
204
221
  writeVercelJson(target);
@@ -37,7 +37,10 @@ export interface DashboardRequesterIdentity {
37
37
  slackUserName?: string;
38
38
  }
39
39
  export interface DashboardSessionReport {
40
- conversationTitle?: string;
40
+ /** Always-populated display title. LLM-generated title when available, otherwise the
41
+ * Slack channel/conversation location label or a generic fallback. Privacy redaction
42
+ * wins over everything else for non-public conversations. */
43
+ displayTitle: string;
41
44
  cumulativeDurationMs: number;
42
45
  cumulativeUsage?: DashboardTurnUsage;
43
46
  conversationId: string;
@@ -48,7 +51,6 @@ export interface DashboardSessionReport {
48
51
  lastProgressAt: string;
49
52
  completedAt?: string;
50
53
  surface: DashboardSurface;
51
- title: string;
52
54
  requesterIdentity?: DashboardRequesterIdentity;
53
55
  channel?: string;
54
56
  channelName?: string;
@@ -93,6 +95,8 @@ export interface DashboardTurnReport extends DashboardSessionReport {
93
95
  }
94
96
  export interface DashboardConversationReport {
95
97
  conversationId: string;
98
+ /** Always-populated display title, computed the same way as DashboardSessionReport.displayTitle. */
99
+ displayTitle: string;
96
100
  generatedAt: string;
97
101
  turns: DashboardTurnReport[];
98
102
  }
package/dist/reporting.js CHANGED
@@ -6,10 +6,12 @@ import {
6
6
  formatSlackConversationRedactedLabel,
7
7
  getAgentPluginOperationalReports,
8
8
  getAgentTurnSessionRecord,
9
+ getConversationDetails,
10
+ getConversationDetailsForIds,
9
11
  listAgentTurnSessionSummaries,
10
12
  listAgentTurnSessionSummariesForConversation,
11
13
  resolveSlackConversationContextFromThreadId
12
- } from "./chunk-N3MORKTH.js";
14
+ } from "./chunk-HOGQL2H6.js";
13
15
  import {
14
16
  discoverSkills
15
17
  } from "./chunk-GT67ZWZQ.js";
@@ -89,12 +91,6 @@ function surfaceFromConversationId(conversationId) {
89
91
  function surfaceFromSummary(summary) {
90
92
  return summary.surface ?? surfaceFromConversationId(summary.conversationId);
91
93
  }
92
- function titleFromSummary(summary) {
93
- if (summary.state === "awaiting_resume" && summary.resumeReason) {
94
- return `Awaiting ${summary.resumeReason} resume`;
95
- }
96
- return `Turn ${summary.sessionId}`;
97
- }
98
94
  function requesterIdentityReport(requester) {
99
95
  if (!requester) return void 0;
100
96
  const identity = {
@@ -116,27 +112,33 @@ function turnUsageReport(usage) {
116
112
  };
117
113
  return Object.keys(report).length > 0 ? report : void 0;
118
114
  }
119
- function sessionReportFromSummary(summary, nowMs = Date.now()) {
115
+ function sessionReportFromSummary(summary, nowMs = Date.now(), details) {
120
116
  const slackThread = parseSlackThreadId(summary.conversationId);
121
117
  const privacy = resolveConversationPrivacy({
122
118
  conversationId: summary.conversationId
123
119
  });
120
+ const effectiveChannelName = details?.channelName ?? summary.channelName;
124
121
  const slackConversation = resolveSlackConversationContextFromThreadId({
125
122
  threadId: summary.conversationId,
126
- channelName: summary.channelName
123
+ channelName: effectiveChannelName
127
124
  });
128
125
  const privateLabel = privacy !== "public" ? slackConversation ? formatSlackConversationRedactedLabel(slackConversation) : PRIVATE_CONVERSATION_LABEL : void 0;
129
- const conversationTitle = privateLabel ?? summary.conversationTitle;
130
- const channelName = privateLabel ?? summary.channelName;
126
+ const channelName = privateLabel ?? effectiveChannelName;
127
+ const effectiveSurface = details?.originSurface ?? surfaceFromSummary(summary);
128
+ const displayTitle = privateLabel ?? details?.displayTitle ?? slackStatsLocationLabel({
129
+ channel: slackThread?.channelId,
130
+ channelName: effectiveChannelName
131
+ }) ?? surfaceFallbackLabel(effectiveSurface);
132
+ const effectiveRequester = details?.originRequester ?? summary.requester;
131
133
  const sentryConversationUrl = buildSentryConversationUrl(
132
134
  summary.conversationId
133
135
  );
134
136
  const sentryTraceUrl = summary.traceId ? buildSentryTraceUrl(summary.traceId) : void 0;
135
- const requesterIdentity = requesterIdentityReport(summary.requester);
137
+ const requesterIdentity = requesterIdentityReport(effectiveRequester);
136
138
  const cumulativeUsage = turnUsageReport(summary.cumulativeUsage);
137
139
  return {
138
140
  conversationId: summary.conversationId,
139
- ...conversationTitle ? { conversationTitle } : {},
141
+ displayTitle,
140
142
  id: summary.sessionId,
141
143
  status: statusFromCheckpoint(summary, nowMs),
142
144
  startedAt: new Date(summary.startedAtMs).toISOString(),
@@ -145,8 +147,7 @@ function sessionReportFromSummary(summary, nowMs = Date.now()) {
145
147
  ...summary.state === "completed" ? { completedAt: new Date(summary.updatedAtMs).toISOString() } : {},
146
148
  cumulativeDurationMs: summary.cumulativeDurationMs,
147
149
  ...cumulativeUsage ? { cumulativeUsage } : {},
148
- surface: surfaceFromSummary(summary),
149
- title: titleFromSummary(summary),
150
+ surface: effectiveSurface,
150
151
  ...requesterIdentity ? { requesterIdentity } : {},
151
152
  ...slackThread ? { channel: slackThread.channelId } : {},
152
153
  ...channelName ? { channelName } : {},
@@ -237,8 +238,27 @@ function slackStatsLocationLabel(input) {
237
238
  }
238
239
  return name || channelId;
239
240
  }
241
+ function surfaceFallbackLabel(surface) {
242
+ if (surface === "scheduler") return "Scheduler";
243
+ if (surface === "api") return "API";
244
+ if (surface === "internal") return "Internal";
245
+ return "Conversation";
246
+ }
247
+ function displayTitleFromDetails(conversationId, details) {
248
+ if (!details) return void 0;
249
+ const slackThread = parseSlackThreadId(conversationId);
250
+ const slackConversation = resolveSlackConversationContextFromThreadId({
251
+ threadId: conversationId,
252
+ channelName: details.channelName
253
+ });
254
+ const privateLabel = resolveConversationPrivacy({ conversationId }) !== "public" ? formatSlackConversationRedactedLabel(slackConversation) ?? PRIVATE_CONVERSATION_LABEL : void 0;
255
+ return privateLabel ?? details.displayTitle ?? slackStatsLocationLabel({
256
+ channel: slackThread?.channelId,
257
+ channelName: details.channelName
258
+ }) ?? (details.originSurface ? surfaceFallbackLabel(details.originSurface) : void 0);
259
+ }
240
260
  function locationLabel(turn) {
241
- return slackStatsLocationLabel(turn) ?? (turn.surface === "scheduler" ? "Scheduler" : turn.surface === "api" ? "API" : turn.surface === "internal" ? "Internal" : "Unknown");
261
+ return slackStatsLocationLabel(turn) ?? surfaceFallbackLabel(turn.surface);
242
262
  }
243
263
  function emptyStatsItem(label) {
244
264
  return {
@@ -265,7 +285,7 @@ function statusSignals(turns) {
265
285
  }
266
286
  function statsItems(map) {
267
287
  return [...map.values()].sort(
268
- (left, right) => right.conversations - left.conversations || right.durationMs - left.durationMs || left.label.localeCompare(right.label)
288
+ (left, right) => right.conversations - left.conversations || right.turns - left.turns || right.durationMs - left.durationMs || left.label.localeCompare(right.label)
269
289
  );
270
290
  }
271
291
  function newestTurn(turns) {
@@ -620,11 +640,18 @@ async function readSessions() {
620
640
  const summaries = await listAgentTurnSessionSummaries(
621
641
  DASHBOARD_SESSION_FEED_LIMIT
622
642
  );
643
+ const detailsByConversationId = await getConversationDetailsForIds(
644
+ summaries.map((s) => s.conversationId)
645
+ );
623
646
  return {
624
647
  source: "turn_session_records",
625
648
  generatedAt: new Date(nowMs).toISOString(),
626
649
  sessions: summaries.map(
627
- (summary) => sessionReportFromSummary(summary, nowMs)
650
+ (summary) => sessionReportFromSummary(
651
+ summary,
652
+ nowMs,
653
+ detailsByConversationId.get(summary.conversationId)
654
+ )
628
655
  )
629
656
  };
630
657
  }
@@ -643,13 +670,20 @@ async function readConversationStats() {
643
670
  summaries: sampledSummaries,
644
671
  truncated
645
672
  });
673
+ const detailsByConversationId = await getConversationDetailsForIds(
674
+ reportSummaries.map((summary) => summary.conversationId)
675
+ );
646
676
  return buildConversationStatsReport({
647
677
  generatedAt,
648
678
  nowMs,
649
679
  sampleLimit: DASHBOARD_CONVERSATION_STATS_LIMIT,
650
680
  sampleSize: sampledSummaries.length,
651
681
  sessions: reportSummaries.map(
652
- (summary) => sessionReportFromSummary(summary, nowMs)
682
+ (summary) => sessionReportFromSummary(
683
+ summary,
684
+ nowMs,
685
+ detailsByConversationId.get(summary.conversationId)
686
+ )
653
687
  ),
654
688
  truncated
655
689
  });
@@ -663,7 +697,11 @@ async function readPluginOperationalReports() {
663
697
  };
664
698
  }
665
699
  async function readConversation(conversationId) {
666
- const summaries = (await listAgentTurnSessionSummariesForConversation(conversationId)).sort(
700
+ const [rawSummaries, details] = await Promise.all([
701
+ listAgentTurnSessionSummariesForConversation(conversationId),
702
+ getConversationDetails(conversationId)
703
+ ]);
704
+ const summaries = rawSummaries.sort(
667
705
  (left, right) => left.startedAtMs - right.startedAtMs || left.updatedAtMs - right.updatedAtMs || left.sessionId.localeCompare(right.sessionId)
668
706
  );
669
707
  const turns = await Promise.all(
@@ -686,7 +724,7 @@ async function readConversation(conversationId) {
686
724
  const traceId = summary.traceId ?? sessionRecord?.traceId ?? (canExposeTranscript ? traceIdFromTranscript(transcript) : void 0);
687
725
  const sentryTraceUrl = traceId ? buildSentryTraceUrl(traceId) : void 0;
688
726
  return {
689
- ...sessionReportFromSummary(summary),
727
+ ...sessionReportFromSummary(summary, Date.now(), details),
690
728
  ...traceId ? { traceId } : {},
691
729
  ...sentryTraceUrl ? { sentryTraceUrl } : {},
692
730
  transcriptAvailable: Boolean(sessionRecord) && canExposeTranscript,
@@ -700,8 +738,11 @@ async function readConversation(conversationId) {
700
738
  };
701
739
  })
702
740
  );
741
+ const firstTurn = turns[0];
742
+ const displayTitle = firstTurn?.displayTitle ?? displayTitleFromDetails(conversationId, details) ?? surfaceFallbackLabel(firstTurn?.surface ?? "slack");
703
743
  return {
704
744
  conversationId,
745
+ displayTitle,
705
746
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
706
747
  turns
707
748
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior",
3
- "version": "0.69.0",
3
+ "version": "0.71.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -65,7 +65,7 @@
65
65
  "node-html-markdown": "^2.0.0",
66
66
  "yaml": "^2.9.0",
67
67
  "zod": "^4.4.3",
68
- "@sentry/junior-plugin-api": "0.69.0"
68
+ "@sentry/junior-plugin-api": "0.71.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.9.1",
@@ -78,7 +78,7 @@
78
78
  "typescript": "^6.0.3",
79
79
  "vercel": "^54.4.0",
80
80
  "vitest": "^4.1.7",
81
- "@sentry/junior-scheduler": "0.69.0"
81
+ "@sentry/junior-scheduler": "0.71.0"
82
82
  },
83
83
  "scripts": {
84
84
  "build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",