@sentry/junior 0.74.1 → 0.76.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 +1 -1
- package/bin/junior.mjs +4 -66
- package/dist/agent-hooks-ZOE7RIED.js +37 -0
- package/dist/api-reference.d.ts +3 -1
- package/dist/app.js +5516 -5422
- package/dist/build/copy-build-content.d.ts +1 -1
- package/dist/build/virtual-config.d.ts +2 -2
- package/dist/chat/agent-dispatch/context.d.ts +2 -3
- package/dist/chat/agent-dispatch/runner.d.ts +2 -0
- package/dist/chat/agent-dispatch/types.d.ts +2 -1
- package/dist/chat/config.d.ts +3 -0
- package/dist/chat/credentials/state-adapter-token-store.d.ts +2 -0
- package/dist/chat/credentials/subject.d.ts +3 -3
- package/dist/chat/credentials/user-token-store.d.ts +17 -12
- package/dist/chat/db.d.ts +8 -0
- package/dist/chat/mcp/auth-store.d.ts +2 -1
- package/dist/chat/mcp/oauth.d.ts +2 -1
- package/dist/chat/oauth-flow.d.ts +3 -1
- package/dist/chat/pi/client.d.ts +15 -7
- package/dist/chat/plugins/agent-hooks.d.ts +20 -13
- package/dist/chat/plugins/auth/oauth-request.d.ts +11 -7
- package/dist/chat/plugins/credential-hooks.d.ts +6 -6
- package/dist/chat/plugins/logging.d.ts +2 -2
- package/dist/chat/plugins/model.d.ts +9 -0
- package/dist/chat/plugins/package-discovery.d.ts +2 -1
- package/dist/chat/plugins/prompt.d.ts +5 -0
- package/dist/chat/plugins/registry.d.ts +4 -0
- package/dist/chat/plugins/state.d.ts +3 -5
- package/dist/chat/plugins/task-callback.d.ts +5 -0
- package/dist/chat/plugins/task-message.d.ts +23 -0
- package/dist/chat/plugins/task-queue.d.ts +5 -0
- package/dist/chat/plugins/task-runner.d.ts +12 -0
- package/dist/chat/plugins/task-signing.d.ts +31 -0
- package/dist/chat/plugins/types.d.ts +1 -0
- package/dist/chat/plugins/validation.d.ts +5 -0
- package/dist/chat/prompt.d.ts +15 -1
- package/dist/chat/requester.d.ts +6 -5
- package/dist/chat/respond-helpers.d.ts +2 -0
- package/dist/chat/respond.d.ts +13 -2
- package/dist/chat/runtime/agent-continue-runner.d.ts +4 -0
- package/dist/chat/runtime/reply-executor.d.ts +5 -1
- package/dist/chat/runtime/slack-resume.d.ts +10 -2
- package/dist/chat/runtime/slack-runtime.d.ts +6 -1
- package/dist/chat/sandbox/egress-credentials.d.ts +8 -8
- package/dist/chat/sandbox/sandbox.d.ts +2 -2
- package/dist/chat/sentry.d.ts +1 -0
- package/dist/chat/services/mcp-auth-orchestration.d.ts +2 -1
- package/dist/chat/services/plugin-auth-orchestration.d.ts +2 -1
- package/dist/chat/services/subscribed-decision.d.ts +2 -2
- package/dist/chat/services/turn-session-record.d.ts +11 -7
- package/dist/chat/sql/db.d.ts +3 -0
- package/dist/chat/sql/executor.d.ts +7 -0
- package/dist/chat/sql/neon.d.ts +2 -4
- package/dist/chat/sql/postgres.d.ts +6 -0
- package/dist/chat/state/turn-session.d.ts +8 -5
- package/dist/chat/task-execution/state.d.ts +7 -2
- package/dist/chat/task-execution/worker.d.ts +1 -1
- package/dist/chat/tools/agent-tools.d.ts +9 -2
- package/dist/chat/tools/slack/context.d.ts +2 -2
- package/dist/chat/tools/types.d.ts +7 -4
- package/dist/chat/vercel-queue-client.d.ts +3 -0
- package/dist/{chunk-YOHFWWBV.js → chunk-2ECJXSVQ.js} +5 -107
- package/dist/{chunk-OR6NQJ5E.js → chunk-4SCWV7TJ.js} +3 -3
- package/dist/chunk-4UO6FK4G.js +64 -0
- package/dist/chunk-56TBVRJG.js +115 -0
- package/dist/{chunk-3BYAPS6B.js → chunk-EJN6G5A2.js} +17 -11
- package/dist/{chunk-SQGMG7OD.js → chunk-HHDUKWVG.js} +508 -149
- package/dist/{chunk-6UP2Z2RZ.js → chunk-JBASI5VV.js} +7 -7
- package/dist/chunk-KNFROR7R.js +127 -0
- package/dist/{chunk-HYHKTFG2.js → chunk-KOIMO7S3.js} +186 -910
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-NFTMTIP3.js +964 -0
- package/dist/chunk-NYKJ3KON.js +1082 -0
- package/dist/{chunk-SJHUF3DP.js → chunk-OJ53FYVG.js} +2 -10
- package/dist/{chunk-KVZL5NZS.js → chunk-Q3XNY442.js} +17 -7
- package/dist/{chunk-YRDS7VKO.js → chunk-Q6XFTRV5.js} +2 -2
- package/dist/chunk-R6Z5XWY3.js +1076 -0
- package/dist/chunk-RV5RYIJW.js +56 -0
- package/dist/chunk-SG5WAA7H.js +132 -0
- package/dist/chunk-ST6YNAXG.js +54 -0
- package/dist/{chunk-GM7HTXYC.js → chunk-T77LUIX3.js} +148 -151
- package/dist/{chunk-CYUI7JU5.js → chunk-VALUBQ7R.js} +22 -30
- package/dist/chunk-XBBC6W45.js +71 -0
- package/dist/chunk-Y2CM7HXH.js +111 -0
- package/dist/{chunk-F6HWCPOC.js → chunk-Y5OFBCBZ.js} +1 -1
- package/dist/{chunk-M4FLLXXD.js → chunk-Z4CIQ3EB.js} +5 -1
- package/dist/{chunk-7Q5YOUUT.js → chunk-ZLMBNBUG.js} +146 -52
- package/dist/{chunk-2LUZA3LY.js → chunk-ZQB37HUX.js} +11 -11
- package/dist/cli/chat.js +87 -8
- package/dist/cli/check.js +8 -7
- package/dist/cli/env.js +4 -53
- package/dist/cli/init.js +6 -1
- package/dist/cli/main.js +84 -0
- package/dist/cli/plugins.js +244 -0
- package/dist/cli/run.js +5 -52
- package/dist/cli/snapshot-warmup.js +12 -11
- package/dist/cli/upgrade.js +385 -26
- package/dist/db-7A7PFRGL.js +17 -0
- package/dist/deployment.d.ts +1 -0
- package/dist/handlers/sandbox-egress-route.d.ts +4 -0
- package/dist/handlers/slack-webhook.d.ts +4 -0
- package/dist/handlers/webhooks.d.ts +6 -13
- package/dist/instrumentation.js +14 -18
- package/dist/nitro.d.ts +1 -1
- package/dist/nitro.js +67 -101
- package/dist/plugin-module.d.ts +21 -0
- package/dist/plugins-PZMDS7AT.js +15 -0
- package/dist/plugins.d.ts +9 -5
- package/dist/registry-OIPAJU2O.js +46 -0
- package/dist/reporting/conversations.d.ts +3 -3
- package/dist/reporting.d.ts +6 -5
- package/dist/reporting.js +42 -28
- package/dist/{runner-27NP2TEO.js → runner-KPLNHDCV.js} +77 -19
- package/dist/sentry-4CP5NNQ5.js +31 -0
- package/dist/validation-SLA6IGF7.js +15 -0
- package/dist/vercel.js +1 -1
- package/package.json +14 -11
- package/dist/chat/conversations/configured.d.ts +0 -5
- package/dist/chat/conversations/state.d.ts +0 -4
- package/dist/chunk-2KG3PWR4.js +0 -17
- package/dist/chunk-JL2SLRAT.js +0 -1970
|
@@ -0,0 +1,1076 @@
|
|
|
1
|
+
import {
|
|
2
|
+
JUNIOR_THREAD_STATE_TTL_MS
|
|
3
|
+
} from "./chunk-Z4CIQ3EB.js";
|
|
4
|
+
import {
|
|
5
|
+
getDefaultRedisStateAdapterFor,
|
|
6
|
+
getStateAdapter
|
|
7
|
+
} from "./chunk-Y5OFBCBZ.js";
|
|
8
|
+
import {
|
|
9
|
+
parseDestination,
|
|
10
|
+
sameDestination
|
|
11
|
+
} from "./chunk-Q6XFTRV5.js";
|
|
12
|
+
import {
|
|
13
|
+
getChatConfig
|
|
14
|
+
} from "./chunk-T77LUIX3.js";
|
|
15
|
+
import {
|
|
16
|
+
parseStoredSlackRequester
|
|
17
|
+
} from "./chunk-VALUBQ7R.js";
|
|
18
|
+
import {
|
|
19
|
+
isRecord,
|
|
20
|
+
toOptionalNumber,
|
|
21
|
+
toOptionalString
|
|
22
|
+
} from "./chunk-EJN6G5A2.js";
|
|
23
|
+
|
|
24
|
+
// src/chat/task-execution/state.ts
|
|
25
|
+
import { randomUUID } from "crypto";
|
|
26
|
+
var CONVERSATION_PREFIX = "junior:conversation";
|
|
27
|
+
var CONVERSATION_SCHEMA_VERSION = 1;
|
|
28
|
+
var CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH = 1e4;
|
|
29
|
+
var CONVERSATION_INDEX_LOCK_TTL_MS = 1e4;
|
|
30
|
+
var CONVERSATION_INDEX_LOCK_WAIT_MS = 2e3;
|
|
31
|
+
var CONVERSATION_INDEX_LOCK_RETRY_MS = 25;
|
|
32
|
+
var CONVERSATION_MUTATION_LOCK_TTL_MS = 1e4;
|
|
33
|
+
var CONVERSATION_MUTATION_WAIT_MS = 1e4;
|
|
34
|
+
var CONVERSATION_MUTATION_RETRY_MS = 25;
|
|
35
|
+
var InvalidConversationRecordError = class extends Error {
|
|
36
|
+
constructor(conversationId) {
|
|
37
|
+
super(`Conversation record is invalid for ${conversationId}`);
|
|
38
|
+
this.name = "InvalidConversationRecordError";
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var CONVERSATION_BY_ACTIVITY_INDEX_KEY = `${CONVERSATION_PREFIX}:by-activity`;
|
|
42
|
+
var CONVERSATION_ACTIVE_INDEX_KEY = `${CONVERSATION_PREFIX}:active`;
|
|
43
|
+
var CONVERSATION_WORK_LEASE_TTL_MS = 9e4;
|
|
44
|
+
var CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15e3;
|
|
45
|
+
var CONVERSATION_WORK_STALE_ENQUEUE_MS = 6e4;
|
|
46
|
+
function conversationKey(conversationId) {
|
|
47
|
+
return `${CONVERSATION_PREFIX}:${conversationId}`;
|
|
48
|
+
}
|
|
49
|
+
function indexLockKey(indexKey) {
|
|
50
|
+
return `${indexKey}:lock`;
|
|
51
|
+
}
|
|
52
|
+
function mutationLockKey(conversationId) {
|
|
53
|
+
return `${CONVERSATION_PREFIX}:mutation:${conversationId}`;
|
|
54
|
+
}
|
|
55
|
+
function now() {
|
|
56
|
+
return Date.now();
|
|
57
|
+
}
|
|
58
|
+
function compareMessages(left, right) {
|
|
59
|
+
return left.createdAtMs - right.createdAtMs || left.receivedAtMs - right.receivedAtMs || left.inboundMessageId.localeCompare(right.inboundMessageId);
|
|
60
|
+
}
|
|
61
|
+
function compareIndexDescending(left, right) {
|
|
62
|
+
return right.score - left.score || right.conversationId.localeCompare(left.conversationId);
|
|
63
|
+
}
|
|
64
|
+
function compareIndexAscending(left, right) {
|
|
65
|
+
return left.score - right.score || left.conversationId.localeCompare(right.conversationId);
|
|
66
|
+
}
|
|
67
|
+
function uniqueStrings(values) {
|
|
68
|
+
return [...new Set(values)];
|
|
69
|
+
}
|
|
70
|
+
function normalizeSource(value) {
|
|
71
|
+
if (value === "api" || value === "internal" || value === "local" || value === "plugin" || value === "scheduler" || value === "slack") {
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
function normalizeExecutionStatus(value) {
|
|
77
|
+
if (value === "awaiting_resume" || value === "idle" || value === "pending" || value === "running") {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
82
|
+
function normalizeMetadata(value) {
|
|
83
|
+
if (!isRecord(value)) {
|
|
84
|
+
return void 0;
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
function normalizeInput(value) {
|
|
89
|
+
if (!isRecord(value)) {
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
const text = toOptionalString(value.text);
|
|
93
|
+
if (!text) {
|
|
94
|
+
return void 0;
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
text,
|
|
98
|
+
authorId: toOptionalString(value.authorId),
|
|
99
|
+
attachments: Array.isArray(value.attachments) ? [...value.attachments] : void 0,
|
|
100
|
+
metadata: normalizeMetadata(value.metadata)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function normalizeMessage(value) {
|
|
104
|
+
if (!isRecord(value)) {
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
const conversationId = toOptionalString(value.conversationId);
|
|
108
|
+
const inboundMessageId = toOptionalString(value.inboundMessageId);
|
|
109
|
+
const source = normalizeSource(value.source);
|
|
110
|
+
const destination = parseDestination(value.destination);
|
|
111
|
+
const createdAtMs = toOptionalNumber(value.createdAtMs);
|
|
112
|
+
const receivedAtMs = toOptionalNumber(value.receivedAtMs);
|
|
113
|
+
const input = normalizeInput(value.input);
|
|
114
|
+
if (!conversationId || !destination || !inboundMessageId || !source || typeof createdAtMs !== "number" || typeof receivedAtMs !== "number" || !input) {
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
conversationId,
|
|
119
|
+
destination,
|
|
120
|
+
inboundMessageId,
|
|
121
|
+
source,
|
|
122
|
+
createdAtMs,
|
|
123
|
+
receivedAtMs,
|
|
124
|
+
input,
|
|
125
|
+
injectedAtMs: toOptionalNumber(value.injectedAtMs)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function normalizeRequester(value) {
|
|
129
|
+
return parseStoredSlackRequester(value);
|
|
130
|
+
}
|
|
131
|
+
function normalizeLease(value) {
|
|
132
|
+
if (!isRecord(value)) {
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
const token = toOptionalString(value.token);
|
|
136
|
+
const acquiredAtMs = toOptionalNumber(value.acquiredAtMs);
|
|
137
|
+
const lastCheckInAtMs = toOptionalNumber(value.lastCheckInAtMs);
|
|
138
|
+
const expiresAtMs = toOptionalNumber(value.expiresAtMs);
|
|
139
|
+
if (!token || typeof acquiredAtMs !== "number" || typeof lastCheckInAtMs !== "number" || typeof expiresAtMs !== "number") {
|
|
140
|
+
return void 0;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
token,
|
|
144
|
+
acquiredAtMs,
|
|
145
|
+
lastCheckInAtMs,
|
|
146
|
+
expiresAtMs
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function normalizeExecution(conversationId, value) {
|
|
150
|
+
if (!isRecord(value)) {
|
|
151
|
+
return void 0;
|
|
152
|
+
}
|
|
153
|
+
const status = normalizeExecutionStatus(value.status);
|
|
154
|
+
if (!status) {
|
|
155
|
+
return void 0;
|
|
156
|
+
}
|
|
157
|
+
const pendingMessages2 = Array.isArray(value.pendingMessages) ? value.pendingMessages.map(normalizeMessage).filter((message) => Boolean(message)).filter((message) => message.conversationId === conversationId).filter((message) => message.injectedAtMs === void 0).sort(compareMessages) : [];
|
|
158
|
+
const inboundMessageIds = Array.isArray(value.inboundMessageIds) ? uniqueStrings(
|
|
159
|
+
value.inboundMessageIds.map((id) => typeof id === "string" ? id : void 0).filter((id) => Boolean(id))
|
|
160
|
+
) : [];
|
|
161
|
+
const lease = normalizeLease(value.lease);
|
|
162
|
+
const normalizedStatus = status === "idle" && lease ? "running" : status === "idle" && pendingMessages2.length > 0 ? "pending" : status;
|
|
163
|
+
return {
|
|
164
|
+
status: normalizedStatus,
|
|
165
|
+
inboundMessageIds: uniqueStrings([
|
|
166
|
+
...inboundMessageIds,
|
|
167
|
+
...pendingMessages2.map((message) => message.inboundMessageId)
|
|
168
|
+
]),
|
|
169
|
+
pendingCount: pendingMessages2.length,
|
|
170
|
+
pendingMessages: pendingMessages2,
|
|
171
|
+
lease,
|
|
172
|
+
lastCheckpointAtMs: toOptionalNumber(value.lastCheckpointAtMs),
|
|
173
|
+
lastEnqueuedAtMs: toOptionalNumber(value.lastEnqueuedAtMs),
|
|
174
|
+
runId: toOptionalString(value.runId),
|
|
175
|
+
updatedAtMs: toOptionalNumber(value.updatedAtMs)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function normalizeConversation(conversationId, value) {
|
|
179
|
+
if (!isRecord(value) || value.schemaVersion !== CONVERSATION_SCHEMA_VERSION) {
|
|
180
|
+
return void 0;
|
|
181
|
+
}
|
|
182
|
+
const storedConversationId = toOptionalString(value.conversationId);
|
|
183
|
+
const createdAtMs = toOptionalNumber(value.createdAtMs);
|
|
184
|
+
const lastActivityAtMs = toOptionalNumber(value.lastActivityAtMs);
|
|
185
|
+
const updatedAtMs = toOptionalNumber(value.updatedAtMs);
|
|
186
|
+
const execution = normalizeExecution(conversationId, value.execution);
|
|
187
|
+
const destination = value.destination === void 0 ? void 0 : parseDestination(value.destination);
|
|
188
|
+
if (storedConversationId !== conversationId || typeof createdAtMs !== "number" || typeof lastActivityAtMs !== "number" || typeof updatedAtMs !== "number" || !execution || value.destination !== void 0 && !destination) {
|
|
189
|
+
return void 0;
|
|
190
|
+
}
|
|
191
|
+
if (execution.pendingMessages.length > 0 && (!destination || execution.pendingMessages.some(
|
|
192
|
+
(message) => !sameDestination(message.destination, destination)
|
|
193
|
+
))) {
|
|
194
|
+
return void 0;
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
schemaVersion: CONVERSATION_SCHEMA_VERSION,
|
|
198
|
+
conversationId,
|
|
199
|
+
createdAtMs,
|
|
200
|
+
lastActivityAtMs,
|
|
201
|
+
updatedAtMs,
|
|
202
|
+
execution,
|
|
203
|
+
...destination ? { destination } : {},
|
|
204
|
+
...toOptionalString(value.title) ? { title: toOptionalString(value.title) } : {},
|
|
205
|
+
...toOptionalString(value.channelName) ? { channelName: toOptionalString(value.channelName) } : {},
|
|
206
|
+
...normalizeRequester(value.requester) ? { requester: normalizeRequester(value.requester) } : {},
|
|
207
|
+
...normalizeSource(value.source) ? { source: normalizeSource(value.source) } : {}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function emptyConversation(args) {
|
|
211
|
+
return {
|
|
212
|
+
schemaVersion: CONVERSATION_SCHEMA_VERSION,
|
|
213
|
+
conversationId: args.conversationId,
|
|
214
|
+
createdAtMs: args.nowMs,
|
|
215
|
+
lastActivityAtMs: args.nowMs,
|
|
216
|
+
updatedAtMs: args.nowMs,
|
|
217
|
+
...args.destination ? { destination: args.destination } : {},
|
|
218
|
+
...args.source ? { source: args.source } : {},
|
|
219
|
+
execution: {
|
|
220
|
+
status: "idle",
|
|
221
|
+
inboundMessageIds: [],
|
|
222
|
+
pendingCount: 0,
|
|
223
|
+
pendingMessages: [],
|
|
224
|
+
updatedAtMs: args.nowMs
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function isLeaseActive(lease, nowMs) {
|
|
229
|
+
return Boolean(lease && lease.expiresAtMs > nowMs);
|
|
230
|
+
}
|
|
231
|
+
function pendingMessages(conversation) {
|
|
232
|
+
return [...conversation.execution.pendingMessages].sort(compareMessages);
|
|
233
|
+
}
|
|
234
|
+
function hasRunnableWork(conversation) {
|
|
235
|
+
return conversation.execution.status !== "idle" || pendingMessages(conversation).length > 0;
|
|
236
|
+
}
|
|
237
|
+
function executionWithPendingMessages(execution, pending) {
|
|
238
|
+
const pendingMessages2 = [...pending].sort(compareMessages);
|
|
239
|
+
const status = execution.status === "idle" && execution.lease ? "running" : execution.status === "idle" && pendingMessages2.length > 0 ? "pending" : execution.status;
|
|
240
|
+
return {
|
|
241
|
+
...execution,
|
|
242
|
+
status,
|
|
243
|
+
inboundMessageIds: uniqueStrings([
|
|
244
|
+
...execution.inboundMessageIds,
|
|
245
|
+
...pendingMessages2.map((message) => message.inboundMessageId)
|
|
246
|
+
]),
|
|
247
|
+
pendingMessages: pendingMessages2,
|
|
248
|
+
pendingCount: pendingMessages2.length
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function withExecutionUpdate(conversation, execution, nowMs) {
|
|
252
|
+
return {
|
|
253
|
+
...conversation,
|
|
254
|
+
updatedAtMs: nowMs,
|
|
255
|
+
execution: {
|
|
256
|
+
...executionWithPendingMessages(execution, execution.pendingMessages),
|
|
257
|
+
updatedAtMs: nowMs
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
async function getConnectedState(stateAdapter) {
|
|
262
|
+
const state = stateAdapter ?? getStateAdapter();
|
|
263
|
+
await state.connect();
|
|
264
|
+
return state;
|
|
265
|
+
}
|
|
266
|
+
async function sleep(ms) {
|
|
267
|
+
await new Promise((resolve) => {
|
|
268
|
+
const timer = setTimeout(resolve, ms);
|
|
269
|
+
timer.unref?.();
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async function withIndexLock(state, indexKey, callback) {
|
|
273
|
+
const startedAtMs = now();
|
|
274
|
+
let lock;
|
|
275
|
+
while (true) {
|
|
276
|
+
lock = await state.acquireLock(
|
|
277
|
+
indexLockKey(indexKey),
|
|
278
|
+
CONVERSATION_INDEX_LOCK_TTL_MS
|
|
279
|
+
);
|
|
280
|
+
if (lock) {
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
if (now() - startedAtMs >= CONVERSATION_INDEX_LOCK_WAIT_MS) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Could not acquire conversation index lock for ${indexKey}`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
await sleep(CONVERSATION_INDEX_LOCK_RETRY_MS);
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
return await callback();
|
|
292
|
+
} finally {
|
|
293
|
+
await state.releaseLock(lock);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function normalizeIndexEntry(value) {
|
|
297
|
+
if (!isRecord(value)) {
|
|
298
|
+
return void 0;
|
|
299
|
+
}
|
|
300
|
+
const conversationId = toOptionalString(value.conversationId);
|
|
301
|
+
const score = toOptionalNumber(value.score);
|
|
302
|
+
if (!conversationId || typeof score !== "number") {
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
return { conversationId, score };
|
|
306
|
+
}
|
|
307
|
+
function uniqueIndexEntries(value) {
|
|
308
|
+
if (!Array.isArray(value)) {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
const entries = /* @__PURE__ */ new Map();
|
|
312
|
+
for (const item of value) {
|
|
313
|
+
const entry = normalizeIndexEntry(item);
|
|
314
|
+
if (!entry) {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const existing = entries.get(entry.conversationId);
|
|
318
|
+
if (!existing || entry.score > existing.score) {
|
|
319
|
+
entries.set(entry.conversationId, entry);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return [...entries.values()];
|
|
323
|
+
}
|
|
324
|
+
function retainedIndexEntries(indexKey, entries) {
|
|
325
|
+
if (indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) {
|
|
326
|
+
return entries.sort(compareIndexDescending).slice(0, CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH);
|
|
327
|
+
}
|
|
328
|
+
if (indexKey === CONVERSATION_ACTIVE_INDEX_KEY) {
|
|
329
|
+
return entries.sort(compareIndexAscending);
|
|
330
|
+
}
|
|
331
|
+
throw new Error(`Unknown conversation index ${indexKey}`);
|
|
332
|
+
}
|
|
333
|
+
function redisIndexKey(indexKey) {
|
|
334
|
+
const prefix = getChatConfig().state.keyPrefix;
|
|
335
|
+
return [...prefix ? [prefix] : [], indexKey].join(":");
|
|
336
|
+
}
|
|
337
|
+
function parseRedisIndexEntries(values) {
|
|
338
|
+
if (!Array.isArray(values)) {
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
const entries = [];
|
|
342
|
+
for (let index = 0; index < values.length; index += 2) {
|
|
343
|
+
const conversationId = toOptionalString(values[index]);
|
|
344
|
+
const score = typeof values[index + 1] === "number" ? values[index + 1] : Number(values[index + 1]);
|
|
345
|
+
if (!conversationId || !Number.isFinite(score)) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
entries.push({ conversationId, score });
|
|
349
|
+
}
|
|
350
|
+
return entries;
|
|
351
|
+
}
|
|
352
|
+
function redisConversationIndexStore(client) {
|
|
353
|
+
const upsertBoundedActivityScript = `
|
|
354
|
+
redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
|
|
355
|
+
redis.call("PEXPIRE", KEYS[1], ARGV[3])
|
|
356
|
+
local extra = redis.call("ZCARD", KEYS[1]) - tonumber(ARGV[4])
|
|
357
|
+
if extra > 0 then
|
|
358
|
+
redis.call("ZREMRANGEBYRANK", KEYS[1], 0, extra - 1)
|
|
359
|
+
end
|
|
360
|
+
return 1
|
|
361
|
+
`;
|
|
362
|
+
return {
|
|
363
|
+
async list(args) {
|
|
364
|
+
const key = redisIndexKey(args.indexKey);
|
|
365
|
+
const limit = args.limit;
|
|
366
|
+
const offset = Math.max(0, args.offset ?? 0);
|
|
367
|
+
if (limit === 0) {
|
|
368
|
+
return [];
|
|
369
|
+
}
|
|
370
|
+
const values = args.scoreMax !== void 0 ? await client.sendCommand([
|
|
371
|
+
"ZRANGEBYSCORE",
|
|
372
|
+
key,
|
|
373
|
+
"-inf",
|
|
374
|
+
String(args.scoreMax),
|
|
375
|
+
"WITHSCORES",
|
|
376
|
+
...limit !== void 0 || offset > 0 ? ["LIMIT", String(offset), String(limit ?? 1e9)] : []
|
|
377
|
+
]) : await client.sendCommand([
|
|
378
|
+
args.order === "asc" ? "ZRANGE" : "ZREVRANGE",
|
|
379
|
+
key,
|
|
380
|
+
String(offset),
|
|
381
|
+
String(
|
|
382
|
+
limit === void 0 ? -1 : offset + Math.max(0, limit - 1)
|
|
383
|
+
),
|
|
384
|
+
"WITHSCORES"
|
|
385
|
+
]);
|
|
386
|
+
return parseRedisIndexEntries(values);
|
|
387
|
+
},
|
|
388
|
+
async remove(args) {
|
|
389
|
+
await client.sendCommand([
|
|
390
|
+
"ZREM",
|
|
391
|
+
redisIndexKey(args.indexKey),
|
|
392
|
+
args.conversationId
|
|
393
|
+
]);
|
|
394
|
+
},
|
|
395
|
+
async upsert(args) {
|
|
396
|
+
const key = redisIndexKey(args.indexKey);
|
|
397
|
+
if (args.indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) {
|
|
398
|
+
await client.sendCommand([
|
|
399
|
+
"EVAL",
|
|
400
|
+
upsertBoundedActivityScript,
|
|
401
|
+
"1",
|
|
402
|
+
key,
|
|
403
|
+
String(args.score),
|
|
404
|
+
args.conversationId,
|
|
405
|
+
String(JUNIOR_THREAD_STATE_TTL_MS),
|
|
406
|
+
String(CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH)
|
|
407
|
+
]);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (args.indexKey === CONVERSATION_ACTIVE_INDEX_KEY) {
|
|
411
|
+
await client.sendCommand([
|
|
412
|
+
"ZADD",
|
|
413
|
+
key,
|
|
414
|
+
String(args.score),
|
|
415
|
+
args.conversationId
|
|
416
|
+
]);
|
|
417
|
+
await client.sendCommand([
|
|
418
|
+
"PEXPIRE",
|
|
419
|
+
key,
|
|
420
|
+
String(JUNIOR_THREAD_STATE_TTL_MS)
|
|
421
|
+
]);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
throw new Error(`Unknown conversation index ${args.indexKey}`);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function emulatedConversationIndexStore(state) {
|
|
429
|
+
const readIndex = async (indexKey) => uniqueIndexEntries(await state.get(indexKey));
|
|
430
|
+
const writeIndex = async (indexKey, entries) => {
|
|
431
|
+
await state.set(indexKey, entries, JUNIOR_THREAD_STATE_TTL_MS);
|
|
432
|
+
};
|
|
433
|
+
return {
|
|
434
|
+
async list(args) {
|
|
435
|
+
const entries = (await readIndex(args.indexKey)).filter(
|
|
436
|
+
(entry) => args.scoreMax === void 0 ? true : entry.score <= args.scoreMax
|
|
437
|
+
).sort(
|
|
438
|
+
args.order === "asc" ? compareIndexAscending : compareIndexDescending
|
|
439
|
+
);
|
|
440
|
+
const offset = Math.max(0, args.offset ?? 0);
|
|
441
|
+
return entries.slice(
|
|
442
|
+
offset,
|
|
443
|
+
args.limit === void 0 ? entries.length : offset + args.limit
|
|
444
|
+
);
|
|
445
|
+
},
|
|
446
|
+
async remove(args) {
|
|
447
|
+
await withIndexLock(state, args.indexKey, async () => {
|
|
448
|
+
const entries = await readIndex(args.indexKey);
|
|
449
|
+
const next = entries.filter(
|
|
450
|
+
(entry) => entry.conversationId !== args.conversationId
|
|
451
|
+
);
|
|
452
|
+
if (next.length === entries.length) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
await writeIndex(args.indexKey, next);
|
|
456
|
+
});
|
|
457
|
+
},
|
|
458
|
+
async upsert(args) {
|
|
459
|
+
await withIndexLock(state, args.indexKey, async () => {
|
|
460
|
+
const entries = await readIndex(args.indexKey);
|
|
461
|
+
const withoutCurrent = entries.filter(
|
|
462
|
+
(entry) => entry.conversationId !== args.conversationId
|
|
463
|
+
);
|
|
464
|
+
const next = retainedIndexEntries(args.indexKey, [
|
|
465
|
+
...withoutCurrent,
|
|
466
|
+
{ conversationId: args.conversationId, score: args.score }
|
|
467
|
+
]);
|
|
468
|
+
await writeIndex(args.indexKey, next);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
async function getConversationIndexStore(state) {
|
|
474
|
+
const redisStateAdapter = await getDefaultRedisStateAdapterFor(state);
|
|
475
|
+
if (redisStateAdapter) {
|
|
476
|
+
return redisConversationIndexStore(redisStateAdapter.getClient());
|
|
477
|
+
}
|
|
478
|
+
return emulatedConversationIndexStore(state);
|
|
479
|
+
}
|
|
480
|
+
async function upsertIndexEntry(args) {
|
|
481
|
+
const index = await getConversationIndexStore(args.state);
|
|
482
|
+
await index.upsert({
|
|
483
|
+
conversationId: args.conversationId,
|
|
484
|
+
indexKey: args.indexKey,
|
|
485
|
+
score: args.score
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
async function removeIndexEntry(args) {
|
|
489
|
+
const index = await getConversationIndexStore(args.state);
|
|
490
|
+
await index.remove({
|
|
491
|
+
conversationId: args.conversationId,
|
|
492
|
+
indexKey: args.indexKey
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
async function acquireMutationLock(state, conversationId) {
|
|
496
|
+
const startedAtMs = now();
|
|
497
|
+
while (true) {
|
|
498
|
+
const lock = await state.acquireLock(
|
|
499
|
+
mutationLockKey(conversationId),
|
|
500
|
+
CONVERSATION_MUTATION_LOCK_TTL_MS
|
|
501
|
+
);
|
|
502
|
+
if (lock) {
|
|
503
|
+
return lock;
|
|
504
|
+
}
|
|
505
|
+
if (now() - startedAtMs >= CONVERSATION_MUTATION_WAIT_MS) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Could not acquire conversation mutation lock for ${conversationId}`
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
await sleep(CONVERSATION_MUTATION_RETRY_MS);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
async function withConversationMutation(args, callback) {
|
|
514
|
+
const state = await getConnectedState(args.state);
|
|
515
|
+
const lock = await acquireMutationLock(state, args.conversationId);
|
|
516
|
+
try {
|
|
517
|
+
return await callback(state);
|
|
518
|
+
} finally {
|
|
519
|
+
await state.releaseLock(lock);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
async function readConversation(state, conversationId) {
|
|
523
|
+
const raw = await state.get(conversationKey(conversationId));
|
|
524
|
+
if (raw == null) {
|
|
525
|
+
return void 0;
|
|
526
|
+
}
|
|
527
|
+
const conversation = normalizeConversation(conversationId, raw);
|
|
528
|
+
if (!conversation) {
|
|
529
|
+
throw new InvalidConversationRecordError(conversationId);
|
|
530
|
+
}
|
|
531
|
+
return conversation;
|
|
532
|
+
}
|
|
533
|
+
async function writeConversation(state, conversation) {
|
|
534
|
+
const execution = executionWithPendingMessages(
|
|
535
|
+
conversation.execution,
|
|
536
|
+
conversation.execution.pendingMessages
|
|
537
|
+
);
|
|
538
|
+
const next = {
|
|
539
|
+
...conversation,
|
|
540
|
+
execution
|
|
541
|
+
};
|
|
542
|
+
await state.set(
|
|
543
|
+
conversationKey(next.conversationId),
|
|
544
|
+
next,
|
|
545
|
+
JUNIOR_THREAD_STATE_TTL_MS
|
|
546
|
+
);
|
|
547
|
+
await upsertIndexEntry({
|
|
548
|
+
state,
|
|
549
|
+
indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
|
|
550
|
+
conversationId: next.conversationId,
|
|
551
|
+
score: next.lastActivityAtMs
|
|
552
|
+
});
|
|
553
|
+
if (!hasRunnableWork(next)) {
|
|
554
|
+
await removeIndexEntry({
|
|
555
|
+
state,
|
|
556
|
+
indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
|
|
557
|
+
conversationId: next.conversationId
|
|
558
|
+
});
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
await upsertIndexEntry({
|
|
562
|
+
state,
|
|
563
|
+
indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
|
|
564
|
+
conversationId: next.conversationId,
|
|
565
|
+
score: next.execution.updatedAtMs ?? next.updatedAtMs
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
function assertSameConversationDestination(args) {
|
|
569
|
+
if (!args.current || sameDestination(args.current, args.next)) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
throw new Error(
|
|
573
|
+
`Conversation destination changed for ${args.conversationId}`
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
function conversationWorkState(conversation) {
|
|
577
|
+
const lease = conversation.execution.lease;
|
|
578
|
+
return {
|
|
579
|
+
...conversation,
|
|
580
|
+
lastEnqueuedAtMs: conversation.execution.lastEnqueuedAtMs,
|
|
581
|
+
...lease ? {
|
|
582
|
+
lease: {
|
|
583
|
+
acquiredAtMs: lease.acquiredAtMs,
|
|
584
|
+
lastCheckInAtMs: lease.lastCheckInAtMs,
|
|
585
|
+
leaseExpiresAtMs: lease.expiresAtMs,
|
|
586
|
+
leaseToken: lease.token
|
|
587
|
+
}
|
|
588
|
+
} : {},
|
|
589
|
+
messages: pendingMessages(conversation),
|
|
590
|
+
needsRun: hasRunnableWork(conversation)
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
async function getConversation(args) {
|
|
594
|
+
const state = await getConnectedState(args.state);
|
|
595
|
+
return await readConversation(state, args.conversationId);
|
|
596
|
+
}
|
|
597
|
+
async function getConversationWorkState(args) {
|
|
598
|
+
const conversation = await getConversation(args);
|
|
599
|
+
return conversation ? conversationWorkState(conversation) : void 0;
|
|
600
|
+
}
|
|
601
|
+
async function appendInboundMessage(args) {
|
|
602
|
+
const nowMs = args.nowMs ?? now();
|
|
603
|
+
return await withConversationMutation(
|
|
604
|
+
{ conversationId: args.message.conversationId, state: args.state },
|
|
605
|
+
async (state) => {
|
|
606
|
+
const current = await readConversation(state, args.message.conversationId) ?? emptyConversation({
|
|
607
|
+
conversationId: args.message.conversationId,
|
|
608
|
+
destination: args.message.destination,
|
|
609
|
+
nowMs,
|
|
610
|
+
source: args.message.source
|
|
611
|
+
});
|
|
612
|
+
assertSameConversationDestination({
|
|
613
|
+
conversationId: args.message.conversationId,
|
|
614
|
+
current: current.destination,
|
|
615
|
+
next: args.message.destination
|
|
616
|
+
});
|
|
617
|
+
const existingPending = current.execution.pendingMessages.some(
|
|
618
|
+
(message) => message.inboundMessageId === args.message.inboundMessageId
|
|
619
|
+
);
|
|
620
|
+
const existing = current.execution.inboundMessageIds.includes(
|
|
621
|
+
args.message.inboundMessageId
|
|
622
|
+
);
|
|
623
|
+
if (existing) {
|
|
624
|
+
if (!existingPending) {
|
|
625
|
+
return { status: "duplicate" };
|
|
626
|
+
}
|
|
627
|
+
const nextStatus = current.execution.status === "idle" ? "pending" : current.execution.status;
|
|
628
|
+
await writeConversation(
|
|
629
|
+
state,
|
|
630
|
+
withExecutionUpdate(
|
|
631
|
+
current,
|
|
632
|
+
{
|
|
633
|
+
...current.execution,
|
|
634
|
+
status: nextStatus
|
|
635
|
+
},
|
|
636
|
+
nowMs
|
|
637
|
+
)
|
|
638
|
+
);
|
|
639
|
+
return { status: "duplicate" };
|
|
640
|
+
}
|
|
641
|
+
const status = current.execution.lease && current.execution.status === "running" ? "running" : current.execution.lease ? "awaiting_resume" : "pending";
|
|
642
|
+
const next = {
|
|
643
|
+
...current,
|
|
644
|
+
destination: current.destination ?? args.message.destination,
|
|
645
|
+
source: current.source ?? args.message.source,
|
|
646
|
+
lastActivityAtMs: nowMs
|
|
647
|
+
};
|
|
648
|
+
await writeConversation(
|
|
649
|
+
state,
|
|
650
|
+
withExecutionUpdate(
|
|
651
|
+
next,
|
|
652
|
+
{
|
|
653
|
+
...current.execution,
|
|
654
|
+
status,
|
|
655
|
+
inboundMessageIds: [
|
|
656
|
+
...current.execution.inboundMessageIds,
|
|
657
|
+
args.message.inboundMessageId
|
|
658
|
+
],
|
|
659
|
+
pendingMessages: [
|
|
660
|
+
...current.execution.pendingMessages,
|
|
661
|
+
args.message
|
|
662
|
+
].sort(compareMessages)
|
|
663
|
+
},
|
|
664
|
+
nowMs
|
|
665
|
+
)
|
|
666
|
+
);
|
|
667
|
+
return { status: "appended" };
|
|
668
|
+
}
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
async function requestConversationWork(args) {
|
|
672
|
+
const nowMs = args.nowMs ?? now();
|
|
673
|
+
return await withConversationMutation(args, async (state) => {
|
|
674
|
+
const existing = await readConversation(state, args.conversationId);
|
|
675
|
+
if (existing) {
|
|
676
|
+
assertSameConversationDestination({
|
|
677
|
+
conversationId: args.conversationId,
|
|
678
|
+
current: existing.destination,
|
|
679
|
+
next: args.destination
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
const current = existing ?? emptyConversation({
|
|
683
|
+
conversationId: args.conversationId,
|
|
684
|
+
destination: args.destination,
|
|
685
|
+
nowMs
|
|
686
|
+
});
|
|
687
|
+
const status = current.execution.lease ? "awaiting_resume" : "pending";
|
|
688
|
+
await writeConversation(
|
|
689
|
+
state,
|
|
690
|
+
withExecutionUpdate(
|
|
691
|
+
{
|
|
692
|
+
...current,
|
|
693
|
+
destination: current.destination ?? args.destination
|
|
694
|
+
},
|
|
695
|
+
{
|
|
696
|
+
...current.execution,
|
|
697
|
+
status
|
|
698
|
+
},
|
|
699
|
+
nowMs
|
|
700
|
+
)
|
|
701
|
+
);
|
|
702
|
+
return { status: existing === void 0 ? "created" : "updated" };
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
async function recordConversationActivity(args) {
|
|
706
|
+
const nowMs = args.nowMs ?? now();
|
|
707
|
+
const activityAtMs = args.activityAtMs ?? nowMs;
|
|
708
|
+
await withConversationMutation(args, async (state) => {
|
|
709
|
+
const existing = await readConversation(state, args.conversationId);
|
|
710
|
+
if (existing && args.destination) {
|
|
711
|
+
assertSameConversationDestination({
|
|
712
|
+
conversationId: args.conversationId,
|
|
713
|
+
current: existing.destination,
|
|
714
|
+
next: args.destination
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
const current = existing ?? emptyConversation({
|
|
718
|
+
conversationId: args.conversationId,
|
|
719
|
+
destination: args.destination,
|
|
720
|
+
nowMs,
|
|
721
|
+
source: args.source
|
|
722
|
+
});
|
|
723
|
+
await writeConversation(state, {
|
|
724
|
+
...current,
|
|
725
|
+
...current.destination ?? args.destination ? { destination: current.destination ?? args.destination } : {},
|
|
726
|
+
...current.source ?? args.source ? { source: current.source ?? args.source } : {},
|
|
727
|
+
...current.channelName ?? args.channelName ? { channelName: current.channelName ?? args.channelName } : {},
|
|
728
|
+
...current.requester ?? args.requester ? { requester: current.requester ?? args.requester } : {},
|
|
729
|
+
...current.title ?? args.title ? { title: current.title ?? args.title } : {},
|
|
730
|
+
lastActivityAtMs: Math.max(current.lastActivityAtMs, activityAtMs),
|
|
731
|
+
updatedAtMs: nowMs,
|
|
732
|
+
execution: executionWithPendingMessages(
|
|
733
|
+
current.execution,
|
|
734
|
+
current.execution.pendingMessages
|
|
735
|
+
)
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
async function markConversationWorkEnqueued(args) {
|
|
740
|
+
const nowMs = args.nowMs ?? now();
|
|
741
|
+
await withConversationMutation(args, async (state) => {
|
|
742
|
+
const current = await readConversation(state, args.conversationId);
|
|
743
|
+
if (!current) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
await writeConversation(
|
|
747
|
+
state,
|
|
748
|
+
withExecutionUpdate(
|
|
749
|
+
current,
|
|
750
|
+
{
|
|
751
|
+
...current.execution,
|
|
752
|
+
lastEnqueuedAtMs: nowMs
|
|
753
|
+
},
|
|
754
|
+
nowMs
|
|
755
|
+
)
|
|
756
|
+
);
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
async function startConversationWork(args) {
|
|
760
|
+
const nowMs = args.nowMs ?? now();
|
|
761
|
+
return await withConversationMutation(args, async (state) => {
|
|
762
|
+
const current = await readConversation(state, args.conversationId);
|
|
763
|
+
if (!current) {
|
|
764
|
+
return { status: "no_work" };
|
|
765
|
+
}
|
|
766
|
+
if (isLeaseActive(current.execution.lease, nowMs)) {
|
|
767
|
+
return {
|
|
768
|
+
status: "active",
|
|
769
|
+
leaseExpiresAtMs: current.execution.lease.expiresAtMs
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
if (!hasRunnableWork(current)) {
|
|
773
|
+
return { status: "no_work" };
|
|
774
|
+
}
|
|
775
|
+
const lease = {
|
|
776
|
+
token: randomUUID(),
|
|
777
|
+
acquiredAtMs: nowMs,
|
|
778
|
+
lastCheckInAtMs: nowMs,
|
|
779
|
+
expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
|
|
780
|
+
};
|
|
781
|
+
await writeConversation(
|
|
782
|
+
state,
|
|
783
|
+
withExecutionUpdate(
|
|
784
|
+
current,
|
|
785
|
+
{
|
|
786
|
+
...current.execution,
|
|
787
|
+
lease,
|
|
788
|
+
status: "running",
|
|
789
|
+
runId: current.execution.runId ?? randomUUID(),
|
|
790
|
+
lastEnqueuedAtMs: void 0
|
|
791
|
+
},
|
|
792
|
+
nowMs
|
|
793
|
+
)
|
|
794
|
+
);
|
|
795
|
+
return {
|
|
796
|
+
status: "acquired",
|
|
797
|
+
leaseToken: lease.token,
|
|
798
|
+
leaseExpiresAtMs: lease.expiresAtMs
|
|
799
|
+
};
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
async function checkInConversationWork(args) {
|
|
803
|
+
const nowMs = args.nowMs ?? now();
|
|
804
|
+
return await withConversationMutation(args, async (state) => {
|
|
805
|
+
const current = await readConversation(state, args.conversationId);
|
|
806
|
+
if (!current || current.execution.lease?.token !== args.leaseToken) {
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
await writeConversation(
|
|
810
|
+
state,
|
|
811
|
+
withExecutionUpdate(
|
|
812
|
+
current,
|
|
813
|
+
{
|
|
814
|
+
...current.execution,
|
|
815
|
+
lease: {
|
|
816
|
+
...current.execution.lease,
|
|
817
|
+
lastCheckInAtMs: nowMs,
|
|
818
|
+
expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
|
|
819
|
+
}
|
|
820
|
+
},
|
|
821
|
+
nowMs
|
|
822
|
+
)
|
|
823
|
+
);
|
|
824
|
+
return true;
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
async function drainConversationMailbox(args) {
|
|
828
|
+
const nowMs = args.nowMs ?? now();
|
|
829
|
+
const pending = await withConversationMutation(args, async (state) => {
|
|
830
|
+
const current = await readConversation(state, args.conversationId);
|
|
831
|
+
if (!current || current.execution.lease?.token !== args.leaseToken) {
|
|
832
|
+
throw new Error(
|
|
833
|
+
`Conversation lease is not held for ${args.conversationId}`
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
return pendingMessages(current);
|
|
837
|
+
});
|
|
838
|
+
if (pending.length === 0) {
|
|
839
|
+
return [];
|
|
840
|
+
}
|
|
841
|
+
const acknowledgedIds = await args.inject(pending);
|
|
842
|
+
const offeredIds = new Set(
|
|
843
|
+
pending.map((message) => message.inboundMessageId)
|
|
844
|
+
);
|
|
845
|
+
for (const inboundMessageId of acknowledgedIds ?? []) {
|
|
846
|
+
if (!offeredIds.has(inboundMessageId)) {
|
|
847
|
+
throw new Error(
|
|
848
|
+
`Conversation mailbox acknowledgement is not pending for ${args.conversationId}`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
const drainedIds = new Set(
|
|
853
|
+
acknowledgedIds ?? pending.map((message) => message.inboundMessageId)
|
|
854
|
+
);
|
|
855
|
+
await withConversationMutation(args, async (state) => {
|
|
856
|
+
const current = await readConversation(state, args.conversationId);
|
|
857
|
+
if (!current || current.execution.lease?.token !== args.leaseToken) {
|
|
858
|
+
throw new Error(
|
|
859
|
+
`Conversation lease is not held for ${args.conversationId}`
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
const pendingMessages2 = current.execution.pendingMessages.filter(
|
|
863
|
+
(message) => !drainedIds.has(message.inboundMessageId)
|
|
864
|
+
);
|
|
865
|
+
await writeConversation(
|
|
866
|
+
state,
|
|
867
|
+
withExecutionUpdate(
|
|
868
|
+
current,
|
|
869
|
+
{
|
|
870
|
+
...current.execution,
|
|
871
|
+
status: current.execution.status === "pending" && pendingMessages2.length === 0 ? "running" : current.execution.status,
|
|
872
|
+
pendingMessages: pendingMessages2
|
|
873
|
+
},
|
|
874
|
+
nowMs
|
|
875
|
+
)
|
|
876
|
+
);
|
|
877
|
+
});
|
|
878
|
+
return pending.filter((message) => drainedIds.has(message.inboundMessageId));
|
|
879
|
+
}
|
|
880
|
+
async function markConversationMessagesInjected(args) {
|
|
881
|
+
const nowMs = args.nowMs ?? now();
|
|
882
|
+
const inboundMessageIds = new Set(args.inboundMessageIds);
|
|
883
|
+
return await withConversationMutation(args, async (state) => {
|
|
884
|
+
const current = await readConversation(state, args.conversationId);
|
|
885
|
+
if (!current || current.execution.lease?.token !== args.leaseToken) {
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
if (inboundMessageIds.size === 0) {
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
891
|
+
const pendingMessages2 = current.execution.pendingMessages.filter(
|
|
892
|
+
(message) => !inboundMessageIds.has(message.inboundMessageId)
|
|
893
|
+
);
|
|
894
|
+
if (pendingMessages2.length === current.execution.pendingMessages.length) {
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
await writeConversation(
|
|
898
|
+
state,
|
|
899
|
+
withExecutionUpdate(
|
|
900
|
+
current,
|
|
901
|
+
{
|
|
902
|
+
...current.execution,
|
|
903
|
+
pendingMessages: pendingMessages2
|
|
904
|
+
},
|
|
905
|
+
nowMs
|
|
906
|
+
)
|
|
907
|
+
);
|
|
908
|
+
return true;
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
async function requestConversationContinuation(args) {
|
|
912
|
+
const nowMs = args.nowMs ?? now();
|
|
913
|
+
return await withConversationMutation(args, async (state) => {
|
|
914
|
+
const current = await readConversation(state, args.conversationId);
|
|
915
|
+
if (!current || current.execution.lease?.token !== args.leaseToken) {
|
|
916
|
+
return false;
|
|
917
|
+
}
|
|
918
|
+
assertSameConversationDestination({
|
|
919
|
+
conversationId: args.conversationId,
|
|
920
|
+
current: current.destination,
|
|
921
|
+
next: args.destination
|
|
922
|
+
});
|
|
923
|
+
await writeConversation(
|
|
924
|
+
state,
|
|
925
|
+
withExecutionUpdate(
|
|
926
|
+
current,
|
|
927
|
+
{
|
|
928
|
+
...current.execution,
|
|
929
|
+
status: "awaiting_resume"
|
|
930
|
+
},
|
|
931
|
+
nowMs
|
|
932
|
+
)
|
|
933
|
+
);
|
|
934
|
+
return true;
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
async function releaseConversationWork(args) {
|
|
938
|
+
const nowMs = args.nowMs ?? now();
|
|
939
|
+
return await withConversationMutation(args, async (state) => {
|
|
940
|
+
const current = await readConversation(state, args.conversationId);
|
|
941
|
+
if (!current || current.execution.lease?.token !== args.leaseToken) {
|
|
942
|
+
return false;
|
|
943
|
+
}
|
|
944
|
+
await writeConversation(
|
|
945
|
+
state,
|
|
946
|
+
withExecutionUpdate(
|
|
947
|
+
current,
|
|
948
|
+
{
|
|
949
|
+
...current.execution,
|
|
950
|
+
lease: void 0,
|
|
951
|
+
status: current.execution.status === "running" ? "pending" : current.execution.status
|
|
952
|
+
},
|
|
953
|
+
nowMs
|
|
954
|
+
)
|
|
955
|
+
);
|
|
956
|
+
return true;
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
async function completeConversationWork(args) {
|
|
960
|
+
const nowMs = args.nowMs ?? now();
|
|
961
|
+
return await withConversationMutation(args, async (state) => {
|
|
962
|
+
const current = await readConversation(state, args.conversationId);
|
|
963
|
+
if (!current || current.execution.lease?.token !== args.leaseToken) {
|
|
964
|
+
return "lost_lease";
|
|
965
|
+
}
|
|
966
|
+
const hasPending = pendingMessages(current).length > 0;
|
|
967
|
+
const needsRun = current.execution.status === "awaiting_resume";
|
|
968
|
+
const runnable = needsRun || hasPending;
|
|
969
|
+
await writeConversation(
|
|
970
|
+
state,
|
|
971
|
+
withExecutionUpdate(
|
|
972
|
+
current,
|
|
973
|
+
{
|
|
974
|
+
...current.execution,
|
|
975
|
+
lease: void 0,
|
|
976
|
+
status: runnable ? "pending" : "idle",
|
|
977
|
+
runId: runnable ? current.execution.runId : void 0
|
|
978
|
+
},
|
|
979
|
+
nowMs
|
|
980
|
+
)
|
|
981
|
+
);
|
|
982
|
+
return runnable ? "pending" : "completed";
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
async function clearExpiredConversationLease(args) {
|
|
986
|
+
const nowMs = args.nowMs ?? now();
|
|
987
|
+
return await withConversationMutation(args, async (state) => {
|
|
988
|
+
const current = await readConversation(state, args.conversationId);
|
|
989
|
+
if (!current?.execution.lease || current.execution.lease.expiresAtMs > nowMs) {
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
992
|
+
await writeConversation(
|
|
993
|
+
state,
|
|
994
|
+
withExecutionUpdate(
|
|
995
|
+
current,
|
|
996
|
+
{
|
|
997
|
+
...current.execution,
|
|
998
|
+
lease: void 0,
|
|
999
|
+
status: "pending"
|
|
1000
|
+
},
|
|
1001
|
+
nowMs
|
|
1002
|
+
)
|
|
1003
|
+
);
|
|
1004
|
+
return true;
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
async function removeActiveConversation(args) {
|
|
1008
|
+
const state = await getConnectedState(args.state);
|
|
1009
|
+
await removeIndexEntry({
|
|
1010
|
+
state,
|
|
1011
|
+
indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
|
|
1012
|
+
conversationId: args.conversationId
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
async function listActiveConversationIds(args = {}) {
|
|
1016
|
+
const state = await getConnectedState(args.state);
|
|
1017
|
+
const index = await getConversationIndexStore(state);
|
|
1018
|
+
const entries = await index.list({
|
|
1019
|
+
indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
|
|
1020
|
+
limit: args.limit,
|
|
1021
|
+
order: "asc",
|
|
1022
|
+
scoreMax: args.staleBeforeMs
|
|
1023
|
+
});
|
|
1024
|
+
return entries.map((entry) => entry.conversationId);
|
|
1025
|
+
}
|
|
1026
|
+
async function listConversationsByActivity(args = {}) {
|
|
1027
|
+
const state = await getConnectedState(args.state);
|
|
1028
|
+
const index = await getConversationIndexStore(state);
|
|
1029
|
+
const entries = await index.list({
|
|
1030
|
+
indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
|
|
1031
|
+
limit: args.limit ?? CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH,
|
|
1032
|
+
offset: args.offset,
|
|
1033
|
+
order: "desc"
|
|
1034
|
+
});
|
|
1035
|
+
const conversations = [];
|
|
1036
|
+
for (const entry of entries) {
|
|
1037
|
+
try {
|
|
1038
|
+
const conversation = await readConversation(state, entry.conversationId);
|
|
1039
|
+
if (conversation) {
|
|
1040
|
+
conversations.push(conversation);
|
|
1041
|
+
}
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
if (!(error instanceof InvalidConversationRecordError)) {
|
|
1044
|
+
throw error;
|
|
1045
|
+
}
|
|
1046
|
+
await removeIndexEntry({
|
|
1047
|
+
state,
|
|
1048
|
+
indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
|
|
1049
|
+
conversationId: entry.conversationId
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return conversations;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
export {
|
|
1057
|
+
CONVERSATION_WORK_CHECK_IN_INTERVAL_MS,
|
|
1058
|
+
CONVERSATION_WORK_STALE_ENQUEUE_MS,
|
|
1059
|
+
getConversation,
|
|
1060
|
+
getConversationWorkState,
|
|
1061
|
+
appendInboundMessage,
|
|
1062
|
+
requestConversationWork,
|
|
1063
|
+
recordConversationActivity,
|
|
1064
|
+
markConversationWorkEnqueued,
|
|
1065
|
+
startConversationWork,
|
|
1066
|
+
checkInConversationWork,
|
|
1067
|
+
drainConversationMailbox,
|
|
1068
|
+
markConversationMessagesInjected,
|
|
1069
|
+
requestConversationContinuation,
|
|
1070
|
+
releaseConversationWork,
|
|
1071
|
+
completeConversationWork,
|
|
1072
|
+
clearExpiredConversationLease,
|
|
1073
|
+
removeActiveConversation,
|
|
1074
|
+
listActiveConversationIds,
|
|
1075
|
+
listConversationsByActivity
|
|
1076
|
+
};
|