@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.
- package/dist/app.js +513 -65
- package/dist/chat/plugins/credential-hooks.d.ts +19 -0
- package/dist/chat/sandbox/egress-credentials.d.ts +1 -0
- package/dist/chat/sandbox/egress-schemas.d.ts +1 -1
- package/dist/chat/state/conversation-details.d.ts +46 -0
- package/dist/chat/state/turn-session.d.ts +0 -3
- package/dist/chat/task-execution/store.d.ts +23 -0
- package/dist/chat/task-execution/worker.d.ts +1 -1
- package/dist/{chunk-N3MORKTH.js → chunk-HOGQL2H6.js} +123 -12
- package/dist/cli/init.js +18 -1
- package/dist/reporting.d.ts +6 -2
- package/dist/reporting.js +62 -21
- package/package.json +3 -3
|
@@ -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.
|
|
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: [
|
|
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);
|
package/dist/reporting.d.ts
CHANGED
|
@@ -37,7 +37,10 @@ export interface DashboardRequesterIdentity {
|
|
|
37
37
|
slackUserName?: string;
|
|
38
38
|
}
|
|
39
39
|
export interface DashboardSessionReport {
|
|
40
|
-
|
|
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-
|
|
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:
|
|
123
|
+
channelName: effectiveChannelName
|
|
127
124
|
});
|
|
128
125
|
const privateLabel = privacy !== "public" ? slackConversation ? formatSlackConversationRedactedLabel(slackConversation) : PRIVATE_CONVERSATION_LABEL : void 0;
|
|
129
|
-
const
|
|
130
|
-
const
|
|
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(
|
|
137
|
+
const requesterIdentity = requesterIdentityReport(effectiveRequester);
|
|
136
138
|
const cumulativeUsage = turnUsageReport(summary.cumulativeUsage);
|
|
137
139
|
return {
|
|
138
140
|
conversationId: summary.conversationId,
|
|
139
|
-
|
|
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:
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
81
|
+
"@sentry/junior-scheduler": "0.71.0"
|
|
82
82
|
},
|
|
83
83
|
"scripts": {
|
|
84
84
|
"build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
|