@sentry/junior 0.1.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/LICENSE +201 -0
- package/README.md +91 -0
- package/bin/junior.mjs +148 -0
- package/dist/app/layout.d.ts +8 -0
- package/dist/app/layout.js +8 -0
- package/dist/bot-DLML4Z7F.js +3108 -0
- package/dist/channel-HJO33DGJ.js +18 -0
- package/dist/chunk-4RBEYCOG.js +12 -0
- package/dist/chunk-7E56WM6K.js +7303 -0
- package/dist/chunk-BBOVH5RF.js +520 -0
- package/dist/chunk-GDNDYMGX.js +333 -0
- package/dist/chunk-MM3YNA4F.js +203 -0
- package/dist/chunk-OD6TOSY4.js +95 -0
- package/dist/chunk-ZA2IDPVG.js +39 -0
- package/dist/chunk-ZBFSIN6G.js +323 -0
- package/dist/client-3GAEMIQ3.js +10 -0
- package/dist/handlers/health.d.ts +3 -0
- package/dist/handlers/health.js +6 -0
- package/dist/handlers/queue-callback.d.ts +3 -0
- package/dist/handlers/queue-callback.js +272 -0
- package/dist/handlers/router.d.ts +9 -0
- package/dist/handlers/router.js +53 -0
- package/dist/handlers/webhooks.d.ts +8 -0
- package/dist/handlers/webhooks.js +7 -0
- package/dist/instrumentation.d.ts +6 -0
- package/dist/instrumentation.js +49 -0
- package/dist/next-config.d.ts +14 -0
- package/dist/next-config.js +108 -0
- package/dist/route-XLYK6CKP.js +309 -0
- package/package.json +80 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasRedisConfig
|
|
3
|
+
} from "./chunk-GDNDYMGX.js";
|
|
4
|
+
|
|
5
|
+
// src/chat/state.ts
|
|
6
|
+
import { createRedisState } from "@chat-adapter/state-redis";
|
|
7
|
+
import { createMemoryState } from "@chat-adapter/state-memory";
|
|
8
|
+
var MIN_LOCK_TTL_MS = 1e3 * 60 * 5;
|
|
9
|
+
var QUEUE_INGRESS_DEDUP_PREFIX = "junior:queue_ingress";
|
|
10
|
+
var QUEUE_MESSAGE_PROCESSING_PREFIX = "junior:queue_message";
|
|
11
|
+
var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
|
|
12
|
+
var QUEUE_MESSAGE_PROCESSING_TTL_MS = 30 * 60 * 1e3;
|
|
13
|
+
var QUEUE_MESSAGE_COMPLETED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
14
|
+
var QUEUE_MESSAGE_FAILED_TTL_MS = 6 * 60 * 60 * 1e3;
|
|
15
|
+
var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
16
|
+
var CLAIM_OR_RECLAIM_PROCESSING_SCRIPT = `
|
|
17
|
+
local key = KEYS[1]
|
|
18
|
+
local nowMs = tonumber(ARGV[1])
|
|
19
|
+
local ttlMs = tonumber(ARGV[2])
|
|
20
|
+
local payload = ARGV[3]
|
|
21
|
+
local current = redis.call("get", key)
|
|
22
|
+
|
|
23
|
+
if not current then
|
|
24
|
+
redis.call("set", key, payload, "PX", ttlMs)
|
|
25
|
+
return 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
local ok, parsed = pcall(cjson.decode, current)
|
|
29
|
+
if not ok or type(parsed) ~= "table" then
|
|
30
|
+
return 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
local status = parsed["status"]
|
|
34
|
+
if status == "failed" then
|
|
35
|
+
redis.call("set", key, payload, "PX", ttlMs)
|
|
36
|
+
return 3
|
|
37
|
+
end
|
|
38
|
+
if status ~= "processing" then
|
|
39
|
+
return 0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
local updatedAtMs = tonumber(parsed["updatedAtMs"])
|
|
43
|
+
if not updatedAtMs then
|
|
44
|
+
return 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if updatedAtMs + ttlMs < nowMs then
|
|
48
|
+
redis.call("set", key, payload, "PX", ttlMs)
|
|
49
|
+
return 2
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
return 0
|
|
53
|
+
`;
|
|
54
|
+
var UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT = `
|
|
55
|
+
local key = KEYS[1]
|
|
56
|
+
local ownerToken = ARGV[1]
|
|
57
|
+
local ttlMs = tonumber(ARGV[2])
|
|
58
|
+
local payload = ARGV[3]
|
|
59
|
+
local current = redis.call("get", key)
|
|
60
|
+
|
|
61
|
+
if not current then
|
|
62
|
+
return 0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
local ok, parsed = pcall(cjson.decode, current)
|
|
66
|
+
if not ok or type(parsed) ~= "table" then
|
|
67
|
+
return 0
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
local currentOwner = parsed["ownerToken"]
|
|
71
|
+
local status = parsed["status"]
|
|
72
|
+
if currentOwner ~= ownerToken then
|
|
73
|
+
return 0
|
|
74
|
+
end
|
|
75
|
+
if status ~= "processing" then
|
|
76
|
+
return 0
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
redis.call("set", key, payload, "PX", ttlMs)
|
|
80
|
+
return 1
|
|
81
|
+
`;
|
|
82
|
+
function createQueuedStateAdapter(base) {
|
|
83
|
+
const acquireLock = async (threadId, ttlMs) => {
|
|
84
|
+
const effectiveTtlMs = Math.max(ttlMs, MIN_LOCK_TTL_MS);
|
|
85
|
+
const lock = await base.acquireLock(threadId, effectiveTtlMs);
|
|
86
|
+
return lock;
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
connect: () => base.connect(),
|
|
90
|
+
disconnect: () => base.disconnect(),
|
|
91
|
+
subscribe: (threadId) => base.subscribe(threadId),
|
|
92
|
+
unsubscribe: (threadId) => base.unsubscribe(threadId),
|
|
93
|
+
isSubscribed: (threadId) => base.isSubscribed(threadId),
|
|
94
|
+
acquireLock,
|
|
95
|
+
releaseLock: (lock) => base.releaseLock(lock),
|
|
96
|
+
extendLock: (lock, ttlMs) => base.extendLock(lock, Math.max(ttlMs, MIN_LOCK_TTL_MS)),
|
|
97
|
+
get: (key) => base.get(key),
|
|
98
|
+
set: (key, value, ttlMs) => base.set(key, value, ttlMs),
|
|
99
|
+
setIfNotExists: (key, value, ttlMs) => base.setIfNotExists(key, value, ttlMs),
|
|
100
|
+
delete: (key) => base.delete(key)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function createStateAdapter() {
|
|
104
|
+
if (process.env.JUNIOR_STATE_ADAPTER?.trim().toLowerCase() === "memory") {
|
|
105
|
+
_redisStateAdapter = void 0;
|
|
106
|
+
return createQueuedStateAdapter(createMemoryState());
|
|
107
|
+
}
|
|
108
|
+
if (!hasRedisConfig()) {
|
|
109
|
+
throw new Error("REDIS_URL is required for durable Slack thread state");
|
|
110
|
+
}
|
|
111
|
+
const redisState = createRedisState({
|
|
112
|
+
url: process.env.REDIS_URL
|
|
113
|
+
});
|
|
114
|
+
_redisStateAdapter = redisState;
|
|
115
|
+
return createQueuedStateAdapter(redisState);
|
|
116
|
+
}
|
|
117
|
+
var _stateAdapter;
|
|
118
|
+
var _redisStateAdapter;
|
|
119
|
+
function getRedisStateAdapter() {
|
|
120
|
+
if (!_redisStateAdapter) {
|
|
121
|
+
getStateAdapter();
|
|
122
|
+
}
|
|
123
|
+
if (!_redisStateAdapter) {
|
|
124
|
+
throw new Error("Redis state adapter is unavailable for this runtime");
|
|
125
|
+
}
|
|
126
|
+
return _redisStateAdapter;
|
|
127
|
+
}
|
|
128
|
+
function queueMessageKey(rawKey) {
|
|
129
|
+
return `${QUEUE_MESSAGE_PROCESSING_PREFIX}:${rawKey}`;
|
|
130
|
+
}
|
|
131
|
+
function parseQueueMessageState(value) {
|
|
132
|
+
if (typeof value !== "string") {
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const parsed = JSON.parse(value);
|
|
137
|
+
if (!parsed || parsed.status !== "processing" && parsed.status !== "completed" && parsed.status !== "failed" || typeof parsed.updatedAtMs !== "number") {
|
|
138
|
+
return void 0;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
status: parsed.status,
|
|
142
|
+
updatedAtMs: parsed.updatedAtMs,
|
|
143
|
+
...typeof parsed.ownerToken === "string" ? { ownerToken: parsed.ownerToken } : {},
|
|
144
|
+
...typeof parsed.queueMessageId === "string" ? { queueMessageId: parsed.queueMessageId } : {},
|
|
145
|
+
...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {}
|
|
146
|
+
};
|
|
147
|
+
} catch {
|
|
148
|
+
return void 0;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function agentTurnSessionKey(conversationId, sessionId) {
|
|
152
|
+
return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
|
|
153
|
+
}
|
|
154
|
+
function isRecord(value) {
|
|
155
|
+
return typeof value === "object" && value !== null;
|
|
156
|
+
}
|
|
157
|
+
function parseAgentTurnSessionCheckpoint(value) {
|
|
158
|
+
if (typeof value !== "string") {
|
|
159
|
+
return void 0;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const parsed = JSON.parse(value);
|
|
163
|
+
if (!isRecord(parsed)) {
|
|
164
|
+
return void 0;
|
|
165
|
+
}
|
|
166
|
+
const status = parsed.state;
|
|
167
|
+
if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed") {
|
|
168
|
+
return void 0;
|
|
169
|
+
}
|
|
170
|
+
const conversationId = parsed.conversationId;
|
|
171
|
+
const sessionId = parsed.sessionId;
|
|
172
|
+
const sliceId = parsed.sliceId;
|
|
173
|
+
const checkpointVersion = parsed.checkpointVersion;
|
|
174
|
+
const updatedAtMs = parsed.updatedAtMs;
|
|
175
|
+
if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
|
|
176
|
+
return void 0;
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
checkpointVersion,
|
|
180
|
+
conversationId,
|
|
181
|
+
sessionId,
|
|
182
|
+
sliceId,
|
|
183
|
+
state: status,
|
|
184
|
+
updatedAtMs,
|
|
185
|
+
piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
|
|
186
|
+
...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
|
|
187
|
+
...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
|
|
188
|
+
};
|
|
189
|
+
} catch {
|
|
190
|
+
return void 0;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function getStateAdapter() {
|
|
194
|
+
if (!_stateAdapter) {
|
|
195
|
+
_stateAdapter = createStateAdapter();
|
|
196
|
+
}
|
|
197
|
+
return _stateAdapter;
|
|
198
|
+
}
|
|
199
|
+
async function claimQueueIngressDedup(rawKey, ttlMs) {
|
|
200
|
+
await getStateAdapter().connect();
|
|
201
|
+
const key = `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
|
|
202
|
+
const result = await getRedisStateAdapter().getClient().set(key, "1", {
|
|
203
|
+
NX: true,
|
|
204
|
+
PX: ttlMs
|
|
205
|
+
});
|
|
206
|
+
return result === "OK";
|
|
207
|
+
}
|
|
208
|
+
async function hasQueueIngressDedup(rawKey) {
|
|
209
|
+
await getStateAdapter().connect();
|
|
210
|
+
const key = `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
|
|
211
|
+
const value = await getRedisStateAdapter().getClient().get(key);
|
|
212
|
+
return typeof value === "string" && value.length > 0;
|
|
213
|
+
}
|
|
214
|
+
async function getQueueMessageProcessingState(rawKey) {
|
|
215
|
+
await getStateAdapter().connect();
|
|
216
|
+
const state = await getStateAdapter().get(queueMessageKey(rawKey));
|
|
217
|
+
return parseQueueMessageState(state);
|
|
218
|
+
}
|
|
219
|
+
async function acquireQueueMessageProcessingOwnership(args) {
|
|
220
|
+
await getStateAdapter().connect();
|
|
221
|
+
const key = queueMessageKey(args.rawKey);
|
|
222
|
+
const nowMs = Date.now();
|
|
223
|
+
const payload = JSON.stringify({
|
|
224
|
+
status: "processing",
|
|
225
|
+
updatedAtMs: nowMs,
|
|
226
|
+
ownerToken: args.ownerToken,
|
|
227
|
+
...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
|
|
228
|
+
});
|
|
229
|
+
const result = await getRedisStateAdapter().getClient().eval(CLAIM_OR_RECLAIM_PROCESSING_SCRIPT, {
|
|
230
|
+
keys: [key],
|
|
231
|
+
arguments: [String(nowMs), String(QUEUE_MESSAGE_PROCESSING_TTL_MS), payload]
|
|
232
|
+
});
|
|
233
|
+
if (result === 1) {
|
|
234
|
+
return "acquired";
|
|
235
|
+
}
|
|
236
|
+
if (result === 2) {
|
|
237
|
+
return "reclaimed";
|
|
238
|
+
}
|
|
239
|
+
if (result === 3) {
|
|
240
|
+
return "recovered";
|
|
241
|
+
}
|
|
242
|
+
return "blocked";
|
|
243
|
+
}
|
|
244
|
+
async function refreshQueueMessageProcessingOwnership(args) {
|
|
245
|
+
await getStateAdapter().connect();
|
|
246
|
+
const nowMs = Date.now();
|
|
247
|
+
const payload = JSON.stringify({
|
|
248
|
+
status: "processing",
|
|
249
|
+
updatedAtMs: nowMs,
|
|
250
|
+
ownerToken: args.ownerToken,
|
|
251
|
+
...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
|
|
252
|
+
});
|
|
253
|
+
const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
|
|
254
|
+
keys: [queueMessageKey(args.rawKey)],
|
|
255
|
+
arguments: [args.ownerToken, String(QUEUE_MESSAGE_PROCESSING_TTL_MS), payload]
|
|
256
|
+
});
|
|
257
|
+
return result === 1;
|
|
258
|
+
}
|
|
259
|
+
async function completeQueueMessageProcessingOwnership(args) {
|
|
260
|
+
await getStateAdapter().connect();
|
|
261
|
+
const payload = JSON.stringify({
|
|
262
|
+
status: "completed",
|
|
263
|
+
updatedAtMs: Date.now(),
|
|
264
|
+
ownerToken: args.ownerToken,
|
|
265
|
+
...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
|
|
266
|
+
});
|
|
267
|
+
const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
|
|
268
|
+
keys: [queueMessageKey(args.rawKey)],
|
|
269
|
+
arguments: [args.ownerToken, String(QUEUE_MESSAGE_COMPLETED_TTL_MS), payload]
|
|
270
|
+
});
|
|
271
|
+
return result === 1;
|
|
272
|
+
}
|
|
273
|
+
async function failQueueMessageProcessingOwnership(args) {
|
|
274
|
+
await getStateAdapter().connect();
|
|
275
|
+
const payload = JSON.stringify({
|
|
276
|
+
status: "failed",
|
|
277
|
+
updatedAtMs: Date.now(),
|
|
278
|
+
ownerToken: args.ownerToken,
|
|
279
|
+
errorMessage: args.errorMessage,
|
|
280
|
+
...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
|
|
281
|
+
});
|
|
282
|
+
const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
|
|
283
|
+
keys: [queueMessageKey(args.rawKey)],
|
|
284
|
+
arguments: [args.ownerToken, String(QUEUE_MESSAGE_FAILED_TTL_MS), payload]
|
|
285
|
+
});
|
|
286
|
+
return result === 1;
|
|
287
|
+
}
|
|
288
|
+
async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
|
|
289
|
+
await getStateAdapter().connect();
|
|
290
|
+
const value = await getStateAdapter().get(agentTurnSessionKey(conversationId, sessionId));
|
|
291
|
+
return parseAgentTurnSessionCheckpoint(value);
|
|
292
|
+
}
|
|
293
|
+
async function upsertAgentTurnSessionCheckpoint(args) {
|
|
294
|
+
await getStateAdapter().connect();
|
|
295
|
+
const existing = await getAgentTurnSessionCheckpoint(args.conversationId, args.sessionId);
|
|
296
|
+
const checkpoint = {
|
|
297
|
+
checkpointVersion: (existing?.checkpointVersion ?? 0) + 1,
|
|
298
|
+
conversationId: args.conversationId,
|
|
299
|
+
sessionId: args.sessionId,
|
|
300
|
+
sliceId: args.sliceId,
|
|
301
|
+
state: args.state,
|
|
302
|
+
updatedAtMs: Date.now(),
|
|
303
|
+
piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
|
|
304
|
+
...args.errorMessage ? { errorMessage: args.errorMessage } : {},
|
|
305
|
+
...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
|
|
306
|
+
};
|
|
307
|
+
const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
|
|
308
|
+
await getStateAdapter().set(agentTurnSessionKey(args.conversationId, args.sessionId), JSON.stringify(checkpoint), ttlMs);
|
|
309
|
+
return checkpoint;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export {
|
|
313
|
+
getStateAdapter,
|
|
314
|
+
claimQueueIngressDedup,
|
|
315
|
+
hasQueueIngressDedup,
|
|
316
|
+
getQueueMessageProcessingState,
|
|
317
|
+
acquireQueueMessageProcessingOwnership,
|
|
318
|
+
refreshQueueMessageProcessingOwnership,
|
|
319
|
+
completeQueueMessageProcessingOwnership,
|
|
320
|
+
failQueueMessageProcessingOwnership,
|
|
321
|
+
getAgentTurnSessionCheckpoint,
|
|
322
|
+
upsertAgentTurnSessionCheckpoint
|
|
323
|
+
};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createQueueCallbackHandler,
|
|
3
|
+
getThreadMessageTopic
|
|
4
|
+
} from "../chunk-ZA2IDPVG.js";
|
|
5
|
+
import {
|
|
6
|
+
acquireQueueMessageProcessingOwnership,
|
|
7
|
+
completeQueueMessageProcessingOwnership,
|
|
8
|
+
failQueueMessageProcessingOwnership,
|
|
9
|
+
getQueueMessageProcessingState,
|
|
10
|
+
getStateAdapter,
|
|
11
|
+
refreshQueueMessageProcessingOwnership
|
|
12
|
+
} from "../chunk-ZBFSIN6G.js";
|
|
13
|
+
import {
|
|
14
|
+
downloadPrivateSlackFile
|
|
15
|
+
} from "../chunk-GDNDYMGX.js";
|
|
16
|
+
import {
|
|
17
|
+
createRequestContext,
|
|
18
|
+
logError,
|
|
19
|
+
logException,
|
|
20
|
+
logWarn,
|
|
21
|
+
setSpanStatus,
|
|
22
|
+
withContext,
|
|
23
|
+
withSpan
|
|
24
|
+
} from "../chunk-BBOVH5RF.js";
|
|
25
|
+
|
|
26
|
+
// src/chat/queue/process-thread-message.ts
|
|
27
|
+
import { Message, ThreadImpl } from "chat";
|
|
28
|
+
|
|
29
|
+
// src/chat/thread-runtime/process-thread-message-runtime.ts
|
|
30
|
+
function rehydrateAttachmentFetchers(payload) {
|
|
31
|
+
for (const attachment of payload.message.attachments) {
|
|
32
|
+
if (!attachment.fetchData && attachment.url) {
|
|
33
|
+
attachment.fetchData = () => downloadPrivateSlackFile(attachment.url);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function processThreadMessageRuntime(args) {
|
|
38
|
+
const { appSlackRuntime } = await import("../bot-DLML4Z7F.js");
|
|
39
|
+
const runtimePayload = {
|
|
40
|
+
message: args.message,
|
|
41
|
+
thread: args.thread
|
|
42
|
+
};
|
|
43
|
+
rehydrateAttachmentFetchers(runtimePayload);
|
|
44
|
+
if (args.kind === "new_mention") {
|
|
45
|
+
await appSlackRuntime.handleNewMention(args.thread, args.message, {
|
|
46
|
+
beforeFirstResponsePost: args.beforeFirstResponsePost
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (args.kind === "subscribed_reply") {
|
|
51
|
+
await appSlackRuntime.handleSubscribedMessage(args.thread, args.message, {
|
|
52
|
+
beforeFirstResponsePost: args.beforeFirstResponsePost,
|
|
53
|
+
preApprovedReply: true
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
await appSlackRuntime.handleSubscribedMessage(args.thread, args.message, {
|
|
58
|
+
beforeFirstResponsePost: args.beforeFirstResponsePost
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/chat/queue/process-thread-message.ts
|
|
63
|
+
var stateAdapterConnected = false;
|
|
64
|
+
function isSerializedThread(thread) {
|
|
65
|
+
return typeof thread === "object" && thread !== null && thread._type === "chat:Thread";
|
|
66
|
+
}
|
|
67
|
+
function isSerializedMessage(message) {
|
|
68
|
+
return typeof message === "object" && message !== null && message._type === "chat:Message";
|
|
69
|
+
}
|
|
70
|
+
function getPayloadChannelId(payload) {
|
|
71
|
+
return payload.thread.channelId;
|
|
72
|
+
}
|
|
73
|
+
function getPayloadUserId(payload) {
|
|
74
|
+
return payload.message.author?.userId;
|
|
75
|
+
}
|
|
76
|
+
function createMessageOwnerToken() {
|
|
77
|
+
return `msg-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
78
|
+
}
|
|
79
|
+
var QueueMessageOwnershipError = class extends Error {
|
|
80
|
+
constructor(stage, dedupKey) {
|
|
81
|
+
super(`Queue message ownership lost during ${stage} for dedupKey=${dedupKey}`);
|
|
82
|
+
this.name = "QueueMessageOwnershipError";
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var defaultProcessQueuedThreadMessageDeps = {
|
|
86
|
+
clearProcessingReaction: async ({ channelId, timestamp }) => {
|
|
87
|
+
const { removeReactionFromMessage } = await import("../channel-HJO33DGJ.js");
|
|
88
|
+
await removeReactionFromMessage({
|
|
89
|
+
channelId,
|
|
90
|
+
timestamp,
|
|
91
|
+
emoji: "eyes"
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
logWarn,
|
|
95
|
+
processRuntime: processThreadMessageRuntime
|
|
96
|
+
};
|
|
97
|
+
function deserializeThread(thread) {
|
|
98
|
+
if (isSerializedThread(thread)) {
|
|
99
|
+
return ThreadImpl.fromJSON(thread);
|
|
100
|
+
}
|
|
101
|
+
return thread;
|
|
102
|
+
}
|
|
103
|
+
function deserializeMessage(message) {
|
|
104
|
+
if (isSerializedMessage(message)) {
|
|
105
|
+
return Message.fromJSON(message);
|
|
106
|
+
}
|
|
107
|
+
return message;
|
|
108
|
+
}
|
|
109
|
+
async function logThreadMessageFailure(payload, errorMessage) {
|
|
110
|
+
logError(
|
|
111
|
+
"queue_message_failed",
|
|
112
|
+
{
|
|
113
|
+
slackThreadId: payload.normalizedThreadId,
|
|
114
|
+
slackChannelId: getPayloadChannelId(payload),
|
|
115
|
+
slackUserId: getPayloadUserId(payload)
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"messaging.message.id": payload.message.id,
|
|
119
|
+
"app.queue.message_kind": payload.kind,
|
|
120
|
+
"app.queue.message_id": payload.queueMessageId,
|
|
121
|
+
"error.message": errorMessage
|
|
122
|
+
},
|
|
123
|
+
"Queue message processing failed"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
async function processQueuedThreadMessage(payload, deps = defaultProcessQueuedThreadMessageDeps) {
|
|
127
|
+
const existingMessageState = await getQueueMessageProcessingState(payload.dedupKey);
|
|
128
|
+
if (existingMessageState?.status === "completed") {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const ownerToken = createMessageOwnerToken();
|
|
132
|
+
const claimResult = await acquireQueueMessageProcessingOwnership({
|
|
133
|
+
rawKey: payload.dedupKey,
|
|
134
|
+
ownerToken,
|
|
135
|
+
queueMessageId: payload.queueMessageId
|
|
136
|
+
});
|
|
137
|
+
if (claimResult === "blocked") {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const threadWasSerialized = isSerializedThread(payload.thread);
|
|
141
|
+
if (threadWasSerialized && !stateAdapterConnected) {
|
|
142
|
+
await getStateAdapter().connect();
|
|
143
|
+
stateAdapterConnected = true;
|
|
144
|
+
}
|
|
145
|
+
const runtimePayload = {
|
|
146
|
+
...payload,
|
|
147
|
+
thread: deserializeThread(payload.thread),
|
|
148
|
+
message: deserializeMessage(payload.message)
|
|
149
|
+
};
|
|
150
|
+
try {
|
|
151
|
+
const refreshed = await refreshQueueMessageProcessingOwnership({
|
|
152
|
+
rawKey: payload.dedupKey,
|
|
153
|
+
ownerToken,
|
|
154
|
+
queueMessageId: payload.queueMessageId
|
|
155
|
+
});
|
|
156
|
+
if (!refreshed) {
|
|
157
|
+
throw new QueueMessageOwnershipError("refresh", payload.dedupKey);
|
|
158
|
+
}
|
|
159
|
+
let reactionCleared = false;
|
|
160
|
+
const clearReactionBeforeFirstResponsePost = async () => {
|
|
161
|
+
if (reactionCleared) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
reactionCleared = true;
|
|
165
|
+
try {
|
|
166
|
+
await deps.clearProcessingReaction({
|
|
167
|
+
channelId: runtimePayload.thread.channelId,
|
|
168
|
+
timestamp: runtimePayload.message.id
|
|
169
|
+
});
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
172
|
+
deps.logWarn(
|
|
173
|
+
"queue_processing_reaction_clear_failed",
|
|
174
|
+
{
|
|
175
|
+
slackThreadId: payload.normalizedThreadId,
|
|
176
|
+
slackChannelId: getPayloadChannelId(payload),
|
|
177
|
+
slackUserId: getPayloadUserId(payload)
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"messaging.message.id": payload.message.id,
|
|
181
|
+
"app.queue.message_kind": payload.kind,
|
|
182
|
+
"app.queue.message_id": payload.queueMessageId,
|
|
183
|
+
"error.message": errorMessage
|
|
184
|
+
},
|
|
185
|
+
"Failed to remove processing reaction before sending queue response"
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
await deps.processRuntime({
|
|
190
|
+
kind: runtimePayload.kind,
|
|
191
|
+
thread: runtimePayload.thread,
|
|
192
|
+
message: runtimePayload.message,
|
|
193
|
+
beforeFirstResponsePost: clearReactionBeforeFirstResponsePost
|
|
194
|
+
});
|
|
195
|
+
const completed = await completeQueueMessageProcessingOwnership({
|
|
196
|
+
rawKey: payload.dedupKey,
|
|
197
|
+
ownerToken,
|
|
198
|
+
queueMessageId: payload.queueMessageId
|
|
199
|
+
});
|
|
200
|
+
if (!completed) {
|
|
201
|
+
throw new QueueMessageOwnershipError("complete", payload.dedupKey);
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
205
|
+
await logThreadMessageFailure(payload, errorMessage);
|
|
206
|
+
const failed = await failQueueMessageProcessingOwnership({
|
|
207
|
+
rawKey: payload.dedupKey,
|
|
208
|
+
ownerToken,
|
|
209
|
+
errorMessage,
|
|
210
|
+
queueMessageId: payload.queueMessageId
|
|
211
|
+
});
|
|
212
|
+
if (!failed && !(error instanceof QueueMessageOwnershipError)) {
|
|
213
|
+
throw new Error(`Failed to persist queue message failure state for dedupKey=${payload.dedupKey}: ${errorMessage}`);
|
|
214
|
+
}
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/handlers/queue-callback.ts
|
|
220
|
+
var callbackHandler = createQueueCallbackHandler(async (message, metadata) => {
|
|
221
|
+
const payload = {
|
|
222
|
+
...message,
|
|
223
|
+
queueMessageId: metadata.messageId
|
|
224
|
+
};
|
|
225
|
+
if (metadata.topicName !== getThreadMessageTopic()) {
|
|
226
|
+
throw new Error(`Unexpected queue topic: ${metadata.topicName}`);
|
|
227
|
+
}
|
|
228
|
+
await withSpan(
|
|
229
|
+
"queue.process_message",
|
|
230
|
+
"queue.process_message",
|
|
231
|
+
{
|
|
232
|
+
slackThreadId: payload.normalizedThreadId,
|
|
233
|
+
slackChannelId: payload.thread.channelId,
|
|
234
|
+
slackUserId: payload.message.author?.userId
|
|
235
|
+
},
|
|
236
|
+
async () => {
|
|
237
|
+
await processQueuedThreadMessage(payload);
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
"messaging.message.id": payload.message.id,
|
|
241
|
+
"app.queue.message_kind": payload.kind,
|
|
242
|
+
"app.queue.message_id": payload.queueMessageId,
|
|
243
|
+
"app.queue.delivery_count": metadata.deliveryCount,
|
|
244
|
+
"app.queue.topic": metadata.topicName
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
async function POST(request) {
|
|
249
|
+
const requestContext = createRequestContext(request, { platform: "queue" });
|
|
250
|
+
return withContext(requestContext, async () => {
|
|
251
|
+
try {
|
|
252
|
+
const response = await callbackHandler(request);
|
|
253
|
+
setSpanStatus(response.status >= 500 ? "error" : "ok");
|
|
254
|
+
return response;
|
|
255
|
+
} catch (error) {
|
|
256
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
257
|
+
logError(
|
|
258
|
+
"queue_callback_failed",
|
|
259
|
+
{},
|
|
260
|
+
{
|
|
261
|
+
"error.message": message
|
|
262
|
+
},
|
|
263
|
+
"Queue callback processing failed"
|
|
264
|
+
);
|
|
265
|
+
logException(error, "queue_callback_failed");
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
export {
|
|
271
|
+
POST
|
|
272
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type RouteContext = {
|
|
2
|
+
params: Promise<{
|
|
3
|
+
path: string[];
|
|
4
|
+
}>;
|
|
5
|
+
};
|
|
6
|
+
declare function GET(request: Request, context: RouteContext): Promise<Response>;
|
|
7
|
+
declare function POST(request: Request, context: RouteContext): Promise<Response>;
|
|
8
|
+
|
|
9
|
+
export { GET, POST };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
POST
|
|
3
|
+
} from "../chunk-OD6TOSY4.js";
|
|
4
|
+
import {
|
|
5
|
+
GET
|
|
6
|
+
} from "../chunk-4RBEYCOG.js";
|
|
7
|
+
import "../chunk-BBOVH5RF.js";
|
|
8
|
+
|
|
9
|
+
// src/handlers/oauth-callback.ts
|
|
10
|
+
async function loadOAuthRoute() {
|
|
11
|
+
return import("../route-XLYK6CKP.js");
|
|
12
|
+
}
|
|
13
|
+
async function GET2(request, context) {
|
|
14
|
+
const route = await loadOAuthRoute();
|
|
15
|
+
return route.GET(request, context);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/handlers/router.ts
|
|
19
|
+
function normalizeRoutePath(pathParts) {
|
|
20
|
+
const route = pathParts.join("/").replace(/^\/+|\/+$/g, "");
|
|
21
|
+
return route.startsWith("api/") ? route.slice("api/".length) : route;
|
|
22
|
+
}
|
|
23
|
+
async function GET3(request, context) {
|
|
24
|
+
const { path } = await context.params;
|
|
25
|
+
const route = normalizeRoutePath(path);
|
|
26
|
+
if (route === "health") {
|
|
27
|
+
return GET();
|
|
28
|
+
}
|
|
29
|
+
const oauthCallbackMatch = route.match(/^oauth\/callback\/([^/]+)$/);
|
|
30
|
+
if (oauthCallbackMatch) {
|
|
31
|
+
const provider = oauthCallbackMatch[1];
|
|
32
|
+
return GET2(request, {
|
|
33
|
+
params: Promise.resolve({ provider })
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return new Response("Not Found", { status: 404 });
|
|
37
|
+
}
|
|
38
|
+
async function POST2(request, context) {
|
|
39
|
+
const { path } = await context.params;
|
|
40
|
+
const route = normalizeRoutePath(path);
|
|
41
|
+
const webhookMatch = route.match(/^webhooks\/([^/]+)$/);
|
|
42
|
+
if (webhookMatch) {
|
|
43
|
+
const platform = webhookMatch[1];
|
|
44
|
+
return POST(request, {
|
|
45
|
+
params: Promise.resolve({ platform })
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return new Response("Not Found", { status: 404 });
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
GET3 as GET,
|
|
52
|
+
POST2 as POST
|
|
53
|
+
};
|