Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions packages/junior/src/chat/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1414,8 +1414,8 @@ export function setSentryScopeContext(
// ---------------------------------------------------------------------------

export interface ErrorReference {
traceId: string;
eventId?: string;
eventId: string;
traceId?: string;
}

type SpanAttributePrimitive = string | number | boolean;
Expand Down Expand Up @@ -1657,23 +1657,24 @@ export function getActiveTraceId(): string | undefined {
}
}

/** Build a trace + event ID reference for error correlation. */
export function resolveErrorReference(eventId?: string): ErrorReference | null {
/** Resolve a Sentry error reference for user-facing error messages. */
export function resolveErrorReference(eventId: string): ErrorReference {
const traceId = getActiveTraceId();
if (!eventId && !traceId) {
return null;
}

if (!traceId) {
Comment thread
cursor[bot] marked this conversation as resolved.
return null;
}

return {
traceId,
...(eventId ? { eventId } : {}),
eventId,
...(traceId ? { traceId } : {}),
};
}

/** Build a user-facing error message that includes the Sentry event ID. */
export function buildErrorResponseMessage(reference: ErrorReference): string {
const parts = [`event_id=${reference.eventId}`];
if (reference.traceId) {
parts.push(`trace_id=${reference.traceId}`);
}
return `I ran into an internal error while processing that. Reference: \`${parts.join(" ")}\`.`;
}

// ---------------------------------------------------------------------------
// Gen-AI attribute serialization
// ---------------------------------------------------------------------------
Expand Down
20 changes: 17 additions & 3 deletions packages/junior/src/chat/runtime/reply-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import type { SlackAdapter } from "@chat-adapter/slack";
import { botConfig } from "@/chat/config";
import { getSlackMessageTs } from "@/chat/slack/message";
import {
buildErrorResponseMessage,
logException,
logInfo,
logWarn,
resolveErrorReference,
setSpanAttributes,
setTags,
withSpan,
Expand Down Expand Up @@ -302,7 +304,7 @@ export function createReplyToThread(deps: ReplyExecutorDeps) {
try {
const toolChannelId =
preparedState.artifacts.assistantContextChannelId ?? channelId;
const reply = await deps.services.generateAssistantReply(userText, {
let reply = await deps.services.generateAssistantReply(userText, {
requester: {
userId: message.author.userId,
userName: message.author.userName ?? fallbackIdentity?.userName,
Expand Down Expand Up @@ -398,16 +400,22 @@ export function createReplyToThread(deps: ReplyExecutorDeps) {
reply.diagnostics.errorMessage ??
"Provider error without explicit message",
);
logException(
const eventId = logException(
providerError,
"agent_turn_provider_error",
diagnosticsContext,
diagnosticsAttributes,
"Agent turn failed with provider error",
);
if (eventId) {
reply = {
...reply,
text: buildErrorResponseMessage(resolveErrorReference(eventId)),
};
}
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
} else if (reply.diagnostics.outcome !== "success") {
const failureReason = getExecutionFailureReason(reply);
logException(
const eventId = logException(
new Error(`Agent turn execution failure: ${failureReason}`),
"agent_turn_execution_failure",
diagnosticsContext,
Expand All @@ -417,6 +425,12 @@ export function createReplyToThread(deps: ReplyExecutorDeps) {
},
"Agent turn completed with execution failure",
);
if (eventId) {
reply = {
...reply,
text: buildErrorResponseMessage(resolveErrorReference(eventId)),
};
}
}

markConversationMessage(
Expand Down
17 changes: 7 additions & 10 deletions packages/junior/src/chat/runtime/slack-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Message, Thread } from "chat";
import { getSubscribedReplyPreflightDecision } from "@/chat/services/subscribed-decision";
import { isRetryableTurnError } from "@/chat/runtime/turn";
import type { ErrorReference } from "@/chat/logging";
import { buildErrorResponseMessage, type ErrorReference } from "@/chat/logging";
import { getSlackErrorObservabilityAttributes } from "@/chat/runtime/thread-context";
import type { SubscribedReplyDecision } from "@/chat/services/subscribed-reply-policy";

Expand Down Expand Up @@ -89,7 +89,7 @@ export interface SlackTurnRuntimeDependencies<TPreparedState> {
) => void;
modelId: string;
now: () => number;
getErrorReference: (eventId?: string) => ErrorReference | null;
getErrorReference: (eventId: string) => ErrorReference;
recordSkippedSubscribedMessage: (args: {
completedAtMs: number;
decision: SubscribedReplyDecision;
Expand Down Expand Up @@ -146,14 +146,11 @@ export interface SlackTurnRuntimeDependencies<TPreparedState> {
) => Promise<void>;
}

function buildFailureMessage(reference: ErrorReference | null): string {
function buildFailureMessage(reference: ErrorReference | undefined): string {
if (!reference) {
return "I ran into an internal error while processing that. Please try again.";
}
if (reference.eventId) {
return `I ran into an internal error while processing that. Reference: \`event_id=${reference.eventId} trace_id=${reference.traceId}\`.`;
}
return `I ran into an internal error while processing that. Reference: \`trace_id=${reference.traceId}\`.`;
return buildErrorResponseMessage(reference);
}

export interface SlackTurnRuntime<
Expand Down Expand Up @@ -215,7 +212,7 @@ export function createSlackTurnRuntime<

const postFallbackErrorReplyWithLogging = async (args: {
thread: Thread;
reference: ErrorReference | null;
reference: ErrorReference | undefined;
errorContext: RuntimeLogContext;
eventId?: string;
postFailureEventName: string;
Expand Down Expand Up @@ -336,7 +333,7 @@ export function createSlackTurnRuntime<
"onNewMention failed",
);
await hooks?.beforeFirstResponsePost?.();
const reference = deps.getErrorReference(eventId);
const reference = eventId ? deps.getErrorReference(eventId) : undefined;
await postFallbackErrorReplyWithLogging({
thread,
reference,
Expand Down Expand Up @@ -492,7 +489,7 @@ export function createSlackTurnRuntime<
"onSubscribedMessage failed",
);
await hooks?.beforeFirstResponsePost?.();
const reference = deps.getErrorReference(eventId);
const reference = eventId ? deps.getErrorReference(eventId) : undefined;
await postFallbackErrorReplyWithLogging({
thread,
reference,
Expand Down
16 changes: 2 additions & 14 deletions packages/junior/src/chat/slack/output.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { FileUpload, PostableMessage } from "chat";
import { logWarn } from "@/chat/logging";
import { renderSlackMrkdwn } from "@/chat/slack/mrkdwn";

const MAX_INLINE_CHARS = 2200;
Expand Down Expand Up @@ -338,20 +337,9 @@ export function buildSlackOutputMessage(
};
}

logWarn(
"slack_output_normalized_empty",
Comment thread
cursor[bot] marked this conversation as resolved.
{},
{
"app.output.original_length": text.length,
"app.output.parsed_length": normalized.length,
"app.output.file_count": fileCount,
},
"Slack output normalized to empty content",
throw new Error(
`Slack output normalized to empty content: original_length=${text.length} parsed_length=${normalized.length}`,
);
return {
markdown: "I couldn't produce a response.",
files,
};
}

return {
Expand Down
12 changes: 6 additions & 6 deletions packages/junior/tests/integration/slack/bot-handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,12 +484,12 @@ describe("bot handlers (integration)", () => {
);

expect(thread.posts).toHaveLength(1);
expect(thread.posts[0]).toEqual(
expect.objectContaining({
markdown:
"Partial output...\n\n[Response interrupted before completion]",
}),
);
const postText =
typeof thread.posts[0] === "string"
? thread.posts[0]
: ((thread.posts[0] as { markdown?: string }).markdown ?? "");
expect(postText).toContain("I ran into an internal error");
expect(postText).toContain("event_id=");
});

it("emits assistant status updates in shared channel threads", async () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/junior/tests/unit/runtime/slack-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe("createSlackTurnRuntime", () => {
assistantUserName: "junior",
decideSubscribedReply,
getChannelId: () => "C123",
getErrorReference: () => null,
getErrorReference: (eventId: string) => ({ eventId }),
getPreparedConversationContext: () => "prior thread context",
getRunId: () => "run_123",
getThreadId: () => "thread_123",
Expand Down
13 changes: 6 additions & 7 deletions packages/junior/tests/unit/slack/slack-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function createMockDeps(
assistantUserName: "test-bot",
modelId: "test-model",
now: () => 1700000000000,
getErrorReference: () => null,
getErrorReference: (eventId: string) => ({ eventId }),
getChannelId: (_thread, message) => message.threadId?.split(":")[1],
getThreadId: (_thread, message) => message.threadId,
getRunId: () => undefined,
Expand Down Expand Up @@ -107,9 +107,9 @@ describe("createSlackTurnRuntime", () => {
replyToThread: vi.fn().mockRejectedValue(replyError),
withSpan: vi.fn(async (_n, _o, _c, cb) => cb()),
logException: vi.fn(() => "evt_123"),
getErrorReference: () => ({
getErrorReference: (_eventId: string) => ({
eventId: "evt_123",
traceId: "trace_ignored",
traceId: "trace_abc",
}),
});
const runtime = createSlackTurnRuntime<TestState>(deps);
Expand All @@ -119,17 +119,16 @@ describe("createSlackTurnRuntime", () => {
await runtime.handleNewMention(thread, message);

expect(thread.posts).toContain(
"I ran into an internal error while processing that. Reference: `event_id=evt_123 trace_id=trace_ignored`.",
"I ran into an internal error while processing that. Reference: `event_id=evt_123 trace_id=trace_abc`.",
);
});

it("falls back to trace id when sentry event id is unavailable", async () => {
it("falls back to generic message when sentry capture returns no event id", async () => {
const replyError = new Error("reply failed");
const deps = createMockDeps({
replyToThread: vi.fn().mockRejectedValue(replyError),
withSpan: vi.fn(async (_n, _o, _c, cb) => cb()),
logException: vi.fn(() => undefined),
getErrorReference: () => ({ traceId: "trace_123" }),
});
const runtime = createSlackTurnRuntime<TestState>(deps);
const thread = createTestThread({});
Expand All @@ -138,7 +137,7 @@ describe("createSlackTurnRuntime", () => {
await runtime.handleNewMention(thread, message);

expect(thread.posts).toContain(
"I ran into an internal error while processing that. Reference: `trace_id=trace_123`.",
"I ran into an internal error while processing that. Please try again.",
);
});
});
Expand Down
Loading