@sentry/junior 0.9.4 → 0.10.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/README.md +0 -7
- package/dist/{chunk-FLP5I4NM.js → chunk-7RM2Y52T.js} +283 -589
- package/dist/{chunk-FS5Y4CF2.js → chunk-BJ4EBVQK.js} +17 -34
- package/dist/cli/init.js +21 -12
- package/dist/cli/snapshot-warmup.js +1 -1
- package/dist/handlers/router.d.ts +2 -4
- package/dist/handlers/router.js +26 -48
- package/dist/handlers/webhooks.d.ts +8 -1
- package/dist/handlers/webhooks.js +6 -5
- package/dist/next-config.js +0 -1
- package/package.json +16 -18
- package/dist/chunk-SO4ORZJR.js +0 -103
- package/dist/chunk-YGERYE7Y.js +0 -685
- package/dist/handlers/queue-callback.d.ts +0 -10
- package/dist/handlers/queue-callback.js +0 -11
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
discoverSkills,
|
|
3
|
+
findSkillByName,
|
|
4
|
+
getCapabilityProvider,
|
|
5
|
+
listCapabilityProviders,
|
|
6
|
+
loadSkillsByName,
|
|
7
|
+
logCapabilityCatalogLoadedOnce,
|
|
8
|
+
parseSkillInvocation,
|
|
9
|
+
stripFrontmatter
|
|
10
|
+
} from "./chunk-WM66QDLA.js";
|
|
1
11
|
import {
|
|
2
12
|
SANDBOX_SKILLS_ROOT,
|
|
3
13
|
SANDBOX_WORKSPACE_ROOT,
|
|
4
14
|
botConfig,
|
|
5
15
|
buildNonInteractiveShellScript,
|
|
6
|
-
|
|
16
|
+
getChatConfig,
|
|
7
17
|
getRuntimeDependencyProfileHash,
|
|
8
18
|
getRuntimeMetadata,
|
|
9
19
|
getSlackBotToken,
|
|
@@ -17,20 +27,11 @@ import {
|
|
|
17
27
|
runNonInteractiveCommand,
|
|
18
28
|
sandboxSkillDir,
|
|
19
29
|
toOptionalTrimmed
|
|
20
|
-
} from "./chunk-
|
|
21
|
-
import {
|
|
22
|
-
discoverSkills,
|
|
23
|
-
findSkillByName,
|
|
24
|
-
getCapabilityProvider,
|
|
25
|
-
listCapabilityProviders,
|
|
26
|
-
loadSkillsByName,
|
|
27
|
-
logCapabilityCatalogLoadedOnce,
|
|
28
|
-
parseSkillInvocation,
|
|
29
|
-
stripFrontmatter
|
|
30
|
-
} from "./chunk-WM66QDLA.js";
|
|
30
|
+
} from "./chunk-BJ4EBVQK.js";
|
|
31
31
|
import {
|
|
32
32
|
CredentialUnavailableError,
|
|
33
33
|
createPluginBroker,
|
|
34
|
+
createRequestContext,
|
|
34
35
|
extractGenAiUsageAttributes,
|
|
35
36
|
getPluginDefinition,
|
|
36
37
|
getPluginMcpProviders,
|
|
@@ -57,102 +58,9 @@ import {
|
|
|
57
58
|
soulPathCandidates
|
|
58
59
|
} from "./chunk-KCLEEKYX.js";
|
|
59
60
|
|
|
60
|
-
// src/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
reason;
|
|
64
|
-
constructor(reason, threadId, details) {
|
|
65
|
-
if (reason === "thread_locked") {
|
|
66
|
-
super(
|
|
67
|
-
`Queue message deferred because thread ${threadId} is already locked`
|
|
68
|
-
);
|
|
69
|
-
} else {
|
|
70
|
-
super(
|
|
71
|
-
`Queue message deferred for thread ${threadId} because activeTurnId=${details?.activeTurnId ?? "unknown"} is still in progress for currentTurnId=${details?.currentTurnId ?? "unknown"}`
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
this.name = "DeferredThreadMessageError";
|
|
75
|
-
this.reason = reason;
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
function isDeferredThreadMessageError(error, reason) {
|
|
79
|
-
if (!(error instanceof DeferredThreadMessageError)) {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
if (!reason) {
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
return error.reason === reason;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// src/chat/queue/transport.ts
|
|
89
|
-
import { handleCallback, send } from "@vercel/queue";
|
|
90
|
-
async function sendQueueMessage(topicName, payload, options) {
|
|
91
|
-
const result = await send(topicName, payload, {
|
|
92
|
-
...options?.idempotencyKey ? { idempotencyKey: options.idempotencyKey } : {}
|
|
93
|
-
});
|
|
94
|
-
return result.messageId ?? void 0;
|
|
95
|
-
}
|
|
96
|
-
function createTransportCallbackHandler(handler, options) {
|
|
97
|
-
return handleCallback(
|
|
98
|
-
async (message, metadata) => {
|
|
99
|
-
await handler(message, {
|
|
100
|
-
messageId: metadata.messageId,
|
|
101
|
-
deliveryCount: metadata.deliveryCount,
|
|
102
|
-
topicName: metadata.topicName
|
|
103
|
-
});
|
|
104
|
-
},
|
|
105
|
-
options ? {
|
|
106
|
-
retry: options.retry ? (error, metadata) => options.retry?.(error, {
|
|
107
|
-
messageId: metadata.messageId,
|
|
108
|
-
deliveryCount: metadata.deliveryCount,
|
|
109
|
-
topicName: metadata.topicName
|
|
110
|
-
}) : void 0
|
|
111
|
-
} : void 0
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// src/chat/queue/client.ts
|
|
116
|
-
var THREAD_MESSAGE_TOPIC = "junior-thread-message";
|
|
117
|
-
var MAX_DELIVERY_ATTEMPTS = 10;
|
|
118
|
-
var THREAD_LOCK_RETRY_MAX_SECONDS = 30;
|
|
119
|
-
var ACTIVE_TURN_RETRY_MAX_SECONDS = 300;
|
|
120
|
-
function getThreadMessageTopic() {
|
|
121
|
-
return THREAD_MESSAGE_TOPIC;
|
|
122
|
-
}
|
|
123
|
-
async function enqueueThreadMessage(payload, options) {
|
|
124
|
-
return await sendQueueMessage(getThreadMessageTopic(), payload, options);
|
|
125
|
-
}
|
|
126
|
-
function createQueueCallbackHandler(handler) {
|
|
127
|
-
return createTransportCallbackHandler(handler, {
|
|
128
|
-
retry: (error, metadata) => {
|
|
129
|
-
if (isDeferredThreadMessageError(error, "thread_locked")) {
|
|
130
|
-
return {
|
|
131
|
-
afterSeconds: Math.min(
|
|
132
|
-
THREAD_LOCK_RETRY_MAX_SECONDS,
|
|
133
|
-
Math.max(5, metadata.deliveryCount * 5)
|
|
134
|
-
)
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
if (isDeferredThreadMessageError(error, "active_turn")) {
|
|
138
|
-
return {
|
|
139
|
-
afterSeconds: Math.min(
|
|
140
|
-
ACTIVE_TURN_RETRY_MAX_SECONDS,
|
|
141
|
-
Math.max(30, metadata.deliveryCount * 30)
|
|
142
|
-
)
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
if (metadata.deliveryCount >= MAX_DELIVERY_ATTEMPTS) {
|
|
146
|
-
return { acknowledge: true };
|
|
147
|
-
}
|
|
148
|
-
const backoffSeconds = Math.min(
|
|
149
|
-
300,
|
|
150
|
-
Math.max(5, metadata.deliveryCount * 5)
|
|
151
|
-
);
|
|
152
|
-
return { afterSeconds: backoffSeconds };
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
}
|
|
61
|
+
// src/handlers/webhooks.ts
|
|
62
|
+
import { after } from "next/server";
|
|
63
|
+
import * as Sentry from "@sentry/nextjs";
|
|
156
64
|
|
|
157
65
|
// src/chat/app/production.ts
|
|
158
66
|
import { createSlackAdapter } from "@chat-adapter/slack";
|
|
@@ -174,9 +82,10 @@ var replyDecisionSchema = z.object({
|
|
|
174
82
|
confidence: z.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
|
|
175
83
|
reason: z.string().optional().describe("Short reason for the decision.")
|
|
176
84
|
});
|
|
177
|
-
var ROUTER_CONFIDENCE_THRESHOLD = 0.
|
|
85
|
+
var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
|
|
178
86
|
var LEADING_SLACK_MENTION_RE = /^\s*<@([A-Z0-9]+)(?:\|([^>]+))?>[\s,:-]*/i;
|
|
179
87
|
var LEADING_NAMED_MENTION_RE = /^\s*@([a-z0-9._-]+)\b[\s,:-]*/i;
|
|
88
|
+
var TRANSCRIPT_MESSAGE_LINE_RE = /^\[(assistant|system|user)\]\s+[^:]+:\s+([\s\S]+)$/i;
|
|
180
89
|
var THREAD_OPTOUT_PATTERNS = [
|
|
181
90
|
/\bstop (?:watching|replying|participating)\b/i,
|
|
182
91
|
/\bstay out\b/i,
|
|
@@ -220,6 +129,36 @@ function isThreadOptOutInstruction(rawText, text) {
|
|
|
220
129
|
(pattern) => pattern.test(rawText) || pattern.test(text)
|
|
221
130
|
);
|
|
222
131
|
}
|
|
132
|
+
function getTranscriptMessageHints(conversationContext) {
|
|
133
|
+
if (!conversationContext) {
|
|
134
|
+
return {
|
|
135
|
+
latestPriorMessageRole: "[none]",
|
|
136
|
+
latestPriorAssistantMessage: "[none]"
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const lines = conversationContext.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
|
|
140
|
+
let latestPriorMessageRole = "[none]";
|
|
141
|
+
let latestPriorAssistantMessage = "[none]";
|
|
142
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
143
|
+
const match = lines[index]?.match(TRANSCRIPT_MESSAGE_LINE_RE);
|
|
144
|
+
if (!match) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (latestPriorMessageRole === "[none]") {
|
|
148
|
+
latestPriorMessageRole = match[1].toLowerCase();
|
|
149
|
+
}
|
|
150
|
+
if (latestPriorAssistantMessage === "[none]" && match[1].toLowerCase() === "assistant") {
|
|
151
|
+
latestPriorAssistantMessage = match[2];
|
|
152
|
+
}
|
|
153
|
+
if (latestPriorMessageRole !== "[none]" && latestPriorAssistantMessage !== "[none]") {
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
latestPriorMessageRole,
|
|
159
|
+
latestPriorAssistantMessage
|
|
160
|
+
};
|
|
161
|
+
}
|
|
223
162
|
function getSubscribedReplyPreflightDecision(args) {
|
|
224
163
|
const text = args.text.trim();
|
|
225
164
|
const rawText = args.rawText.trim();
|
|
@@ -241,6 +180,7 @@ function getSubscribedReplyPreflightDecision(args) {
|
|
|
241
180
|
};
|
|
242
181
|
}
|
|
243
182
|
function buildRouterSystemPrompt(botUserName, conversationContext, isExplicitMention) {
|
|
183
|
+
const { latestPriorMessageRole, latestPriorAssistantMessage } = getTranscriptMessageHints(conversationContext);
|
|
244
184
|
return [
|
|
245
185
|
"You are a message router for a Slack assistant named Junior in a subscribed Slack thread.",
|
|
246
186
|
"Decide whether Junior should reply to the latest message.",
|
|
@@ -264,8 +204,14 @@ function buildRouterSystemPrompt(botUserName, conversationContext, isExplicitMen
|
|
|
264
204
|
"",
|
|
265
205
|
"Examples of messages Junior SHOULD reply to (should_reply=true):",
|
|
266
206
|
"- Direct follow-ups to Junior's response: 'Can you explain that last point in more detail?'",
|
|
207
|
+
"- Self-referential follow-ups after Junior just answered: 'What did you just say about the budget?', 'Can you explain your last response in more detail?'",
|
|
267
208
|
"- Explicit requests for Junior's help: 'Junior, what's causing this error?'",
|
|
268
209
|
"",
|
|
210
|
+
"Treat a message as directed at Junior when it explicitly refers to Junior's immediately previous reply",
|
|
211
|
+
"using language like 'you just said', 'your last response', 'your last answer', or similar self-reference.",
|
|
212
|
+
"Do not confuse that with general topic continuation. A message like 'What about the billing worker timeline?'",
|
|
213
|
+
"still should_reply=false unless it clearly asks Junior for help.",
|
|
214
|
+
"",
|
|
269
215
|
"When in doubt, should_reply=false. Most messages in a thread are human-to-human conversation.",
|
|
270
216
|
"",
|
|
271
217
|
"If the user is clearly telling Junior to stop watching, replying, or participating in the thread,",
|
|
@@ -278,6 +224,8 @@ function buildRouterSystemPrompt(botUserName, conversationContext, isExplicitMen
|
|
|
278
224
|
"",
|
|
279
225
|
`<assistant-name>${escapeXml(botUserName)}</assistant-name>`,
|
|
280
226
|
`<explicit-mention>${isExplicitMention ? "true" : "false"}</explicit-mention>`,
|
|
227
|
+
`<latest-prior-message-role>${escapeXml(latestPriorMessageRole)}</latest-prior-message-role>`,
|
|
228
|
+
`<latest-prior-assistant-message>${escapeXml(latestPriorAssistantMessage)}</latest-prior-assistant-message>`,
|
|
281
229
|
`<thread-context>${escapeXml(conversationContext?.trim() || "[none]")}</thread-context>`
|
|
282
230
|
].join("\n");
|
|
283
231
|
}
|
|
@@ -710,16 +658,6 @@ function createSlackTurnRuntime(deps) {
|
|
|
710
658
|
);
|
|
711
659
|
return;
|
|
712
660
|
}
|
|
713
|
-
if (isRetryableTurnError(error)) {
|
|
714
|
-
deps.logException(
|
|
715
|
-
error,
|
|
716
|
-
"mention_handler_retryable_failure",
|
|
717
|
-
errorContext,
|
|
718
|
-
{ "app.turn.retryable_reason": error.reason },
|
|
719
|
-
"onNewMention failed with retryable error"
|
|
720
|
-
);
|
|
721
|
-
throw error;
|
|
722
|
-
}
|
|
723
661
|
const eventId = deps.logException(
|
|
724
662
|
error,
|
|
725
663
|
"mention_handler_failed",
|
|
@@ -852,16 +790,6 @@ function createSlackTurnRuntime(deps) {
|
|
|
852
790
|
);
|
|
853
791
|
return;
|
|
854
792
|
}
|
|
855
|
-
if (isRetryableTurnError(error)) {
|
|
856
|
-
deps.logException(
|
|
857
|
-
error,
|
|
858
|
-
"subscribed_message_handler_retryable_failure",
|
|
859
|
-
errorContext,
|
|
860
|
-
{ "app.turn.retryable_reason": error.reason },
|
|
861
|
-
"onSubscribedMessage failed with retryable error"
|
|
862
|
-
);
|
|
863
|
-
throw error;
|
|
864
|
-
}
|
|
865
793
|
const eventId = deps.logException(
|
|
866
794
|
error,
|
|
867
795
|
"subscribed_message_handler_failed",
|
|
@@ -2592,6 +2520,7 @@ function buildSystemPrompt(params) {
|
|
|
2592
2520
|
"- Use `bash` to inspect skill files from `skill_dir` and run shell commands inside the sandbox workspace.",
|
|
2593
2521
|
"- When using CLI tools through `bash`, prefer deterministic non-interactive flags and avoid commands that wait for prompts or editors.",
|
|
2594
2522
|
"- Keep routine setup and research steps silent in user-facing replies. Do not narrate duplicate checks, credential issuance, file writes, or similar internal progress unless the result is user-relevant.",
|
|
2523
|
+
"- If a routine prerequisite check finds nothing notable, omit it entirely from the final reply and report only the user-relevant outcome.",
|
|
2595
2524
|
"- Prefer a single result-focused reply after tool work completes. Only send an interim reply when you need user input or have a concrete blocking problem to report.",
|
|
2596
2525
|
"- Use `attachFile` for files that actually exist in the sandbox (for example screenshots, PDFs, logs), or for `attachment_path` values returned by `imageGenerate`.",
|
|
2597
2526
|
"- If the user asks to see/share/show a screenshot or file, attach the file with `attachFile` instead of only reporting its path.",
|
|
@@ -4893,31 +4822,6 @@ async function addReactionToMessage(input) {
|
|
|
4893
4822
|
);
|
|
4894
4823
|
return { ok: true };
|
|
4895
4824
|
}
|
|
4896
|
-
async function removeReactionFromMessage(input) {
|
|
4897
|
-
const client2 = getSlackClient();
|
|
4898
|
-
const channelId = normalizeSlackConversationId(input.channelId);
|
|
4899
|
-
if (!channelId) {
|
|
4900
|
-
throw new Error("Slack reaction requires a valid channel ID");
|
|
4901
|
-
}
|
|
4902
|
-
const timestamp = input.timestamp.trim();
|
|
4903
|
-
if (!timestamp) {
|
|
4904
|
-
throw new Error("Slack reaction requires a target message timestamp");
|
|
4905
|
-
}
|
|
4906
|
-
const emoji = normalizeSlackEmojiName(input.emoji);
|
|
4907
|
-
if (!emoji) {
|
|
4908
|
-
throw new Error("Slack reaction requires a valid emoji alias name");
|
|
4909
|
-
}
|
|
4910
|
-
await withSlackRetries(
|
|
4911
|
-
() => client2.reactions.remove({
|
|
4912
|
-
channel: channelId,
|
|
4913
|
-
timestamp,
|
|
4914
|
-
name: emoji
|
|
4915
|
-
}),
|
|
4916
|
-
3,
|
|
4917
|
-
{ action: "reactions.remove" }
|
|
4918
|
-
);
|
|
4919
|
-
return { ok: true };
|
|
4920
|
-
}
|
|
4921
4825
|
async function listChannelMessages(input) {
|
|
4922
4826
|
const client2 = getSlackClient();
|
|
4923
4827
|
const channelId = normalizeSlackConversationId(input.channelId);
|
|
@@ -8391,6 +8295,9 @@ function enforceAttachmentClaimTruth(text, hasAttachedFiles) {
|
|
|
8391
8295
|
Note: No file was attached in this turn. I need to attach the file before claiming it is shared.`;
|
|
8392
8296
|
}
|
|
8393
8297
|
|
|
8298
|
+
// src/chat/runtime/thread-state.ts
|
|
8299
|
+
import { THREAD_STATE_TTL_MS } from "chat";
|
|
8300
|
+
|
|
8394
8301
|
// src/chat/configuration/validation.ts
|
|
8395
8302
|
var CONFIG_KEY_RE = /^[a-z0-9]+(?:\.[a-z0-9-]+)+$/;
|
|
8396
8303
|
var SECRET_KEY_RE = /(?:^|[_.-])(token|secret|password|passphrase|api[-_]?key|private[-_]?key|credential|auth)(?:$|[_.-])/i;
|
|
@@ -8823,6 +8730,25 @@ function buildArtifactStatePatch(patch) {
|
|
|
8823
8730
|
}
|
|
8824
8731
|
|
|
8825
8732
|
// src/chat/runtime/thread-state.ts
|
|
8733
|
+
function threadStateKey(threadId) {
|
|
8734
|
+
return `thread-state:${threadId}`;
|
|
8735
|
+
}
|
|
8736
|
+
function buildThreadStatePayload(patch) {
|
|
8737
|
+
const payload = {};
|
|
8738
|
+
if (patch.artifacts) {
|
|
8739
|
+
Object.assign(payload, buildArtifactStatePatch(patch.artifacts));
|
|
8740
|
+
}
|
|
8741
|
+
if (patch.conversation) {
|
|
8742
|
+
Object.assign(payload, buildConversationStatePatch(patch.conversation));
|
|
8743
|
+
}
|
|
8744
|
+
if (patch.sandboxId !== void 0) {
|
|
8745
|
+
payload.app_sandbox_id = patch.sandboxId;
|
|
8746
|
+
}
|
|
8747
|
+
if (patch.sandboxDependencyProfileHash !== void 0) {
|
|
8748
|
+
payload.app_sandbox_dependency_profile_hash = patch.sandboxDependencyProfileHash;
|
|
8749
|
+
}
|
|
8750
|
+
return payload;
|
|
8751
|
+
}
|
|
8826
8752
|
function mergeArtifactsState(artifacts, patch) {
|
|
8827
8753
|
if (!patch) {
|
|
8828
8754
|
return artifacts;
|
|
@@ -8837,24 +8763,30 @@ function mergeArtifactsState(artifacts, patch) {
|
|
|
8837
8763
|
};
|
|
8838
8764
|
}
|
|
8839
8765
|
async function persistThreadState(thread, patch) {
|
|
8840
|
-
const payload =
|
|
8841
|
-
if (patch.artifacts) {
|
|
8842
|
-
Object.assign(payload, buildArtifactStatePatch(patch.artifacts));
|
|
8843
|
-
}
|
|
8844
|
-
if (patch.conversation) {
|
|
8845
|
-
Object.assign(payload, buildConversationStatePatch(patch.conversation));
|
|
8846
|
-
}
|
|
8847
|
-
if (patch.sandboxId) {
|
|
8848
|
-
payload.app_sandbox_id = patch.sandboxId;
|
|
8849
|
-
}
|
|
8850
|
-
if (patch.sandboxDependencyProfileHash) {
|
|
8851
|
-
payload.app_sandbox_dependency_profile_hash = patch.sandboxDependencyProfileHash;
|
|
8852
|
-
}
|
|
8766
|
+
const payload = buildThreadStatePayload(patch);
|
|
8853
8767
|
if (Object.keys(payload).length === 0) {
|
|
8854
8768
|
return;
|
|
8855
8769
|
}
|
|
8856
8770
|
await thread.setState(payload);
|
|
8857
8771
|
}
|
|
8772
|
+
async function getPersistedThreadState(threadId) {
|
|
8773
|
+
const stateAdapter = getStateAdapter();
|
|
8774
|
+
await stateAdapter.connect();
|
|
8775
|
+
return await stateAdapter.get(
|
|
8776
|
+
threadStateKey(threadId)
|
|
8777
|
+
) ?? {};
|
|
8778
|
+
}
|
|
8779
|
+
async function persistThreadStateById(threadId, patch) {
|
|
8780
|
+
const payload = buildThreadStatePayload(patch);
|
|
8781
|
+
if (Object.keys(payload).length === 0) {
|
|
8782
|
+
return;
|
|
8783
|
+
}
|
|
8784
|
+
const stateAdapter = getStateAdapter();
|
|
8785
|
+
await stateAdapter.connect();
|
|
8786
|
+
const key = threadStateKey(threadId);
|
|
8787
|
+
const existing = await stateAdapter.get(key) ?? {};
|
|
8788
|
+
await stateAdapter.set(key, { ...existing, ...payload }, THREAD_STATE_TTL_MS);
|
|
8789
|
+
}
|
|
8858
8790
|
function getChannelConfigurationService(thread) {
|
|
8859
8791
|
const channel = thread.channel;
|
|
8860
8792
|
return createChannelConfigurationService({
|
|
@@ -8876,14 +8808,6 @@ function getSessionIdentifiers(context) {
|
|
|
8876
8808
|
sessionId: context.correlation?.turnId
|
|
8877
8809
|
};
|
|
8878
8810
|
}
|
|
8879
|
-
var AgentTurnTimeoutError = class extends Error {
|
|
8880
|
-
timeoutMs;
|
|
8881
|
-
constructor(timeoutMs) {
|
|
8882
|
-
super(`Agent turn timed out after ${timeoutMs}ms`);
|
|
8883
|
-
this.name = "AgentTurnTimeoutError";
|
|
8884
|
-
this.timeoutMs = timeoutMs;
|
|
8885
|
-
}
|
|
8886
|
-
};
|
|
8887
8811
|
var McpAuthorizationPauseError = class extends Error {
|
|
8888
8812
|
provider;
|
|
8889
8813
|
constructor(provider) {
|
|
@@ -9502,7 +9426,11 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9502
9426
|
timeoutId = setTimeout(() => {
|
|
9503
9427
|
didTimeout = true;
|
|
9504
9428
|
agent.abort();
|
|
9505
|
-
reject(
|
|
9429
|
+
reject(
|
|
9430
|
+
new Error(
|
|
9431
|
+
`Agent turn timed out after ${botConfig.turnTimeoutMs}ms`
|
|
9432
|
+
)
|
|
9433
|
+
);
|
|
9506
9434
|
}, botConfig.turnTimeoutMs);
|
|
9507
9435
|
});
|
|
9508
9436
|
try {
|
|
@@ -9737,71 +9665,6 @@ async function generateAssistantReply(messageText, context = {}) {
|
|
|
9737
9665
|
`conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`
|
|
9738
9666
|
);
|
|
9739
9667
|
}
|
|
9740
|
-
if (error instanceof AgentTurnTimeoutError && timeoutResumeConversationId && timeoutResumeSessionId) {
|
|
9741
|
-
const nextSliceId = timeoutResumeSliceId + 1;
|
|
9742
|
-
logException(
|
|
9743
|
-
error,
|
|
9744
|
-
"agent_turn_timeout_resume_triggered",
|
|
9745
|
-
{
|
|
9746
|
-
slackThreadId: context.correlation?.threadId,
|
|
9747
|
-
slackUserId: context.correlation?.requesterId,
|
|
9748
|
-
slackChannelId: context.correlation?.channelId,
|
|
9749
|
-
runId: context.correlation?.runId,
|
|
9750
|
-
assistantUserName: context.assistant?.userName,
|
|
9751
|
-
modelId: botConfig.modelId
|
|
9752
|
-
},
|
|
9753
|
-
{
|
|
9754
|
-
"app.ai.turn_timeout_ms": error.timeoutMs,
|
|
9755
|
-
"app.ai.resume_conversation_id": timeoutResumeConversationId,
|
|
9756
|
-
"app.ai.resume_session_id": timeoutResumeSessionId,
|
|
9757
|
-
"app.ai.resume_from_slice_id": timeoutResumeSliceId,
|
|
9758
|
-
"app.ai.resume_next_slice_id": nextSliceId
|
|
9759
|
-
},
|
|
9760
|
-
"Agent turn timed out and will be resumed"
|
|
9761
|
-
);
|
|
9762
|
-
try {
|
|
9763
|
-
const latestCheckpoint = await getAgentTurnSessionCheckpoint(
|
|
9764
|
-
timeoutResumeConversationId,
|
|
9765
|
-
timeoutResumeSessionId
|
|
9766
|
-
);
|
|
9767
|
-
const piMessages = timeoutResumeMessages.length > 0 ? timeoutResumeMessages : latestCheckpoint?.piMessages ?? [];
|
|
9768
|
-
await upsertAgentTurnSessionCheckpoint({
|
|
9769
|
-
conversationId: timeoutResumeConversationId,
|
|
9770
|
-
sessionId: timeoutResumeSessionId,
|
|
9771
|
-
sliceId: nextSliceId,
|
|
9772
|
-
state: "awaiting_resume",
|
|
9773
|
-
piMessages,
|
|
9774
|
-
loadedSkillNames: loadedSkillNamesForResume,
|
|
9775
|
-
resumeReason: "timeout",
|
|
9776
|
-
resumedFromSliceId: timeoutResumeSliceId,
|
|
9777
|
-
errorMessage: error.message
|
|
9778
|
-
});
|
|
9779
|
-
} catch (checkpointError) {
|
|
9780
|
-
logException(
|
|
9781
|
-
checkpointError,
|
|
9782
|
-
"agent_turn_timeout_resume_checkpoint_failed",
|
|
9783
|
-
{
|
|
9784
|
-
slackThreadId: context.correlation?.threadId,
|
|
9785
|
-
slackUserId: context.correlation?.requesterId,
|
|
9786
|
-
slackChannelId: context.correlation?.channelId,
|
|
9787
|
-
runId: context.correlation?.runId,
|
|
9788
|
-
assistantUserName: context.assistant?.userName,
|
|
9789
|
-
modelId: botConfig.modelId
|
|
9790
|
-
},
|
|
9791
|
-
{
|
|
9792
|
-
"app.ai.resume_conversation_id": timeoutResumeConversationId,
|
|
9793
|
-
"app.ai.resume_session_id": timeoutResumeSessionId,
|
|
9794
|
-
"app.ai.resume_from_slice_id": timeoutResumeSliceId,
|
|
9795
|
-
"app.ai.resume_next_slice_id": nextSliceId
|
|
9796
|
-
},
|
|
9797
|
-
"Failed to persist timeout checkpoint before retry"
|
|
9798
|
-
);
|
|
9799
|
-
}
|
|
9800
|
-
throw new RetryableTurnError(
|
|
9801
|
-
"agent_turn_timeout_resume",
|
|
9802
|
-
`conversation=${timeoutResumeConversationId} session=${timeoutResumeSessionId} slice=${nextSliceId}`
|
|
9803
|
-
);
|
|
9804
|
-
}
|
|
9805
9668
|
if (isRetryableTurnError(error)) {
|
|
9806
9669
|
throw error;
|
|
9807
9670
|
}
|
|
@@ -11236,7 +11099,10 @@ function createReplyToThread(deps) {
|
|
|
11236
11099
|
);
|
|
11237
11100
|
}
|
|
11238
11101
|
} catch (error) {
|
|
11239
|
-
shouldPersistFailureState = !isRetryableTurnError(
|
|
11102
|
+
shouldPersistFailureState = !isRetryableTurnError(
|
|
11103
|
+
error,
|
|
11104
|
+
"mcp_auth_resume"
|
|
11105
|
+
);
|
|
11240
11106
|
throw error;
|
|
11241
11107
|
} finally {
|
|
11242
11108
|
textStream.end();
|
|
@@ -11510,57 +11376,7 @@ import {
|
|
|
11510
11376
|
Chat
|
|
11511
11377
|
} from "chat";
|
|
11512
11378
|
|
|
11513
|
-
// src/chat/state/queue-ingress-store.ts
|
|
11514
|
-
var QUEUE_INGRESS_DEDUP_PREFIX = "junior:queue_ingress";
|
|
11515
|
-
function queueIngressDedupKey(rawKey) {
|
|
11516
|
-
return `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
|
|
11517
|
-
}
|
|
11518
|
-
async function claimQueueIngressDedup(rawKey, ttlMs) {
|
|
11519
|
-
const { stateAdapter, redisStateAdapter } = await getConnectedStateContext();
|
|
11520
|
-
const key = queueIngressDedupKey(rawKey);
|
|
11521
|
-
if (redisStateAdapter) {
|
|
11522
|
-
const result = await redisStateAdapter.getClient().set(key, "1", {
|
|
11523
|
-
NX: true,
|
|
11524
|
-
PX: ttlMs
|
|
11525
|
-
});
|
|
11526
|
-
return result === "OK";
|
|
11527
|
-
}
|
|
11528
|
-
return await stateAdapter.setIfNotExists(key, "1", ttlMs);
|
|
11529
|
-
}
|
|
11530
|
-
async function hasQueueIngressDedup(rawKey) {
|
|
11531
|
-
const { stateAdapter, redisStateAdapter } = await getConnectedStateContext();
|
|
11532
|
-
const key = queueIngressDedupKey(rawKey);
|
|
11533
|
-
const value = redisStateAdapter ? await redisStateAdapter.getClient().get(key) : await stateAdapter.get(key);
|
|
11534
|
-
return typeof value === "string" && value.length > 0;
|
|
11535
|
-
}
|
|
11536
|
-
|
|
11537
11379
|
// src/chat/ingress/message-router.ts
|
|
11538
|
-
var QUEUE_INGRESS_DEDUP_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
11539
|
-
function nonEmptyString(value) {
|
|
11540
|
-
if (typeof value !== "string") return void 0;
|
|
11541
|
-
const trimmed = value.trim();
|
|
11542
|
-
return trimmed || void 0;
|
|
11543
|
-
}
|
|
11544
|
-
function serializeMessageForQueue(message) {
|
|
11545
|
-
const candidate = message;
|
|
11546
|
-
if (typeof candidate.toJSON === "function") {
|
|
11547
|
-
return candidate.toJSON();
|
|
11548
|
-
}
|
|
11549
|
-
return {
|
|
11550
|
-
_type: "chat:Message",
|
|
11551
|
-
...message
|
|
11552
|
-
};
|
|
11553
|
-
}
|
|
11554
|
-
function serializeThreadForQueue(thread) {
|
|
11555
|
-
const candidate = thread;
|
|
11556
|
-
if (typeof candidate.toJSON === "function") {
|
|
11557
|
-
return candidate.toJSON();
|
|
11558
|
-
}
|
|
11559
|
-
return {
|
|
11560
|
-
_type: "chat:Thread",
|
|
11561
|
-
...thread
|
|
11562
|
-
};
|
|
11563
|
-
}
|
|
11564
11380
|
function normalizeIncomingSlackThreadId(threadId, message) {
|
|
11565
11381
|
if (!threadId.startsWith("slack:")) {
|
|
11566
11382
|
return threadId;
|
|
@@ -11579,260 +11395,10 @@ function normalizeIncomingSlackThreadId(threadId, message) {
|
|
|
11579
11395
|
}
|
|
11580
11396
|
return `slack:${channelId}:${threadTs}`;
|
|
11581
11397
|
}
|
|
11582
|
-
function
|
|
11583
|
-
return
|
|
11584
|
-
|
|
11585
|
-
|
|
11586
|
-
const parts = threadId.split(":");
|
|
11587
|
-
return parts.length === 3 && parts[0] === "slack" && parts[1]?.startsWith("D");
|
|
11588
|
-
}
|
|
11589
|
-
function determineThreadMessageKind(args) {
|
|
11590
|
-
if (args.isDirectMessage) {
|
|
11591
|
-
return "new_mention";
|
|
11592
|
-
}
|
|
11593
|
-
if (args.isSubscribed) {
|
|
11594
|
-
return "subscribed_message";
|
|
11595
|
-
}
|
|
11596
|
-
if (args.isMention) {
|
|
11597
|
-
return "new_mention";
|
|
11598
|
-
}
|
|
11599
|
-
return void 0;
|
|
11600
|
-
}
|
|
11601
|
-
function getMessageLogContext(args) {
|
|
11602
|
-
return {
|
|
11603
|
-
slackThreadId: args.normalizedThreadId,
|
|
11604
|
-
slackChannelId: nonEmptyString(args.message.raw?.channel),
|
|
11605
|
-
slackUserId: args.message.author?.userId
|
|
11606
|
-
};
|
|
11607
|
-
}
|
|
11608
|
-
function logIgnoredIngressResult(args) {
|
|
11609
|
-
logInfo(
|
|
11610
|
-
args.eventName,
|
|
11611
|
-
args.logContext,
|
|
11612
|
-
{
|
|
11613
|
-
...args.messageId ? { "messaging.message.id": args.messageId } : {},
|
|
11614
|
-
...args.kind ? { "app.queue.message_kind": args.kind } : {},
|
|
11615
|
-
...args.dedupKey ? { "app.queue.dedup_key": args.dedupKey } : {},
|
|
11616
|
-
...args.decisionReason ? { "app.decision.reason": args.decisionReason } : {},
|
|
11617
|
-
"app.queue.route_result": args.routeResult
|
|
11618
|
-
},
|
|
11619
|
-
args.body
|
|
11620
|
-
);
|
|
11621
|
-
}
|
|
11622
|
-
async function enqueueQueueIngressMessage(args) {
|
|
11623
|
-
if (args.enqueueThreadMessage) {
|
|
11624
|
-
return await args.enqueueThreadMessage(args.payload, args.dedupKey);
|
|
11625
|
-
}
|
|
11626
|
-
return await enqueueThreadMessage(args.payload, {
|
|
11627
|
-
idempotencyKey: args.dedupKey
|
|
11628
|
-
});
|
|
11629
|
-
}
|
|
11630
|
-
async function routeIncomingMessageToQueue(args) {
|
|
11631
|
-
const { adapter, runtime } = args;
|
|
11632
|
-
const message = args.message;
|
|
11633
|
-
if (!message || typeof message !== "object") {
|
|
11634
|
-
return "ignored_non_object";
|
|
11635
|
-
}
|
|
11636
|
-
const normalizedThreadId = normalizeIncomingSlackThreadId(
|
|
11637
|
-
args.threadId,
|
|
11638
|
-
message
|
|
11639
|
-
);
|
|
11640
|
-
const baseLogContext = getMessageLogContext({
|
|
11641
|
-
message,
|
|
11642
|
-
normalizedThreadId
|
|
11643
|
-
});
|
|
11644
|
-
if ("threadId" in message) {
|
|
11645
|
-
message.threadId = normalizedThreadId;
|
|
11646
|
-
}
|
|
11647
|
-
const typedMessage = message;
|
|
11648
|
-
if (typedMessage.author?.isMe) {
|
|
11649
|
-
logIgnoredIngressResult({
|
|
11650
|
-
eventName: "queue_ingress_ignored_self_message",
|
|
11651
|
-
logContext: baseLogContext,
|
|
11652
|
-
messageId: nonEmptyString(typedMessage.id),
|
|
11653
|
-
routeResult: "ignored_self_message",
|
|
11654
|
-
body: "Ignoring self-authored message before queue routing"
|
|
11655
|
-
});
|
|
11656
|
-
return "ignored_self_message";
|
|
11657
|
-
}
|
|
11658
|
-
const messageId = nonEmptyString(typedMessage.id);
|
|
11659
|
-
if (!messageId) {
|
|
11660
|
-
logIgnoredIngressResult({
|
|
11661
|
-
eventName: "queue_ingress_ignored_missing_message_id",
|
|
11662
|
-
logContext: baseLogContext,
|
|
11663
|
-
routeResult: "ignored_missing_message_id",
|
|
11664
|
-
body: "Ignoring message without an id before queue routing"
|
|
11665
|
-
});
|
|
11666
|
-
return "ignored_missing_message_id";
|
|
11667
|
-
}
|
|
11668
|
-
const isSubscribed = await getStateAdapter().isSubscribed(normalizedThreadId);
|
|
11669
|
-
const mentionSource = typedMessage.isMention ? "sdk_flag" : runtime.detectMention?.(adapter, message) ? "fallback_detector" : void 0;
|
|
11670
|
-
const isMention = mentionSource !== void 0;
|
|
11671
|
-
if (isMention && !typedMessage.isMention) {
|
|
11672
|
-
typedMessage.isMention = true;
|
|
11673
|
-
}
|
|
11674
|
-
const isDirectMessage = isSlackDirectMessageThreadId(normalizedThreadId);
|
|
11675
|
-
const kind = determineThreadMessageKind({
|
|
11676
|
-
isDirectMessage,
|
|
11677
|
-
isSubscribed,
|
|
11678
|
-
isMention
|
|
11679
|
-
});
|
|
11680
|
-
if (!kind) {
|
|
11681
|
-
logIgnoredIngressResult({
|
|
11682
|
-
eventName: "queue_ingress_ignored_unsubscribed_non_mention",
|
|
11683
|
-
logContext: baseLogContext,
|
|
11684
|
-
messageId,
|
|
11685
|
-
routeResult: "ignored_unsubscribed_non_mention",
|
|
11686
|
-
body: "Ignoring unsubscribed non-mention message before queue routing"
|
|
11687
|
-
});
|
|
11688
|
-
return "ignored_unsubscribed_non_mention";
|
|
11689
|
-
}
|
|
11690
|
-
const dedupKey = buildQueueIngressDedupKey(normalizedThreadId, messageId);
|
|
11691
|
-
const alreadyDeduped = await hasQueueIngressDedup(dedupKey);
|
|
11692
|
-
if (alreadyDeduped) {
|
|
11693
|
-
logInfo(
|
|
11694
|
-
"queue_ingress_dedup_hit",
|
|
11695
|
-
baseLogContext,
|
|
11696
|
-
{
|
|
11697
|
-
"messaging.message.id": messageId,
|
|
11698
|
-
"app.queue.message_kind": kind,
|
|
11699
|
-
"app.queue.dedup_key": dedupKey,
|
|
11700
|
-
"app.queue.dedup_outcome": "duplicate",
|
|
11701
|
-
...mentionSource ? { "app.slack.mention_source": mentionSource } : {},
|
|
11702
|
-
"app.queue.route_result": "ignored_duplicate"
|
|
11703
|
-
},
|
|
11704
|
-
"Skipping duplicate incoming message before queue enqueue"
|
|
11705
|
-
);
|
|
11706
|
-
return "ignored_duplicate";
|
|
11707
|
-
}
|
|
11708
|
-
const thread = await runtime.createThread(
|
|
11709
|
-
adapter,
|
|
11710
|
-
normalizedThreadId,
|
|
11711
|
-
message,
|
|
11712
|
-
isSubscribed
|
|
11713
|
-
);
|
|
11714
|
-
const serializedMessage = serializeMessageForQueue(message);
|
|
11715
|
-
const serializedThread = serializeThreadForQueue(thread);
|
|
11716
|
-
const payload = {
|
|
11717
|
-
dedupKey,
|
|
11718
|
-
kind,
|
|
11719
|
-
message: serializedMessage,
|
|
11720
|
-
normalizedThreadId,
|
|
11721
|
-
thread: serializedThread
|
|
11722
|
-
};
|
|
11723
|
-
await withContext(
|
|
11724
|
-
{
|
|
11725
|
-
slackThreadId: normalizedThreadId,
|
|
11726
|
-
slackChannelId: thread.channelId,
|
|
11727
|
-
slackUserId: message.author.userId
|
|
11728
|
-
},
|
|
11729
|
-
async () => {
|
|
11730
|
-
let processingReactionAdded = false;
|
|
11731
|
-
let queueMessageId;
|
|
11732
|
-
try {
|
|
11733
|
-
await addReactionToMessage({
|
|
11734
|
-
channelId: thread.channelId,
|
|
11735
|
-
timestamp: messageId,
|
|
11736
|
-
emoji: "eyes"
|
|
11737
|
-
});
|
|
11738
|
-
processingReactionAdded = true;
|
|
11739
|
-
} catch (error) {
|
|
11740
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
11741
|
-
logWarn(
|
|
11742
|
-
"queue_ingress_reaction_add_failed",
|
|
11743
|
-
{},
|
|
11744
|
-
{
|
|
11745
|
-
"messaging.message.id": messageId,
|
|
11746
|
-
"app.queue.message_kind": kind,
|
|
11747
|
-
...mentionSource ? { "app.slack.mention_source": mentionSource } : {},
|
|
11748
|
-
"error.message": errorMessage
|
|
11749
|
-
},
|
|
11750
|
-
"Failed to add ingress processing reaction"
|
|
11751
|
-
);
|
|
11752
|
-
}
|
|
11753
|
-
try {
|
|
11754
|
-
await withSpan(
|
|
11755
|
-
"queue.enqueue_message",
|
|
11756
|
-
"queue.enqueue_message",
|
|
11757
|
-
{
|
|
11758
|
-
slackThreadId: normalizedThreadId,
|
|
11759
|
-
slackChannelId: thread.channelId,
|
|
11760
|
-
slackUserId: message.author.userId
|
|
11761
|
-
},
|
|
11762
|
-
async () => {
|
|
11763
|
-
queueMessageId = await enqueueQueueIngressMessage({
|
|
11764
|
-
dedupKey,
|
|
11765
|
-
enqueueThreadMessage: args.enqueueThreadMessage,
|
|
11766
|
-
payload
|
|
11767
|
-
});
|
|
11768
|
-
if (queueMessageId) {
|
|
11769
|
-
setSpanAttributes({
|
|
11770
|
-
"app.queue.message_id": queueMessageId
|
|
11771
|
-
});
|
|
11772
|
-
}
|
|
11773
|
-
},
|
|
11774
|
-
{
|
|
11775
|
-
"messaging.message.id": messageId,
|
|
11776
|
-
"app.queue.message_kind": kind
|
|
11777
|
-
}
|
|
11778
|
-
);
|
|
11779
|
-
} catch (error) {
|
|
11780
|
-
if (processingReactionAdded) {
|
|
11781
|
-
try {
|
|
11782
|
-
await removeReactionFromMessage({
|
|
11783
|
-
channelId: thread.channelId,
|
|
11784
|
-
timestamp: messageId,
|
|
11785
|
-
emoji: "eyes"
|
|
11786
|
-
});
|
|
11787
|
-
} catch (cleanupError) {
|
|
11788
|
-
const cleanupErrorMessage = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
|
|
11789
|
-
logWarn(
|
|
11790
|
-
"queue_ingress_reaction_cleanup_failed",
|
|
11791
|
-
{},
|
|
11792
|
-
{
|
|
11793
|
-
"messaging.message.id": messageId,
|
|
11794
|
-
"app.queue.message_kind": kind,
|
|
11795
|
-
"error.message": cleanupErrorMessage
|
|
11796
|
-
},
|
|
11797
|
-
"Failed to remove ingress processing reaction after enqueue failure"
|
|
11798
|
-
);
|
|
11799
|
-
}
|
|
11800
|
-
}
|
|
11801
|
-
throw error;
|
|
11802
|
-
}
|
|
11803
|
-
logInfo(
|
|
11804
|
-
"queue_ingress_enqueued",
|
|
11805
|
-
{},
|
|
11806
|
-
{
|
|
11807
|
-
"messaging.message.id": messageId,
|
|
11808
|
-
"app.queue.message_kind": kind,
|
|
11809
|
-
...mentionSource ? { "app.slack.mention_source": mentionSource } : {},
|
|
11810
|
-
"app.queue.dedup_key": dedupKey,
|
|
11811
|
-
"app.queue.dedup_outcome": "primary",
|
|
11812
|
-
"app.queue.route_result": "routed",
|
|
11813
|
-
...queueMessageId ? { "app.queue.message_id": queueMessageId } : {}
|
|
11814
|
-
},
|
|
11815
|
-
"Routing incoming message to queue"
|
|
11816
|
-
);
|
|
11817
|
-
const marked = await claimQueueIngressDedup(
|
|
11818
|
-
dedupKey,
|
|
11819
|
-
QUEUE_INGRESS_DEDUP_TTL_MS
|
|
11820
|
-
);
|
|
11821
|
-
if (!marked) {
|
|
11822
|
-
logInfo(
|
|
11823
|
-
"queue_ingress_dedup_mark_failed",
|
|
11824
|
-
{},
|
|
11825
|
-
{
|
|
11826
|
-
"messaging.message.id": messageId,
|
|
11827
|
-
"app.queue.message_kind": kind,
|
|
11828
|
-
"app.queue.dedup_key": dedupKey
|
|
11829
|
-
},
|
|
11830
|
-
"Queue ingress dedup state write failed after enqueue"
|
|
11831
|
-
);
|
|
11832
|
-
}
|
|
11833
|
-
}
|
|
11834
|
-
);
|
|
11835
|
-
return "routed";
|
|
11398
|
+
function nonEmptyString(value) {
|
|
11399
|
+
if (typeof value !== "string") return void 0;
|
|
11400
|
+
const trimmed = value.trim();
|
|
11401
|
+
return trimmed || void 0;
|
|
11836
11402
|
}
|
|
11837
11403
|
|
|
11838
11404
|
// src/chat/ingress/junior-chat.ts
|
|
@@ -11843,42 +11409,62 @@ function enqueueBackgroundTask(options, task) {
|
|
|
11843
11409
|
options.waitUntil(task);
|
|
11844
11410
|
}
|
|
11845
11411
|
var JuniorChat = class extends Chat {
|
|
11412
|
+
/**
|
|
11413
|
+
* Normalize Slack thread IDs before the SDK's concurrency queue.
|
|
11414
|
+
*
|
|
11415
|
+
* The SDK uses the `threadId` parameter as the lock/queue key
|
|
11416
|
+
* (Chat.handleIncomingMessage → getLockKey). @chat-adapter/slack
|
|
11417
|
+
* (as of 4.22.0) builds DM thread IDs as `slack:<channel>:` (empty
|
|
11418
|
+
* thread_ts) when the Slack event has no `thread_ts` field — it uses
|
|
11419
|
+
* `event.thread_ts || ""` instead of falling back to `event.ts`.
|
|
11420
|
+
* See @chat-adapter/slack/dist/index.js:1466.
|
|
11421
|
+
*
|
|
11422
|
+
* A DM root event arrives as `slack:D123:` while a reply in the same
|
|
11423
|
+
* thread carries `slack:D123:<ts>`, splitting the lock/state/subscription
|
|
11424
|
+
* keys and breaking conversation continuity.
|
|
11425
|
+
*
|
|
11426
|
+
* We fix this by resolving the message eagerly (even when the adapter
|
|
11427
|
+
* provides a factory), deriving the canonical thread ID from
|
|
11428
|
+
* `raw.channel` + `raw.thread_ts ?? raw.ts`, and passing both the
|
|
11429
|
+
* normalized threadId and concrete message to super.processMessage.
|
|
11430
|
+
*
|
|
11431
|
+
* Remove this override when @chat-adapter/slack uses `event.ts` as
|
|
11432
|
+
* the DM thread_ts fallback.
|
|
11433
|
+
*/
|
|
11846
11434
|
processMessage(adapter, threadId, messageOrFactory, options) {
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
|
|
11851
|
-
|
|
11852
|
-
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
threadId,
|
|
11856
|
-
message,
|
|
11857
|
-
runtime: {
|
|
11858
|
-
createThread: runtime.createThread.bind(
|
|
11859
|
-
this
|
|
11860
|
-
),
|
|
11861
|
-
detectMention: runtime.detectMention?.bind(this)
|
|
11862
|
-
}
|
|
11863
|
-
});
|
|
11864
|
-
if (result === "ignored_missing_message_id") {
|
|
11865
|
-
const normalizedThreadId = normalizeIncomingSlackThreadId(
|
|
11435
|
+
if (typeof messageOrFactory === "function") {
|
|
11436
|
+
const runtime = this;
|
|
11437
|
+
enqueueBackgroundTask(
|
|
11438
|
+
options,
|
|
11439
|
+
(async () => {
|
|
11440
|
+
try {
|
|
11441
|
+
const message = await messageOrFactory();
|
|
11442
|
+
const normalized2 = normalizeIncomingSlackThreadId(
|
|
11866
11443
|
threadId,
|
|
11867
11444
|
message
|
|
11868
11445
|
);
|
|
11869
|
-
|
|
11870
|
-
threadId
|
|
11871
|
-
|
|
11446
|
+
if (normalized2 !== threadId && "threadId" in message) {
|
|
11447
|
+
message.threadId = normalized2;
|
|
11448
|
+
}
|
|
11449
|
+
super.processMessage(adapter, normalized2, message, options);
|
|
11450
|
+
} catch (error) {
|
|
11451
|
+
runtime.logger?.error?.("Message factory resolution error", {
|
|
11452
|
+
error,
|
|
11453
|
+
threadId
|
|
11872
11454
|
});
|
|
11873
11455
|
}
|
|
11874
|
-
}
|
|
11875
|
-
|
|
11876
|
-
|
|
11877
|
-
|
|
11878
|
-
|
|
11879
|
-
|
|
11880
|
-
|
|
11456
|
+
})()
|
|
11457
|
+
);
|
|
11458
|
+
return;
|
|
11459
|
+
}
|
|
11460
|
+
const normalized = normalizeIncomingSlackThreadId(
|
|
11461
|
+
threadId,
|
|
11462
|
+
messageOrFactory
|
|
11881
11463
|
);
|
|
11464
|
+
if (normalized !== threadId && "threadId" in messageOrFactory) {
|
|
11465
|
+
messageOrFactory.threadId = normalized;
|
|
11466
|
+
}
|
|
11467
|
+
super.processMessage(adapter, normalized, messageOrFactory, options);
|
|
11882
11468
|
}
|
|
11883
11469
|
processReaction(event, options) {
|
|
11884
11470
|
const runtime = this;
|
|
@@ -12159,6 +11745,15 @@ async function publishAppHomeView(slackClient, userId, userTokenStore) {
|
|
|
12159
11745
|
await slackClient.views.publish({ user_id: userId, view });
|
|
12160
11746
|
}
|
|
12161
11747
|
|
|
11748
|
+
// src/chat/queue/thread-message-dispatcher.ts
|
|
11749
|
+
function rehydrateAttachmentFetchers(message, downloadPrivateSlackFile2 = downloadPrivateSlackFile) {
|
|
11750
|
+
for (const attachment of message.attachments) {
|
|
11751
|
+
if (!attachment.fetchData && attachment.url) {
|
|
11752
|
+
attachment.fetchData = () => downloadPrivateSlackFile2(attachment.url);
|
|
11753
|
+
}
|
|
11754
|
+
}
|
|
11755
|
+
}
|
|
11756
|
+
|
|
12162
11757
|
// src/chat/ingress/slash-command.ts
|
|
12163
11758
|
async function postEphemeral(event, text) {
|
|
12164
11759
|
await event.channel.postEphemeral(event.user, text, { fallbackToDM: false });
|
|
@@ -12248,6 +11843,15 @@ var productionSlackRuntime;
|
|
|
12248
11843
|
function createProductionBot() {
|
|
12249
11844
|
return new JuniorChat({
|
|
12250
11845
|
userName: botConfig.userName,
|
|
11846
|
+
concurrency: {
|
|
11847
|
+
strategy: "queue",
|
|
11848
|
+
// The SDK's default queueEntryTtlMs is 90s, but Junior turns can
|
|
11849
|
+
// run up to botConfig.turnTimeoutMs (default 12min). A follow-up
|
|
11850
|
+
// message that arrives during a long turn would expire in the
|
|
11851
|
+
// queue before the lock is released. Set the TTL to exceed the
|
|
11852
|
+
// maximum turn duration so queued messages survive.
|
|
11853
|
+
queueEntryTtlMs: botConfig.turnTimeoutMs + 6e4
|
|
11854
|
+
},
|
|
12251
11855
|
adapters: {
|
|
12252
11856
|
slack: (() => {
|
|
12253
11857
|
const signingSecret = getSlackSigningSecret();
|
|
@@ -12268,9 +11872,22 @@ function createProductionBot() {
|
|
|
12268
11872
|
state: getStateAdapter()
|
|
12269
11873
|
});
|
|
12270
11874
|
}
|
|
11875
|
+
function rehydrateAttachments(message) {
|
|
11876
|
+
rehydrateAttachmentFetchers(message);
|
|
11877
|
+
}
|
|
12271
11878
|
function registerProductionHandlers(bot, slackRuntime) {
|
|
12272
|
-
bot.onNewMention(
|
|
12273
|
-
|
|
11879
|
+
bot.onNewMention((thread, message) => {
|
|
11880
|
+
rehydrateAttachments(message);
|
|
11881
|
+
return slackRuntime.handleNewMention(thread, message);
|
|
11882
|
+
});
|
|
11883
|
+
bot.onDirectMessage((thread, message) => {
|
|
11884
|
+
rehydrateAttachments(message);
|
|
11885
|
+
return slackRuntime.handleNewMention(thread, message);
|
|
11886
|
+
});
|
|
11887
|
+
bot.onSubscribedMessage((thread, message) => {
|
|
11888
|
+
rehydrateAttachments(message);
|
|
11889
|
+
return slackRuntime.handleSubscribedMessage(thread, message);
|
|
11890
|
+
});
|
|
12274
11891
|
bot.onAssistantThreadStarted(
|
|
12275
11892
|
(event) => slackRuntime.handleAssistantThreadStarted(event)
|
|
12276
11893
|
);
|
|
@@ -12365,9 +11982,91 @@ function getProductionBot() {
|
|
|
12365
11982
|
initializeProductionApp();
|
|
12366
11983
|
return productionBot;
|
|
12367
11984
|
}
|
|
12368
|
-
|
|
12369
|
-
|
|
12370
|
-
|
|
11985
|
+
|
|
11986
|
+
// src/handlers/webhooks.ts
|
|
11987
|
+
var maxDuration = getChatConfig().functionMaxDurationSeconds;
|
|
11988
|
+
async function POST(request, context) {
|
|
11989
|
+
const bot = getProductionBot();
|
|
11990
|
+
const { platform } = await context.params;
|
|
11991
|
+
const handler = bot.webhooks[platform];
|
|
11992
|
+
const requestContext = createRequestContext(request, { platform });
|
|
11993
|
+
const requestUrl = new URL(request.url);
|
|
11994
|
+
return withContext(requestContext, async () => {
|
|
11995
|
+
if (!handler) {
|
|
11996
|
+
const error = new Error(`Unknown platform: ${platform}`);
|
|
11997
|
+
logException(
|
|
11998
|
+
error,
|
|
11999
|
+
"webhook_platform_unknown",
|
|
12000
|
+
{},
|
|
12001
|
+
{
|
|
12002
|
+
"http.response.status_code": 404
|
|
12003
|
+
},
|
|
12004
|
+
`Unknown platform: ${platform}`
|
|
12005
|
+
);
|
|
12006
|
+
return new Response(`Unknown platform: ${platform}`, { status: 404 });
|
|
12007
|
+
}
|
|
12008
|
+
try {
|
|
12009
|
+
return await withSpan(
|
|
12010
|
+
"http.server.request",
|
|
12011
|
+
"http.server",
|
|
12012
|
+
requestContext,
|
|
12013
|
+
async () => {
|
|
12014
|
+
try {
|
|
12015
|
+
const activeSpan = Sentry.getActiveSpan();
|
|
12016
|
+
const response = await handler(request, {
|
|
12017
|
+
waitUntil: (task) => after(() => {
|
|
12018
|
+
const runTask = () => {
|
|
12019
|
+
const taskOrFactory = task;
|
|
12020
|
+
return typeof taskOrFactory === "function" ? taskOrFactory() : taskOrFactory;
|
|
12021
|
+
};
|
|
12022
|
+
if (activeSpan) {
|
|
12023
|
+
return Sentry.withActiveSpan(activeSpan, runTask);
|
|
12024
|
+
}
|
|
12025
|
+
return runTask();
|
|
12026
|
+
})
|
|
12027
|
+
});
|
|
12028
|
+
if (response.status >= 400) {
|
|
12029
|
+
let responseBodySnippet;
|
|
12030
|
+
try {
|
|
12031
|
+
responseBodySnippet = (await response.clone().text()).slice(
|
|
12032
|
+
0,
|
|
12033
|
+
300
|
|
12034
|
+
);
|
|
12035
|
+
} catch {
|
|
12036
|
+
responseBodySnippet = void 0;
|
|
12037
|
+
}
|
|
12038
|
+
logWarn(
|
|
12039
|
+
"webhook_non_success_response",
|
|
12040
|
+
{},
|
|
12041
|
+
{
|
|
12042
|
+
"http.response.status_code": response.status,
|
|
12043
|
+
"http.request.header.x_slack_signature": request.headers.get("x-slack-signature") ?? void 0,
|
|
12044
|
+
"http.request.header.x_slack_request_timestamp": request.headers.get("x-slack-request-timestamp") ?? void 0,
|
|
12045
|
+
...responseBodySnippet ? { "app.webhook.response_body": responseBodySnippet } : {}
|
|
12046
|
+
},
|
|
12047
|
+
`Webhook ${platform} returned ${response.status}`
|
|
12048
|
+
);
|
|
12049
|
+
}
|
|
12050
|
+
setSpanAttributes({
|
|
12051
|
+
"http.response.status_code": response.status
|
|
12052
|
+
});
|
|
12053
|
+
setSpanStatus(response.status >= 500 ? "error" : "ok");
|
|
12054
|
+
return response;
|
|
12055
|
+
} catch (error) {
|
|
12056
|
+
setSpanStatus("error");
|
|
12057
|
+
throw error;
|
|
12058
|
+
}
|
|
12059
|
+
},
|
|
12060
|
+
{
|
|
12061
|
+
"http.request.method": request.method,
|
|
12062
|
+
"url.path": requestUrl.pathname
|
|
12063
|
+
}
|
|
12064
|
+
);
|
|
12065
|
+
} catch (error) {
|
|
12066
|
+
logException(error, "webhook_handler_failed");
|
|
12067
|
+
throw error;
|
|
12068
|
+
}
|
|
12069
|
+
});
|
|
12371
12070
|
}
|
|
12372
12071
|
|
|
12373
12072
|
export {
|
|
@@ -12376,13 +12075,13 @@ export {
|
|
|
12376
12075
|
buildSlackOutputMessage,
|
|
12377
12076
|
getSlackClient,
|
|
12378
12077
|
uploadFilesToThread,
|
|
12379
|
-
downloadPrivateSlackFile,
|
|
12380
12078
|
formatProviderLabel,
|
|
12381
12079
|
resolveBaseUrl,
|
|
12382
12080
|
finalizeMcpAuthorization,
|
|
12383
12081
|
coerceThreadArtifactsState,
|
|
12384
12082
|
mergeArtifactsState,
|
|
12385
|
-
|
|
12083
|
+
getPersistedThreadState,
|
|
12084
|
+
persistThreadStateById,
|
|
12386
12085
|
generateConversationId,
|
|
12387
12086
|
normalizeConversationText,
|
|
12388
12087
|
updateConversationStats,
|
|
@@ -12391,18 +12090,13 @@ export {
|
|
|
12391
12090
|
buildConversationContext,
|
|
12392
12091
|
escapeXml,
|
|
12393
12092
|
createUserTokenStore,
|
|
12394
|
-
removeReactionFromMessage,
|
|
12395
12093
|
truncateStatusText,
|
|
12396
|
-
buildDeterministicTurnId,
|
|
12397
12094
|
isRetryableTurnError,
|
|
12398
12095
|
markTurnCompleted,
|
|
12399
12096
|
markTurnFailed,
|
|
12400
12097
|
resolveReplyDelivery,
|
|
12401
12098
|
generateAssistantReply,
|
|
12402
12099
|
publishAppHomeView,
|
|
12403
|
-
|
|
12404
|
-
|
|
12405
|
-
createQueueCallbackHandler,
|
|
12406
|
-
getProductionBot,
|
|
12407
|
-
getProductionSlackRuntime
|
|
12100
|
+
maxDuration,
|
|
12101
|
+
POST
|
|
12408
12102
|
};
|