@sentry/junior 0.115.0 → 0.116.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{agent-hooks-4EUUXP2F.js → agent-hooks-67NFITDH.js} +9 -9
- package/dist/api/conversations/detail.d.ts +18 -0
- package/dist/api/conversations/event-page.d.ts +1 -1
- package/dist/api/conversations/events.d.ts +1 -1
- package/dist/api/people/profile.d.ts +2 -2
- package/dist/api/people/shared.d.ts +1 -1
- package/dist/api/personal-tokens/create.d.ts +11 -0
- package/dist/api/personal-tokens/list.d.ts +12 -0
- package/dist/api/personal-tokens/revoke.d.ts +5 -0
- package/dist/api/personal-tokens/routes.d.ts +4 -0
- package/dist/api/plugins.d.ts +0 -1
- package/dist/api/route.d.ts +1 -1
- package/dist/api/schema/conversation.d.ts +18 -0
- package/dist/api/schema/person.d.ts +3 -3
- package/dist/api/schema/personal-token.d.ts +38 -0
- package/dist/api/schema.d.ts +2 -0
- package/dist/api/schema.js +15 -3
- package/dist/api.d.ts +1 -0
- package/dist/api.js +179 -39
- package/dist/app.js +1035 -853
- package/dist/{catalog-runtime-Y32UEQLB.js → catalog-runtime-CWZY6GTH.js} +5 -5
- package/dist/chat/agent/request.d.ts +1 -0
- package/dist/chat/agent/resume.d.ts +1 -0
- package/dist/chat/agent/sandbox.d.ts +1 -0
- package/dist/chat/agent/tools.d.ts +1 -0
- package/dist/chat/agent-dispatch/context.d.ts +2 -0
- package/dist/chat/agent-dispatch/heartbeat.d.ts +11 -5
- package/dist/chat/agent-dispatch/signing.d.ts +14 -5
- package/dist/chat/agent-dispatch/store.d.ts +36 -15
- package/dist/chat/agent-dispatch/types.d.ts +23 -10
- package/dist/chat/agent-dispatch/work.d.ts +58 -0
- package/dist/chat/app/factory.d.ts +12 -3
- package/dist/chat/conversations/history.d.ts +285 -290
- package/dist/chat/conversations/projection.d.ts +2 -2
- package/dist/chat/mcp/tool-manager.d.ts +2 -1
- package/dist/chat/pi/conversation-events.d.ts +6 -2
- package/dist/chat/plugins/annotations.d.ts +8 -0
- package/dist/chat/plugins/registry.d.ts +0 -3
- package/dist/chat/plugins/types.d.ts +0 -2
- package/dist/chat/runtime/agent-continue-runner.d.ts +4 -0
- package/dist/chat/runtime/reply-executor.d.ts +6 -1
- package/dist/chat/runtime/slack-resume.d.ts +5 -1
- package/dist/chat/runtime/turn-input.d.ts +3 -0
- package/dist/chat/sandbox/sandbox.d.ts +2 -1
- package/dist/chat/sandbox/session.d.ts +2 -11
- package/dist/chat/sandbox/workspace.d.ts +10 -2
- package/dist/chat/services/turn-session-record.d.ts +14 -1
- package/dist/chat/slack/dispatch-turn.d.ts +25 -0
- package/dist/chat/state/turn-session.d.ts +11 -0
- package/dist/chat/tool-support/text-range-result.d.ts +3 -0
- package/dist/chat/tool-support/zod-tool.d.ts +1 -0
- package/dist/chat/tools/definition.d.ts +3 -10
- package/dist/chat/tools/query-conversation-events.d.ts +68 -0
- package/dist/chat/tools/sandbox/file-utils.d.ts +8 -1
- package/dist/chat/tools/sandbox/find-files.d.ts +2 -1
- package/dist/chat/tools/sandbox/grep.d.ts +2 -1
- package/dist/{chunk-6FKLPVF3.js → chunk-25GU5B5E.js} +1 -1
- package/dist/{chunk-PVZHG2XZ.js → chunk-3H45VOBZ.js} +32 -7
- package/dist/{chunk-NVUQSFWX.js → chunk-3JOGUT46.js} +3 -15
- package/dist/{chunk-G5SX4UPR.js → chunk-4DD6AOCG.js} +2 -2
- package/dist/{chunk-67CVNDJ7.js → chunk-53ZKTQO6.js} +1 -1
- package/dist/{chunk-43RYMWJ2.js → chunk-5UNB623T.js} +809 -213
- package/dist/{chunk-2QG4MZQ7.js → chunk-6RQ5GCWH.js} +2 -2
- package/dist/{chunk-MIAKQNYX.js → chunk-6ZK6FZIV.js} +1 -1
- package/dist/{chunk-6V762AB4.js → chunk-BXLS3I4W.js} +1 -1
- package/dist/{chunk-CWEZEPD4.js → chunk-CI3VK7X5.js} +68 -2
- package/dist/{chunk-EW5MLKT5.js → chunk-JZPQ5K4V.js} +1 -55
- package/dist/{chunk-HWCQRGLL.js → chunk-KBNDFU6J.js} +105 -37
- package/dist/{chunk-FHC4JUSF.js → chunk-MGH5ADQS.js} +1 -2
- package/dist/{chunk-TVE25LPW.js → chunk-N7TMYLF2.js} +1 -1
- package/dist/{chunk-CEYOL62C.js → chunk-OF5FODBL.js} +120 -20
- package/dist/{chunk-6OPTF4BQ.js → chunk-PE44LDDQ.js} +0 -1
- package/dist/{chunk-LYA2AB5G.js → chunk-PGKWXHYJ.js} +8 -52
- package/dist/{chunk-SYJT7AKB.js → chunk-PZGPCOSD.js} +1 -1
- package/dist/{chunk-JQFJO5WH.js → chunk-RXRQJF7Y.js} +74 -17
- package/dist/{chunk-G36V2YGB.js → chunk-TC4ZEM2D.js} +33 -1
- package/dist/{chunk-AGLQYELI.js → chunk-UEVLIYK5.js} +170 -170
- package/dist/{chunk-YCLO7D7M.js → chunk-VKZ6WVAY.js} +77 -26
- package/dist/{chunk-RAVKCKXA.js → chunk-VZCSLYCE.js} +20 -5
- package/dist/{chunk-SG5WAA7H.js → chunk-WKU7TY22.js} +0 -3
- package/dist/cli/chat.js +23 -23
- package/dist/cli/check.js +6 -6
- package/dist/cli/plugins.js +13 -13
- package/dist/cli/snapshot-warmup.js +8 -8
- package/dist/cli/upgrade.js +7 -7
- package/dist/db/schema/api-tokens.d.ts +160 -0
- package/dist/db/schema/conversation-annotations.d.ts +142 -0
- package/dist/db/schema.d.ts +305 -1
- package/dist/{db-KBSJHAZU.js → db-57FHVGGQ.js} +6 -6
- package/dist/handlers/agent-dispatch.d.ts +7 -3
- package/dist/instrumentation.js +2 -2
- package/dist/nitro.js +5 -5
- package/dist/{oauth-callback-server-EYRMJTCS.js → oauth-callback-server-PAQRPXZX.js} +18 -18
- package/dist/personal-tokens/store.d.ts +27 -0
- package/dist/{plugins-PZMDS7AT.js → plugins-IUJA4VBU.js} +1 -1
- package/dist/reporting-schema.d.ts +0 -2
- package/dist/{runner-RQGZ5Z2S.js → runner-4ISZYJEX.js} +12 -12
- package/dist/{skills-JJQIE3BT.js → skills-UE2KXQMB.js} +6 -6
- package/dist/{validation-MZ47PZP5.js → validation-PO5LHTJT.js} +6 -6
- package/dist/version.js +1 -1
- package/migrations/0008_sticky_rage.sql +14 -0
- package/migrations/0009_slippery_harrier.sql +12 -0
- package/migrations/0010_native_conversation_history.sql +142 -0
- package/migrations/meta/0008_snapshot.json +1076 -0
- package/migrations/meta/0009_snapshot.json +1146 -0
- package/migrations/meta/0010_snapshot.json +1190 -0
- package/migrations/meta/_journal.json +22 -1
- package/package.json +6 -7
- package/dist/chat/agent-dispatch/runner.d.ts +0 -13
- package/dist/chat/capabilities/catalog.d.ts +0 -16
- package/dist/{chunk-KNFROR7R.js → chunk-5I6ORJUY.js} +1 -1
package/dist/app.js
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
startConversationWork,
|
|
49
49
|
startSlackProcessingReaction,
|
|
50
50
|
stripLeadingBotMention
|
|
51
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-VKZ6WVAY.js";
|
|
52
52
|
import "./chunk-FA6POGLH.js";
|
|
53
53
|
import {
|
|
54
54
|
ConversationTurnLifecycleService,
|
|
@@ -63,15 +63,15 @@ import {
|
|
|
63
63
|
processPluginTask,
|
|
64
64
|
requireTurnFailureEventId,
|
|
65
65
|
scheduleSessionCompletedPluginTasks
|
|
66
|
-
} from "./chunk-
|
|
66
|
+
} from "./chunk-UEVLIYK5.js";
|
|
67
67
|
import {
|
|
68
68
|
GET,
|
|
69
69
|
conversationVisibilityFromSlackChannelType,
|
|
70
70
|
resolveSlackChannelTypeFromMessage,
|
|
71
71
|
resolveSlackConversationContext
|
|
72
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-4DD6AOCG.js";
|
|
73
73
|
import "./chunk-4CODA7P5.js";
|
|
74
|
-
import "./chunk-
|
|
74
|
+
import "./chunk-PE44LDDQ.js";
|
|
75
75
|
import {
|
|
76
76
|
ConversationQueueMessageRejectedError,
|
|
77
77
|
conversationQueueMessageSchema,
|
|
@@ -79,22 +79,22 @@ import {
|
|
|
79
79
|
isConversationQueueMessageRejectedError,
|
|
80
80
|
resolveConversationWorkQueueTopic,
|
|
81
81
|
verifyConversationQueueMessage
|
|
82
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-6ZK6FZIV.js";
|
|
83
83
|
import {
|
|
84
84
|
PLUGIN_TASK_QUEUE_TOPIC,
|
|
85
85
|
createVercelQueueClient,
|
|
86
86
|
verifyPluginTaskQueueMessage
|
|
87
|
-
} from "./chunk-
|
|
87
|
+
} from "./chunk-5I6ORJUY.js";
|
|
88
88
|
import {
|
|
89
89
|
validatePluginEgressCredentialHooks,
|
|
90
90
|
validatePluginRegistrations
|
|
91
|
-
} from "./chunk-
|
|
91
|
+
} from "./chunk-BXLS3I4W.js";
|
|
92
92
|
import {
|
|
93
93
|
defineJuniorPlugins,
|
|
94
94
|
pluginCatalogConfigFromEnv,
|
|
95
95
|
pluginCatalogConfigFromPluginSet,
|
|
96
96
|
pluginRuntimeRegistrationsFromPluginSet
|
|
97
|
-
} from "./chunk-
|
|
97
|
+
} from "./chunk-WKU7TY22.js";
|
|
98
98
|
import {
|
|
99
99
|
AuthorizationFlowDisabledError,
|
|
100
100
|
PluginCredentialFailureError,
|
|
@@ -121,7 +121,7 @@ import {
|
|
|
121
121
|
setConfigDefaults,
|
|
122
122
|
shouldEmitDevAgentTrace,
|
|
123
123
|
shouldPropagateSandboxEgressTrace
|
|
124
|
-
} from "./chunk-
|
|
124
|
+
} from "./chunk-5UNB623T.js";
|
|
125
125
|
import "./chunk-DIB4GX3G.js";
|
|
126
126
|
import {
|
|
127
127
|
RetryableDeliveryError,
|
|
@@ -132,10 +132,9 @@ import {
|
|
|
132
132
|
deleteMcpStoredOAuthCredentials,
|
|
133
133
|
formatProviderLabel,
|
|
134
134
|
postSlackMessage,
|
|
135
|
-
resolveBaseUrl,
|
|
136
135
|
splitSlackReplyText,
|
|
137
136
|
startOAuthFlow
|
|
138
|
-
} from "./chunk-
|
|
137
|
+
} from "./chunk-PGKWXHYJ.js";
|
|
139
138
|
import {
|
|
140
139
|
ConversationTurnBoundaryError,
|
|
141
140
|
CooperativeTurnYieldError,
|
|
@@ -187,12 +186,11 @@ import {
|
|
|
187
186
|
startActiveTurn,
|
|
188
187
|
stripRuntimeTurnContext,
|
|
189
188
|
trimTrailingAssistantMessages,
|
|
190
|
-
turnHasReply,
|
|
191
189
|
updateConversationStats,
|
|
192
190
|
upsertConversationMessage
|
|
193
|
-
} from "./chunk-
|
|
191
|
+
} from "./chunk-OF5FODBL.js";
|
|
194
192
|
import "./chunk-HDSDCEPK.js";
|
|
195
|
-
import "./chunk-
|
|
193
|
+
import "./chunk-3H45VOBZ.js";
|
|
196
194
|
import "./chunk-MU6HHZEN.js";
|
|
197
195
|
import "./chunk-PDO5BLNM.js";
|
|
198
196
|
import "./chunk-UIE3R5XU.js";
|
|
@@ -209,11 +207,11 @@ import {
|
|
|
209
207
|
validatePlugins,
|
|
210
208
|
verifyScheduledTaskCredentialSubject,
|
|
211
209
|
verifySlackDirectCredentialSubject
|
|
212
|
-
} from "./chunk-
|
|
210
|
+
} from "./chunk-VZCSLYCE.js";
|
|
213
211
|
import {
|
|
214
212
|
acquireActiveLock,
|
|
215
213
|
getStateAdapter
|
|
216
|
-
} from "./chunk-
|
|
214
|
+
} from "./chunk-53ZKTQO6.js";
|
|
217
215
|
import {
|
|
218
216
|
SlackActionError,
|
|
219
217
|
downloadPrivateSlackFile,
|
|
@@ -223,11 +221,10 @@ import {
|
|
|
223
221
|
normalizeSlackConversationId,
|
|
224
222
|
runWithSlackInstallationToken,
|
|
225
223
|
runWithWorkspaceTeamId
|
|
226
|
-
} from "./chunk-
|
|
224
|
+
} from "./chunk-CI3VK7X5.js";
|
|
227
225
|
import {
|
|
228
226
|
contextProvenance,
|
|
229
227
|
createSlackDestination,
|
|
230
|
-
destinationKey,
|
|
231
228
|
getConversationEventStore,
|
|
232
229
|
getConversationStore,
|
|
233
230
|
getDb,
|
|
@@ -235,11 +232,11 @@ import {
|
|
|
235
232
|
instructionProvenanceFor,
|
|
236
233
|
requireSlackDestination,
|
|
237
234
|
withConversationEventLock
|
|
238
|
-
} from "./chunk-
|
|
235
|
+
} from "./chunk-KBNDFU6J.js";
|
|
239
236
|
import {
|
|
240
237
|
juniorConversationEvents,
|
|
241
238
|
juniorConversations
|
|
242
|
-
} from "./chunk-
|
|
239
|
+
} from "./chunk-RXRQJF7Y.js";
|
|
243
240
|
import {
|
|
244
241
|
sleep
|
|
245
242
|
} from "./chunk-4ZNGQH7C.js";
|
|
@@ -260,18 +257,18 @@ import {
|
|
|
260
257
|
modelIdForProfile,
|
|
261
258
|
setSlackReactionConfig,
|
|
262
259
|
standardModelId
|
|
263
|
-
} from "./chunk-
|
|
260
|
+
} from "./chunk-PZGPCOSD.js";
|
|
264
261
|
import {
|
|
265
262
|
juniorDestinations
|
|
266
263
|
} from "./chunk-VFUK3X5B.js";
|
|
267
264
|
import {
|
|
268
265
|
parseSlackThreadId
|
|
269
266
|
} from "./chunk-FXOYURO4.js";
|
|
270
|
-
import "./chunk-
|
|
267
|
+
import "./chunk-6RQ5GCWH.js";
|
|
271
268
|
import {
|
|
272
269
|
pluginCatalogRuntime
|
|
273
|
-
} from "./chunk-
|
|
274
|
-
import "./chunk-
|
|
270
|
+
} from "./chunk-25GU5B5E.js";
|
|
271
|
+
import "./chunk-JZPQ5K4V.js";
|
|
275
272
|
import {
|
|
276
273
|
credentialSubjectSchema
|
|
277
274
|
} from "./chunk-W3U3YQYI.js";
|
|
@@ -297,7 +294,7 @@ import {
|
|
|
297
294
|
setTags,
|
|
298
295
|
withContext,
|
|
299
296
|
withSpan
|
|
300
|
-
} from "./chunk-
|
|
297
|
+
} from "./chunk-3JOGUT46.js";
|
|
301
298
|
import {
|
|
302
299
|
toOptionalString
|
|
303
300
|
} from "./chunk-OB42YVAE.js";
|
|
@@ -312,10 +309,10 @@ import "./chunk-B3IT5END.js";
|
|
|
312
309
|
import "./chunk-3V7OFJZC.js";
|
|
313
310
|
import {
|
|
314
311
|
JUNIOR_PLUGIN_TASK_CALLBACK_ROUTE
|
|
315
|
-
} from "./chunk-
|
|
312
|
+
} from "./chunk-N7TMYLF2.js";
|
|
316
313
|
import {
|
|
317
314
|
JUNIOR_VERSION
|
|
318
|
-
} from "./chunk-
|
|
315
|
+
} from "./chunk-MGH5ADQS.js";
|
|
319
316
|
import "./chunk-MLKGABMK.js";
|
|
320
317
|
|
|
321
318
|
// src/app.ts
|
|
@@ -325,13 +322,16 @@ import { Hono } from "hono";
|
|
|
325
322
|
|
|
326
323
|
// src/chat/agent-dispatch/signing.ts
|
|
327
324
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
328
|
-
|
|
325
|
+
import { z } from "zod";
|
|
329
326
|
var DISPATCH_HMAC_CONTEXT = "junior.agent_dispatch.v1";
|
|
330
327
|
var DISPATCH_SIGNATURE_VERSION = "v1";
|
|
331
328
|
var DISPATCH_MAX_SKEW_MS = 5 * 60 * 1e3;
|
|
332
|
-
var DISPATCH_CALLBACK_TIMEOUT_MS = 1e4;
|
|
333
329
|
var DISPATCH_TIMESTAMP_HEADER = "x-junior-dispatch-timestamp";
|
|
334
330
|
var DISPATCH_SIGNATURE_HEADER = "x-junior-dispatch-signature";
|
|
331
|
+
var legacyDispatchCallbackSchema = z.object({
|
|
332
|
+
expectedVersion: z.number().int().positive(),
|
|
333
|
+
id: z.string().min(1)
|
|
334
|
+
}).strict();
|
|
335
335
|
function getDispatchSecret() {
|
|
336
336
|
return process.env.JUNIOR_SECRET?.trim() || void 0;
|
|
337
337
|
}
|
|
@@ -350,50 +350,6 @@ function timingSafeMatch(expected, actual) {
|
|
|
350
350
|
}
|
|
351
351
|
return timingSafeEqual(expectedBuffer, actualBuffer);
|
|
352
352
|
}
|
|
353
|
-
function parseDispatchCallback(value) {
|
|
354
|
-
if (!value || typeof value !== "object") {
|
|
355
|
-
return void 0;
|
|
356
|
-
}
|
|
357
|
-
const record = value;
|
|
358
|
-
if (typeof record.id !== "string" || typeof record.expectedVersion !== "number") {
|
|
359
|
-
return void 0;
|
|
360
|
-
}
|
|
361
|
-
return {
|
|
362
|
-
id: record.id,
|
|
363
|
-
expectedVersion: record.expectedVersion
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
async function scheduleDispatchCallback(callback) {
|
|
367
|
-
const baseUrl = resolveBaseUrl();
|
|
368
|
-
if (!baseUrl) {
|
|
369
|
-
throw new Error(
|
|
370
|
-
"Cannot determine base URL for agent dispatch callback (set JUNIOR_BASE_URL or deploy to Vercel)"
|
|
371
|
-
);
|
|
372
|
-
}
|
|
373
|
-
const secret = getDispatchSecret();
|
|
374
|
-
if (!secret) {
|
|
375
|
-
throw new Error(
|
|
376
|
-
"Cannot determine agent dispatch secret (set JUNIOR_SECRET)"
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
const body = JSON.stringify(callback);
|
|
380
|
-
const timestamp = Date.now().toString();
|
|
381
|
-
const response = await fetch(`${baseUrl}${DISPATCH_CALLBACK_PATH}`, {
|
|
382
|
-
method: "POST",
|
|
383
|
-
headers: {
|
|
384
|
-
"content-type": "application/json",
|
|
385
|
-
[DISPATCH_TIMESTAMP_HEADER]: timestamp,
|
|
386
|
-
[DISPATCH_SIGNATURE_HEADER]: signBody(secret, timestamp, body)
|
|
387
|
-
},
|
|
388
|
-
signal: AbortSignal.timeout(DISPATCH_CALLBACK_TIMEOUT_MS),
|
|
389
|
-
body
|
|
390
|
-
});
|
|
391
|
-
if (!response.ok) {
|
|
392
|
-
throw new Error(
|
|
393
|
-
`Agent dispatch callback failed with status ${response.status}`
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
353
|
async function verifyDispatchCallbackRequest(request) {
|
|
398
354
|
const timestamp = request.headers.get(DISPATCH_TIMESTAMP_HEADER)?.trim() ?? "";
|
|
399
355
|
const signature = request.headers.get(DISPATCH_SIGNATURE_HEADER)?.trim() ?? "";
|
|
@@ -411,7 +367,8 @@ async function verifyDispatchCallbackRequest(request) {
|
|
|
411
367
|
return void 0;
|
|
412
368
|
}
|
|
413
369
|
try {
|
|
414
|
-
|
|
370
|
+
const parsed = legacyDispatchCallbackSchema.safeParse(JSON.parse(body));
|
|
371
|
+
return parsed.success ? parsed.data : void 0;
|
|
415
372
|
} catch {
|
|
416
373
|
return void 0;
|
|
417
374
|
}
|
|
@@ -425,17 +382,18 @@ import {
|
|
|
425
382
|
isSlackDestination,
|
|
426
383
|
sourceSchema
|
|
427
384
|
} from "@sentry/junior-plugin-api";
|
|
428
|
-
import { z } from "zod";
|
|
385
|
+
import { z as z2 } from "zod";
|
|
429
386
|
var DISPATCH_PREFIX = "junior:agent_dispatch";
|
|
430
387
|
var DISPATCH_LOCK_TTL_MS = 10 * 60 * 1e3;
|
|
431
|
-
var
|
|
432
|
-
var
|
|
433
|
-
var
|
|
434
|
-
var nonEmptyExactStringSchema = z.string().min(1).refine(
|
|
388
|
+
var DISPATCH_MAILBOX_APPEND_INDEX_LOCK_TTL_MS = 1e4;
|
|
389
|
+
var DISPATCH_MAILBOX_APPEND_INDEX_MAX_LENGTH = 1e4;
|
|
390
|
+
var nonEmptyExactStringSchema = z2.string().min(1).refine(
|
|
435
391
|
(value) => value === value.trim() && value.toLowerCase() !== "unknown"
|
|
436
392
|
);
|
|
437
|
-
var
|
|
438
|
-
|
|
393
|
+
var pendingDispatchMailboxAppendIndexSchema = z2.array(
|
|
394
|
+
nonEmptyExactStringSchema
|
|
395
|
+
);
|
|
396
|
+
var dispatchStatusSchema = z2.enum([
|
|
439
397
|
"pending",
|
|
440
398
|
"running",
|
|
441
399
|
"awaiting_resume",
|
|
@@ -443,35 +401,30 @@ var dispatchStatusSchema = z.enum([
|
|
|
443
401
|
"failed",
|
|
444
402
|
"blocked"
|
|
445
403
|
]);
|
|
446
|
-
var dispatchActorSchema =
|
|
447
|
-
platform:
|
|
404
|
+
var dispatchActorSchema = z2.object({
|
|
405
|
+
platform: z2.literal("system"),
|
|
448
406
|
name: nonEmptyExactStringSchema
|
|
449
407
|
}).strict();
|
|
450
|
-
var dispatchRecordSchema =
|
|
408
|
+
var dispatchRecordSchema = z2.object({
|
|
451
409
|
actor: dispatchActorSchema,
|
|
452
|
-
|
|
453
|
-
createdAtMs: z.number().finite(),
|
|
410
|
+
createdAtMs: z2.number().finite(),
|
|
454
411
|
credentialSubject: credentialSubjectSchema.optional(),
|
|
455
412
|
destination: destinationSchema,
|
|
456
413
|
destinationVisibility: destinationVisibilitySchema,
|
|
457
|
-
errorMessage:
|
|
414
|
+
errorMessage: z2.string().optional(),
|
|
458
415
|
id: nonEmptyExactStringSchema,
|
|
459
|
-
idempotencyKey:
|
|
460
|
-
input:
|
|
461
|
-
|
|
462
|
-
leaseExpiresAtMs: z.number().finite().optional(),
|
|
463
|
-
maxAttempts: z.number().int().positive(),
|
|
464
|
-
metadata: z.record(z.string(), z.string()).optional(),
|
|
416
|
+
idempotencyKey: z2.string().min(1),
|
|
417
|
+
input: z2.string().min(1),
|
|
418
|
+
metadata: z2.record(z2.string(), z2.string()).optional(),
|
|
465
419
|
plugin: nonEmptyExactStringSchema,
|
|
466
|
-
resultMessageTs:
|
|
420
|
+
resultMessageTs: z2.string().optional(),
|
|
467
421
|
source: sourceSchema,
|
|
468
422
|
status: dispatchStatusSchema,
|
|
469
|
-
updatedAtMs:
|
|
470
|
-
version: z.number().int().positive()
|
|
423
|
+
updatedAtMs: z2.number().finite()
|
|
471
424
|
}).strict().superRefine((record, ctx) => {
|
|
472
425
|
if (!isSlackDestination(record.destination)) {
|
|
473
426
|
ctx.addIssue({
|
|
474
|
-
code:
|
|
427
|
+
code: z2.ZodIssueCode.custom,
|
|
475
428
|
message: "Dispatch destination platform must be slack",
|
|
476
429
|
path: ["destination"]
|
|
477
430
|
});
|
|
@@ -484,7 +437,7 @@ var dispatchRecordSchema = z.object({
|
|
|
484
437
|
if (subject.allowedWhen === "private-direct-conversation") {
|
|
485
438
|
if (!record.destination.channelId.startsWith("D")) {
|
|
486
439
|
ctx.addIssue({
|
|
487
|
-
code:
|
|
440
|
+
code: z2.ZodIssueCode.custom,
|
|
488
441
|
message: "Dispatch credentialSubject requires a private direct Slack destination",
|
|
489
442
|
path: ["credentialSubject"]
|
|
490
443
|
});
|
|
@@ -492,7 +445,7 @@ var dispatchRecordSchema = z.object({
|
|
|
492
445
|
}
|
|
493
446
|
if (subject.binding.type !== "slack-direct-conversation" || subject.binding.teamId !== record.destination.teamId || subject.binding.channelId !== record.destination.channelId) {
|
|
494
447
|
ctx.addIssue({
|
|
495
|
-
code:
|
|
448
|
+
code: z2.ZodIssueCode.custom,
|
|
496
449
|
message: "Dispatch credentialSubject binding must match destination",
|
|
497
450
|
path: ["credentialSubject", "binding"]
|
|
498
451
|
});
|
|
@@ -501,27 +454,31 @@ var dispatchRecordSchema = z.object({
|
|
|
501
454
|
}
|
|
502
455
|
if (subject.binding.type !== "scheduled-task" || subject.binding.plugin !== record.plugin || subject.binding.taskId !== subject.taskId) {
|
|
503
456
|
ctx.addIssue({
|
|
504
|
-
code:
|
|
457
|
+
code: z2.ZodIssueCode.custom,
|
|
505
458
|
message: "Dispatch credentialSubject binding must match task",
|
|
506
459
|
path: ["credentialSubject", "binding"]
|
|
507
460
|
});
|
|
508
461
|
}
|
|
509
462
|
});
|
|
463
|
+
var storedDispatchRecordSchema = dispatchRecordSchema.extend({
|
|
464
|
+
attempt: z2.number().int().nonnegative().optional(),
|
|
465
|
+
lastCallbackAtMs: z2.number().finite().optional(),
|
|
466
|
+
leaseExpiresAtMs: z2.number().finite().optional(),
|
|
467
|
+
maxAttempts: z2.number().int().positive().optional(),
|
|
468
|
+
version: z2.number().int().positive().optional()
|
|
469
|
+
});
|
|
510
470
|
function getDispatchStorageKey(id) {
|
|
511
471
|
return `${DISPATCH_PREFIX}:record:${id}`;
|
|
512
472
|
}
|
|
513
|
-
function
|
|
473
|
+
function dispatchLockKey(id) {
|
|
474
|
+
return `${DISPATCH_PREFIX}:lock:${id}`;
|
|
475
|
+
}
|
|
476
|
+
function pendingDispatchMailboxAppendIndexKey() {
|
|
514
477
|
return `${DISPATCH_PREFIX}:incomplete`;
|
|
515
478
|
}
|
|
516
|
-
function
|
|
479
|
+
function pendingDispatchMailboxAppendIndexLockKey() {
|
|
517
480
|
return `${DISPATCH_PREFIX}:incomplete:lock`;
|
|
518
481
|
}
|
|
519
|
-
function parseIncompleteDispatchIndex(value) {
|
|
520
|
-
return value === void 0 || value === null ? [] : incompleteDispatchIndexSchema.parse(value);
|
|
521
|
-
}
|
|
522
|
-
function dispatchLockKey(id) {
|
|
523
|
-
return `${DISPATCH_PREFIX}:lock:${id}`;
|
|
524
|
-
}
|
|
525
482
|
function normalizeMetadata(metadata) {
|
|
526
483
|
if (!metadata) {
|
|
527
484
|
return void 0;
|
|
@@ -536,15 +493,42 @@ function buildDispatchId(plugin, idempotencyKey) {
|
|
|
536
493
|
return `dispatch_${digest}`;
|
|
537
494
|
}
|
|
538
495
|
function parseDispatchRecord(value) {
|
|
539
|
-
const parsed =
|
|
540
|
-
return parsed
|
|
496
|
+
const parsed = parseStoredDispatchRecord(value);
|
|
497
|
+
return parsed?.record;
|
|
541
498
|
}
|
|
542
|
-
function
|
|
543
|
-
|
|
499
|
+
function parseStoredDispatchRecord(value) {
|
|
500
|
+
const parsed = storedDispatchRecordSchema.safeParse(value);
|
|
501
|
+
if (!parsed.success) {
|
|
502
|
+
return void 0;
|
|
503
|
+
}
|
|
504
|
+
const {
|
|
505
|
+
attempt: _attempt,
|
|
506
|
+
lastCallbackAtMs: _lastCallbackAtMs,
|
|
507
|
+
leaseExpiresAtMs: _leaseExpiresAtMs,
|
|
508
|
+
maxAttempts: _maxAttempts,
|
|
509
|
+
version: _version,
|
|
510
|
+
...record
|
|
511
|
+
} = parsed.data;
|
|
512
|
+
return {
|
|
513
|
+
...typeof parsed.data.leaseExpiresAtMs === "number" ? { legacyLeaseExpiresAtMs: parsed.data.leaseExpiresAtMs } : {},
|
|
514
|
+
record
|
|
515
|
+
};
|
|
544
516
|
}
|
|
545
517
|
function getDispatchConversationId(dispatch) {
|
|
546
518
|
return `agent-dispatch:${dispatch.id}`;
|
|
547
519
|
}
|
|
520
|
+
function getDispatchInputMessageId(dispatchId) {
|
|
521
|
+
return `agent-dispatch:${dispatchId}`;
|
|
522
|
+
}
|
|
523
|
+
function getLegacyDispatchInputMessageId(dispatchId) {
|
|
524
|
+
return `dispatch:${dispatchId}:user`;
|
|
525
|
+
}
|
|
526
|
+
function getDispatchInputMessageIds(dispatchId) {
|
|
527
|
+
return [
|
|
528
|
+
getDispatchInputMessageId(dispatchId),
|
|
529
|
+
getLegacyDispatchInputMessageId(dispatchId)
|
|
530
|
+
];
|
|
531
|
+
}
|
|
548
532
|
function getDispatchTurnId(dispatchId) {
|
|
549
533
|
return `dispatch:${dispatchId}`;
|
|
550
534
|
}
|
|
@@ -559,7 +543,7 @@ function toDispatchProjection(record) {
|
|
|
559
543
|
function isTerminalDispatchStatus(status) {
|
|
560
544
|
return status === "completed" || status === "failed" || status === "blocked";
|
|
561
545
|
}
|
|
562
|
-
async function withDispatchLock(dispatchId,
|
|
546
|
+
async function withDispatchLock(dispatchId, task) {
|
|
563
547
|
const state = getStateAdapter();
|
|
564
548
|
await state.connect();
|
|
565
549
|
const lock = await state.acquireLock(
|
|
@@ -570,45 +554,11 @@ async function withDispatchLock(dispatchId, callback) {
|
|
|
570
554
|
throw new Error(`Could not acquire dispatch lock for ${dispatchId}`);
|
|
571
555
|
}
|
|
572
556
|
try {
|
|
573
|
-
return await
|
|
557
|
+
return await task(state);
|
|
574
558
|
} finally {
|
|
575
559
|
await state.releaseLock(lock);
|
|
576
560
|
}
|
|
577
561
|
}
|
|
578
|
-
async function withIncompleteDispatchIndexLock(state, callback) {
|
|
579
|
-
const lock = await state.acquireLock(
|
|
580
|
-
incompleteDispatchIndexLockKey(),
|
|
581
|
-
DISPATCH_INDEX_LOCK_TTL_MS
|
|
582
|
-
);
|
|
583
|
-
if (!lock) {
|
|
584
|
-
throw new Error("Could not acquire incomplete dispatch index lock");
|
|
585
|
-
}
|
|
586
|
-
try {
|
|
587
|
-
return await callback();
|
|
588
|
-
} finally {
|
|
589
|
-
await state.releaseLock(lock);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
async function syncIncompleteDispatchIndex(state, record) {
|
|
593
|
-
await withIncompleteDispatchIndexLock(state, async () => {
|
|
594
|
-
const ids = [
|
|
595
|
-
...new Set(
|
|
596
|
-
parseIncompleteDispatchIndex(
|
|
597
|
-
await state.get(incompleteDispatchIndexKey())
|
|
598
|
-
)
|
|
599
|
-
)
|
|
600
|
-
];
|
|
601
|
-
const next = isTerminalDispatchStatus(record.status) ? ids.filter((id) => id !== record.id) : ids.includes(record.id) ? ids : [...ids, record.id];
|
|
602
|
-
if (next.length === ids.length && next.every((id, index) => id === ids[index])) {
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
await state.set(
|
|
606
|
-
incompleteDispatchIndexKey(),
|
|
607
|
-
next.slice(-DISPATCH_INDEX_MAX_LENGTH),
|
|
608
|
-
JUNIOR_THREAD_STATE_TTL_MS
|
|
609
|
-
);
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
562
|
async function putRecord(state, record) {
|
|
613
563
|
const parsed = dispatchRecordSchema.safeParse(record);
|
|
614
564
|
if (!parsed.success) {
|
|
@@ -622,7 +572,32 @@ async function putRecord(state, record) {
|
|
|
622
572
|
next,
|
|
623
573
|
JUNIOR_THREAD_STATE_TTL_MS
|
|
624
574
|
);
|
|
625
|
-
|
|
575
|
+
}
|
|
576
|
+
async function updatePendingDispatchMailboxAppendIndex(state, update) {
|
|
577
|
+
const lock = await state.acquireLock(
|
|
578
|
+
pendingDispatchMailboxAppendIndexLockKey(),
|
|
579
|
+
DISPATCH_MAILBOX_APPEND_INDEX_LOCK_TTL_MS
|
|
580
|
+
);
|
|
581
|
+
if (!lock) {
|
|
582
|
+
throw new Error("Could not acquire dispatch mailbox append index lock");
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
const stored = await state.get(pendingDispatchMailboxAppendIndexKey());
|
|
586
|
+
const ids = stored === void 0 || stored === null ? [] : pendingDispatchMailboxAppendIndexSchema.parse(stored);
|
|
587
|
+
const next = [...new Set(update([...new Set(ids)]))].slice(
|
|
588
|
+
-DISPATCH_MAILBOX_APPEND_INDEX_MAX_LENGTH
|
|
589
|
+
);
|
|
590
|
+
if (next.length === ids.length && next.every((id, index) => id === ids[index])) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
await state.set(
|
|
594
|
+
pendingDispatchMailboxAppendIndexKey(),
|
|
595
|
+
next,
|
|
596
|
+
JUNIOR_THREAD_STATE_TTL_MS
|
|
597
|
+
);
|
|
598
|
+
} finally {
|
|
599
|
+
await state.releaseLock(lock);
|
|
600
|
+
}
|
|
626
601
|
}
|
|
627
602
|
async function getDispatchRecord(id) {
|
|
628
603
|
const state = getStateAdapter();
|
|
@@ -641,7 +616,6 @@ async function createOrGetDispatch(args) {
|
|
|
641
616
|
const metadata = normalizeMetadata(args.options.metadata);
|
|
642
617
|
const record = {
|
|
643
618
|
actor: { platform: "system", name: args.plugin },
|
|
644
|
-
attempt: 0,
|
|
645
619
|
createdAtMs: args.nowMs,
|
|
646
620
|
...args.options.credentialSubject ? { credentialSubject: args.options.credentialSubject } : {},
|
|
647
621
|
destination: args.options.destination,
|
|
@@ -649,37 +623,113 @@ async function createOrGetDispatch(args) {
|
|
|
649
623
|
id,
|
|
650
624
|
idempotencyKey: args.options.idempotencyKey,
|
|
651
625
|
input: args.options.input,
|
|
652
|
-
maxAttempts: DEFAULT_MAX_ATTEMPTS,
|
|
653
626
|
...metadata ? { metadata } : {},
|
|
654
627
|
plugin: args.plugin,
|
|
655
628
|
status: "pending",
|
|
656
629
|
source: args.options.source,
|
|
657
|
-
updatedAtMs: args.nowMs
|
|
658
|
-
version: 1
|
|
630
|
+
updatedAtMs: args.nowMs
|
|
659
631
|
};
|
|
632
|
+
await updatePendingDispatchMailboxAppendIndex(
|
|
633
|
+
state,
|
|
634
|
+
(ids) => ids.includes(id) ? ids : [...ids, id]
|
|
635
|
+
);
|
|
660
636
|
await putRecord(state, record);
|
|
661
637
|
return { record, status: "created" };
|
|
662
638
|
});
|
|
663
639
|
}
|
|
664
|
-
async function
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
640
|
+
async function transitionDispatch(id, transition) {
|
|
641
|
+
return await withDispatchLock(id, async (state) => {
|
|
642
|
+
const current = parseDispatchRecord(
|
|
643
|
+
await state.get(getDispatchStorageKey(id))
|
|
644
|
+
);
|
|
645
|
+
if (!current) {
|
|
646
|
+
return void 0;
|
|
647
|
+
}
|
|
648
|
+
const transitioned = transition(current);
|
|
649
|
+
if (transitioned === current) {
|
|
650
|
+
return current;
|
|
651
|
+
}
|
|
652
|
+
const next = {
|
|
653
|
+
...transitioned,
|
|
654
|
+
updatedAtMs: Date.now()
|
|
655
|
+
};
|
|
656
|
+
await putRecord(state, next);
|
|
657
|
+
return next;
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
async function markDispatchRunning(id) {
|
|
661
|
+
return await transitionDispatch(
|
|
662
|
+
id,
|
|
663
|
+
(record) => isTerminalDispatchStatus(record.status) ? record : { ...record, status: "running", errorMessage: void 0 }
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
async function markDispatchAwaitingResume(id) {
|
|
667
|
+
return await transitionDispatch(
|
|
668
|
+
id,
|
|
669
|
+
(record) => isTerminalDispatchStatus(record.status) ? record : {
|
|
670
|
+
...record,
|
|
671
|
+
status: "awaiting_resume"
|
|
672
|
+
}
|
|
673
|
+
);
|
|
672
674
|
}
|
|
673
|
-
async function
|
|
675
|
+
async function markDispatchBlocked(id, errorMessage, resultMessageTs) {
|
|
676
|
+
return await transitionDispatch(
|
|
677
|
+
id,
|
|
678
|
+
(record) => isTerminalDispatchStatus(record.status) ? record : {
|
|
679
|
+
...record,
|
|
680
|
+
errorMessage,
|
|
681
|
+
...resultMessageTs ? { resultMessageTs } : {},
|
|
682
|
+
status: "blocked"
|
|
683
|
+
}
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
async function markDispatchCompleted(id, resultMessageTs) {
|
|
687
|
+
return await transitionDispatch(
|
|
688
|
+
id,
|
|
689
|
+
(record) => isTerminalDispatchStatus(record.status) ? record : {
|
|
690
|
+
...record,
|
|
691
|
+
errorMessage: void 0,
|
|
692
|
+
...resultMessageTs ? { resultMessageTs } : {},
|
|
693
|
+
status: "completed"
|
|
694
|
+
}
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
async function markDispatchFailed(id, errorMessage, resultMessageTs) {
|
|
698
|
+
return await transitionDispatch(
|
|
699
|
+
id,
|
|
700
|
+
(record) => isTerminalDispatchStatus(record.status) ? record : {
|
|
701
|
+
...record,
|
|
702
|
+
errorMessage,
|
|
703
|
+
...resultMessageTs ? { resultMessageTs } : {},
|
|
704
|
+
status: "failed"
|
|
705
|
+
}
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
async function confirmDispatchMailboxAppend(id) {
|
|
674
709
|
const state = getStateAdapter();
|
|
675
710
|
await state.connect();
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
711
|
+
await updatePendingDispatchMailboxAppendIndex(
|
|
712
|
+
state,
|
|
713
|
+
(ids) => ids.filter((candidate) => candidate !== id)
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
async function listPendingDispatchMailboxAppends() {
|
|
717
|
+
const state = getStateAdapter();
|
|
718
|
+
await state.connect();
|
|
719
|
+
const stored = await state.get(pendingDispatchMailboxAppendIndexKey());
|
|
720
|
+
return stored === void 0 || stored === null ? [] : [...new Set(pendingDispatchMailboxAppendIndexSchema.parse(stored))];
|
|
721
|
+
}
|
|
722
|
+
async function claimDispatchMailboxAppend(id, nowMs) {
|
|
723
|
+
return await withDispatchLock(id, async (state) => {
|
|
724
|
+
const stored = parseStoredDispatchRecord(
|
|
725
|
+
await state.get(getDispatchStorageKey(id))
|
|
726
|
+
);
|
|
727
|
+
if (!stored || stored.legacyLeaseExpiresAtMs && stored.legacyLeaseExpiresAtMs > nowMs) {
|
|
728
|
+
return void 0;
|
|
729
|
+
}
|
|
730
|
+
await putRecord(state, stored.record);
|
|
731
|
+
return stored.record;
|
|
732
|
+
});
|
|
683
733
|
}
|
|
684
734
|
async function getPluginDispatchProjection(args) {
|
|
685
735
|
const record = await getDispatchRecord(args.id);
|
|
@@ -689,487 +739,358 @@ async function getPluginDispatchProjection(args) {
|
|
|
689
739
|
return toDispatchProjection(record);
|
|
690
740
|
}
|
|
691
741
|
|
|
692
|
-
// src/chat/agent-dispatch/
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
buildDispatchConversationText(args.dispatch)
|
|
706
|
-
),
|
|
707
|
-
createdAtMs: args.nowMs,
|
|
708
|
-
author: {
|
|
709
|
-
userName: `system:${args.dispatch.actor.name}`,
|
|
710
|
-
isBot: true
|
|
742
|
+
// src/chat/agent-dispatch/work.ts
|
|
743
|
+
import { z as z3 } from "zod";
|
|
744
|
+
var agentDispatchMailboxMetadataSchema = z3.object({
|
|
745
|
+
dispatchId: z3.string().min(1),
|
|
746
|
+
kind: z3.literal("agent_dispatch")
|
|
747
|
+
}).strict();
|
|
748
|
+
var AGENT_DISPATCH_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
749
|
+
function buildDispatchRoutingContext(dispatch) {
|
|
750
|
+
return {
|
|
751
|
+
actor: dispatch.actor,
|
|
752
|
+
credentialContext: {
|
|
753
|
+
actor: dispatch.actor,
|
|
754
|
+
...dispatch.credentialSubject ? { subject: dispatch.credentialSubject } : {}
|
|
711
755
|
},
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
756
|
+
destinationVisibility: dispatch.destinationVisibility,
|
|
757
|
+
dispatch: {
|
|
758
|
+
actor: dispatch.actor,
|
|
759
|
+
id: dispatch.id,
|
|
760
|
+
metadata: dispatch.metadata,
|
|
761
|
+
plugin: dispatch.plugin
|
|
762
|
+
},
|
|
763
|
+
surface: "api"
|
|
764
|
+
};
|
|
716
765
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
766
|
+
function buildAgentDispatchInboundMessage(dispatch, nowMs = Date.now()) {
|
|
767
|
+
return {
|
|
768
|
+
conversationId: getDispatchConversationId(dispatch),
|
|
769
|
+
createdAtMs: dispatch.createdAtMs,
|
|
770
|
+
delivery: "defer",
|
|
771
|
+
destination: dispatch.destination,
|
|
772
|
+
inboundMessageId: getDispatchInputMessageId(dispatch.id),
|
|
773
|
+
input: {
|
|
774
|
+
// The dispatch record is the idempotent request authority. The mailbox
|
|
775
|
+
// only signals which request this conversation lease may advance.
|
|
776
|
+
text: "[plugin dispatch]",
|
|
777
|
+
metadata: {
|
|
778
|
+
dispatchId: dispatch.id,
|
|
779
|
+
kind: "agent_dispatch"
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
receivedAtMs: nowMs,
|
|
783
|
+
source: "plugin"
|
|
784
|
+
};
|
|
723
785
|
}
|
|
724
|
-
async function
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
786
|
+
async function enqueueAgentDispatch(dispatch, options) {
|
|
787
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
788
|
+
const claimedDispatch = await claimDispatchMailboxAppend(dispatch.id, nowMs);
|
|
789
|
+
if (!claimedDispatch) {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
await (options.conversationStore ?? getConversationStore()).recordActivity({
|
|
793
|
+
activityAtMs: claimedDispatch.createdAtMs,
|
|
794
|
+
conversationId: getDispatchConversationId(claimedDispatch),
|
|
795
|
+
destination: claimedDispatch.destination,
|
|
796
|
+
nowMs,
|
|
797
|
+
source: "plugin",
|
|
798
|
+
visibility: claimedDispatch.destinationVisibility
|
|
799
|
+
});
|
|
800
|
+
await appendAndEnqueueInboundMessage({
|
|
801
|
+
message: buildAgentDispatchInboundMessage(claimedDispatch, nowMs),
|
|
802
|
+
conversationStore: options.conversationStore,
|
|
803
|
+
nowMs,
|
|
804
|
+
queue: options.queue,
|
|
805
|
+
state: options.state
|
|
738
806
|
});
|
|
807
|
+
await confirmDispatchMailboxAppend(claimedDispatch.id);
|
|
739
808
|
}
|
|
740
|
-
function
|
|
741
|
-
if (
|
|
742
|
-
return
|
|
809
|
+
function dispatchIdFromMessages(messages) {
|
|
810
|
+
if (messages.length === 0) {
|
|
811
|
+
return void 0;
|
|
743
812
|
}
|
|
744
|
-
|
|
745
|
-
|
|
813
|
+
const parsed = messages.map(
|
|
814
|
+
(message) => agentDispatchMailboxMetadataSchema.safeParse(message.input.metadata)
|
|
815
|
+
);
|
|
816
|
+
if (parsed.every((result) => !result.success)) {
|
|
817
|
+
return void 0;
|
|
746
818
|
}
|
|
747
|
-
if (
|
|
748
|
-
|
|
819
|
+
if (parsed.some((result) => !result.success)) {
|
|
820
|
+
throw new Error("Conversation mailbox mixes dispatch and provider input");
|
|
749
821
|
}
|
|
750
|
-
|
|
822
|
+
const ids = new Set(
|
|
823
|
+
parsed.map((result) => {
|
|
824
|
+
if (!result.success) {
|
|
825
|
+
throw new Error("Dispatch mailbox metadata failed validation");
|
|
826
|
+
}
|
|
827
|
+
return result.data.dispatchId;
|
|
828
|
+
})
|
|
829
|
+
);
|
|
830
|
+
if (ids.size !== 1) {
|
|
831
|
+
throw new Error("Conversation mailbox contains multiple dispatches");
|
|
832
|
+
}
|
|
833
|
+
return ids.values().next().value;
|
|
751
834
|
}
|
|
752
|
-
async function
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
835
|
+
async function resolveAgentDispatchId(context) {
|
|
836
|
+
const mailboxDispatchId = dispatchIdFromMessages(context.attempt.messages);
|
|
837
|
+
if (mailboxDispatchId) {
|
|
838
|
+
return mailboxDispatchId;
|
|
839
|
+
}
|
|
840
|
+
if (context.attempt.messages.length > 0) {
|
|
841
|
+
return void 0;
|
|
842
|
+
}
|
|
843
|
+
const summaries = await listAgentTurnSessionSummariesForConversation(
|
|
844
|
+
context.conversationId
|
|
845
|
+
);
|
|
846
|
+
const activeDispatchIds = new Set(
|
|
847
|
+
summaries.filter(
|
|
848
|
+
(summary) => (summary.state === "awaiting_resume" || summary.state === "running") && Boolean(summary.dispatchId)
|
|
849
|
+
).map((summary) => summary.dispatchId).filter((id) => Boolean(id))
|
|
850
|
+
);
|
|
851
|
+
if (activeDispatchIds.size > 1) {
|
|
852
|
+
throw new Error(
|
|
853
|
+
`Conversation ${context.conversationId} has multiple active dispatches`
|
|
760
854
|
);
|
|
761
|
-
|
|
762
|
-
|
|
855
|
+
}
|
|
856
|
+
const activeDispatchId = activeDispatchIds.values().next().value;
|
|
857
|
+
if (activeDispatchId) {
|
|
858
|
+
return activeDispatchId;
|
|
859
|
+
}
|
|
860
|
+
const durableDispatchIds = new Set(
|
|
861
|
+
summaries.map((summary) => summary.dispatchId).filter((id) => Boolean(id))
|
|
862
|
+
);
|
|
863
|
+
if (durableDispatchIds.size > 1) {
|
|
864
|
+
throw new Error(
|
|
865
|
+
`Conversation ${context.conversationId} has multiple dispatch sessions`
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
const durableDispatchId = durableDispatchIds.values().next().value;
|
|
869
|
+
if (durableDispatchId) {
|
|
870
|
+
const dispatch2 = await getDispatchRecord(durableDispatchId);
|
|
871
|
+
if (dispatch2 && !isTerminalDispatchStatus(dispatch2.status)) {
|
|
872
|
+
return durableDispatchId;
|
|
763
873
|
}
|
|
764
|
-
return await updateDispatchRecord(state, {
|
|
765
|
-
...current,
|
|
766
|
-
lastCallbackAtMs: nowMs,
|
|
767
|
-
leaseExpiresAtMs: nowMs + DISPATCH_SLICE_LEASE_MS,
|
|
768
|
-
status: "running"
|
|
769
|
-
});
|
|
770
|
-
});
|
|
771
|
-
if (!claimedDispatch) {
|
|
772
|
-
return;
|
|
773
874
|
}
|
|
774
|
-
|
|
875
|
+
const prefix = "agent-dispatch:";
|
|
876
|
+
if (!context.conversationId.startsWith(prefix)) {
|
|
877
|
+
return void 0;
|
|
878
|
+
}
|
|
879
|
+
const dispatchId = context.conversationId.slice(prefix.length);
|
|
880
|
+
const dispatch = await getDispatchRecord(dispatchId);
|
|
881
|
+
if (!dispatch) {
|
|
882
|
+
return void 0;
|
|
883
|
+
}
|
|
884
|
+
return isTerminalDispatchStatus(dispatch.status) ? void 0 : dispatch.id;
|
|
885
|
+
}
|
|
886
|
+
function createAgentDispatchWorkRouter(options) {
|
|
887
|
+
return async (context) => {
|
|
888
|
+
const dispatchId = await resolveAgentDispatchId(context);
|
|
889
|
+
return dispatchId ? await options.dispatchWorker(context, dispatchId) : await options.fallbackWorker(context);
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
async function readDispatchTurnResult(dispatch) {
|
|
775
893
|
const conversationId = getDispatchConversationId(dispatch);
|
|
776
894
|
const turnId = getDispatchTurnId(dispatch.id);
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
895
|
+
const storedSession = await getAgentTurnSessionRecord(conversationId, turnId);
|
|
896
|
+
const summary = (await listAgentTurnSessionSummariesForConversation(conversationId)).find((candidate) => candidate.sessionId === turnId);
|
|
897
|
+
const session = storedSession ?? summary;
|
|
898
|
+
const dispatchOutcome = summary?.dispatchOutcome ?? storedSession?.dispatchOutcome;
|
|
899
|
+
const resultMessageTs = summary?.resultMessageId ?? storedSession?.resultMessageId;
|
|
900
|
+
const errorMessage = storedSession?.errorMessage;
|
|
901
|
+
if (dispatchOutcome) {
|
|
902
|
+
return {
|
|
903
|
+
...errorMessage ? { errorMessage } : {},
|
|
904
|
+
outcome: dispatchOutcome,
|
|
905
|
+
...resultMessageTs ? { resultMessageTs } : {}
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
if (resultMessageTs) {
|
|
909
|
+
return {
|
|
910
|
+
outcome: "completed",
|
|
911
|
+
resultMessageTs
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
if (!session) {
|
|
915
|
+
return {};
|
|
916
|
+
}
|
|
917
|
+
if (session.state === "awaiting_resume") {
|
|
918
|
+
return { hasResumableRun: true, outcome: "awaiting_resume" };
|
|
919
|
+
}
|
|
920
|
+
if (session.state === "running") {
|
|
921
|
+
return { hasResumableRun: true };
|
|
922
|
+
}
|
|
923
|
+
if (session.state !== "completed") {
|
|
924
|
+
return {};
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
outcome: "completed",
|
|
928
|
+
...resultMessageTs ? { resultMessageTs } : {}
|
|
785
929
|
};
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
930
|
+
}
|
|
931
|
+
async function projectDispatchTurnResult(dispatchId, result) {
|
|
932
|
+
switch (result.outcome) {
|
|
933
|
+
case "awaiting_resume":
|
|
934
|
+
await markDispatchAwaitingResume(dispatchId);
|
|
935
|
+
break;
|
|
936
|
+
case "blocked":
|
|
937
|
+
await markDispatchBlocked(
|
|
938
|
+
dispatchId,
|
|
939
|
+
"Dispatch requires authorization that is unavailable for background work",
|
|
940
|
+
result.resultMessageTs
|
|
941
|
+
);
|
|
942
|
+
break;
|
|
943
|
+
case "failed":
|
|
944
|
+
await markDispatchFailed(
|
|
945
|
+
dispatchId,
|
|
946
|
+
result.errorMessage ?? "Agent turn failed",
|
|
947
|
+
result.resultMessageTs
|
|
948
|
+
);
|
|
949
|
+
break;
|
|
950
|
+
case "completed":
|
|
951
|
+
await markDispatchCompleted(dispatchId, result.resultMessageTs);
|
|
952
|
+
break;
|
|
803
953
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
954
|
+
}
|
|
955
|
+
function getDispatchBlockingError(error) {
|
|
956
|
+
const cause = getConversationTurnBoundaryError(error)?.cause ?? error;
|
|
957
|
+
return cause instanceof AuthorizationFlowDisabledError || cause instanceof PluginCredentialFailureError ? cause : void 0;
|
|
958
|
+
}
|
|
959
|
+
async function persistBlockedDispatchTurn(dispatch, error) {
|
|
960
|
+
const conversationId = getDispatchConversationId(dispatch);
|
|
961
|
+
const sessionId = getDispatchTurnId(dispatch.id);
|
|
962
|
+
const session = await getAgentTurnSessionRecord(conversationId, sessionId);
|
|
963
|
+
await recordAgentTurnSessionSummary({
|
|
964
|
+
actor: dispatch.actor,
|
|
965
|
+
conversationId,
|
|
966
|
+
destination: dispatch.destination,
|
|
967
|
+
destinationVisibility: dispatch.destinationVisibility,
|
|
968
|
+
dispatchId: dispatch.id,
|
|
969
|
+
dispatchOutcome: "blocked",
|
|
970
|
+
sessionId,
|
|
971
|
+
sliceId: session?.sliceId ?? 1,
|
|
972
|
+
source: dispatch.source,
|
|
973
|
+
state: "failed",
|
|
974
|
+
surface: "api"
|
|
975
|
+
});
|
|
976
|
+
await markDispatchBlocked(
|
|
977
|
+
dispatch.id,
|
|
978
|
+
error instanceof AuthorizationFlowDisabledError ? `Dispatch requires ${error.provider} authorization.` : error.message
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
function createAgentDispatchConversationWorker(options) {
|
|
982
|
+
return async (context, dispatchId) => {
|
|
983
|
+
const dispatch = await getDispatchRecord(dispatchId);
|
|
984
|
+
if (!dispatch) {
|
|
985
|
+
throw new Error(`Dispatch record is missing for ${dispatchId}`);
|
|
822
986
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
const completedSession = await getAgentTurnSessionRecord(
|
|
828
|
-
conversationId,
|
|
829
|
-
turnId
|
|
830
|
-
);
|
|
831
|
-
if (completedSession?.state === "completed") {
|
|
832
|
-
const deliveredMessage = [...conversation.messages].reverse().find(
|
|
833
|
-
(message) => message.id.startsWith(`${turnId}:assistant:`) && typeof message.meta?.slackTs === "string"
|
|
987
|
+
const expectedConversationId = getDispatchConversationId(dispatch);
|
|
988
|
+
if (context.conversationId !== expectedConversationId) {
|
|
989
|
+
throw new Error(
|
|
990
|
+
`Dispatch ${dispatch.id} belongs to ${expectedConversationId}, not ${context.conversationId}`
|
|
834
991
|
);
|
|
835
|
-
await markDispatch({
|
|
836
|
-
dispatch,
|
|
837
|
-
status: "completed",
|
|
838
|
-
...deliveredMessage?.meta?.slackTs ? { resultMessageTs: deliveredMessage.meta.slackTs } : {}
|
|
839
|
-
});
|
|
840
|
-
return;
|
|
841
992
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
const
|
|
849
|
-
|
|
850
|
-
dispatch,
|
|
851
|
-
nowMs
|
|
852
|
-
});
|
|
853
|
-
await persistConversationMessages({ conversation, conversationId });
|
|
854
|
-
await turnLifecycle.start({
|
|
855
|
-
conversationId,
|
|
856
|
-
turnId,
|
|
857
|
-
createdAtMs: nowMs,
|
|
858
|
-
inputMessageIds: [userMessageId],
|
|
859
|
-
surface: "api"
|
|
860
|
-
});
|
|
861
|
-
lifecycleStarted = true;
|
|
862
|
-
failureCode = "agent_run_failed";
|
|
863
|
-
const conversationContext = buildConversationContext(conversation, {
|
|
864
|
-
excludeMessageId: userMessageId
|
|
865
|
-
});
|
|
866
|
-
let resultMessageTs;
|
|
867
|
-
let assistantMessageDelivered = turnHasReply(conversation, turnId);
|
|
868
|
-
const deliverAssistantMessage = async (assistantMessage) => {
|
|
869
|
-
if (!assistantMessage.text.trim()) {
|
|
993
|
+
if (context.destination.platform !== dispatch.destination.platform || context.destination.teamId !== dispatch.destination.teamId || context.destination.channelId !== dispatch.destination.channelId) {
|
|
994
|
+
throw new Error(
|
|
995
|
+
`Dispatch ${dispatch.id} destination does not match its conversation lease`
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
let acknowledged = context.attempt.messages.length === 0;
|
|
999
|
+
const acknowledge = async () => {
|
|
1000
|
+
if (acknowledged) {
|
|
870
1001
|
return;
|
|
871
1002
|
}
|
|
872
|
-
failureCode = "delivery_failed";
|
|
873
1003
|
try {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
});
|
|
879
|
-
} catch (error) {
|
|
880
|
-
if (isRetryableSlackPostError(error)) {
|
|
881
|
-
throw new RetryableDeliveryError(error);
|
|
882
|
-
}
|
|
883
|
-
throw error;
|
|
884
|
-
}
|
|
885
|
-
assistantMessageDelivered = true;
|
|
886
|
-
const recordedMessageId = recordDeliveredAssistantMessage({
|
|
887
|
-
conversation,
|
|
888
|
-
sessionId: turnId,
|
|
889
|
-
text: assistantMessage.text,
|
|
890
|
-
userMessageId
|
|
891
|
-
});
|
|
892
|
-
if (resultMessageTs) {
|
|
893
|
-
markConversationMessage(conversation, recordedMessageId, {
|
|
894
|
-
slackTs: resultMessageTs
|
|
895
|
-
});
|
|
896
|
-
}
|
|
897
|
-
try {
|
|
898
|
-
await persistWithRetry(
|
|
899
|
-
() => persistConversationMessages({ conversation, conversationId })
|
|
900
|
-
);
|
|
901
|
-
} catch (persistError) {
|
|
902
|
-
logException(
|
|
903
|
-
new Error("Accepted assistant message persistence failed"),
|
|
904
|
-
"agent_dispatch_assistant_message_post_delivery_persist_failed",
|
|
905
|
-
logContext,
|
|
906
|
-
{
|
|
907
|
-
"error.type": persistError instanceof Error ? persistError.name : typeof persistError
|
|
908
|
-
},
|
|
909
|
-
"Failed to persist an accepted dispatch assistant message"
|
|
1004
|
+
await context.attempt.ack();
|
|
1005
|
+
} catch {
|
|
1006
|
+
throw new TurnInputCommitLostError(
|
|
1007
|
+
`Conversation work lease lost before dispatch inbox ack for ${context.conversationId}`
|
|
910
1008
|
);
|
|
911
1009
|
}
|
|
912
|
-
|
|
1010
|
+
acknowledged = true;
|
|
913
1011
|
};
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
runId: dispatch.id,
|
|
918
|
-
input: {
|
|
919
|
-
messageText: dispatch.input,
|
|
920
|
-
conversationContext,
|
|
921
|
-
// Pi history for redelivered dispatch slices comes from the SQL
|
|
922
|
-
// event-store projection, not a thread-state mirror.
|
|
923
|
-
piMessages: await loadProjection({ conversationId })
|
|
924
|
-
},
|
|
925
|
-
routing: {
|
|
926
|
-
credentialContext: {
|
|
927
|
-
actor: dispatch.actor,
|
|
928
|
-
...dispatch.credentialSubject ? { subject: dispatch.credentialSubject } : {}
|
|
929
|
-
},
|
|
930
|
-
destination: dispatch.destination,
|
|
931
|
-
destinationVisibility: dispatch.destinationVisibility,
|
|
932
|
-
source: dispatch.source,
|
|
933
|
-
dispatch: {
|
|
934
|
-
actor: dispatch.actor,
|
|
935
|
-
metadata: dispatch.metadata,
|
|
936
|
-
plugin: dispatch.plugin
|
|
937
|
-
},
|
|
938
|
-
surface: "api",
|
|
939
|
-
toolChannelId: dispatch.destination.channelId
|
|
940
|
-
},
|
|
941
|
-
policy: {
|
|
942
|
-
authorizationFlowMode: "disabled",
|
|
943
|
-
configuration,
|
|
944
|
-
channelConfiguration
|
|
945
|
-
},
|
|
946
|
-
state: {
|
|
947
|
-
artifactState: artifacts,
|
|
948
|
-
sandboxRef
|
|
949
|
-
},
|
|
950
|
-
delivery: {
|
|
951
|
-
onAssistantMessage: deliverAssistantMessage
|
|
952
|
-
},
|
|
953
|
-
durability: {
|
|
954
|
-
onSandboxRefChanged: async (nextSandboxRef) => {
|
|
955
|
-
sandboxRef = nextSandboxRef;
|
|
956
|
-
await persistRuntimePatch({
|
|
957
|
-
threadId: conversationId,
|
|
958
|
-
conversation,
|
|
959
|
-
artifacts,
|
|
960
|
-
sandboxRef
|
|
961
|
-
});
|
|
962
|
-
},
|
|
963
|
-
onArtifactStateUpdated: async (nextArtifacts2) => {
|
|
964
|
-
artifacts = nextArtifacts2;
|
|
965
|
-
await persistRuntimePatch({
|
|
966
|
-
threadId: conversationId,
|
|
967
|
-
conversation,
|
|
968
|
-
artifacts,
|
|
969
|
-
sandboxRef
|
|
970
|
-
});
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
});
|
|
974
|
-
if (outcome.status === "awaiting_auth") {
|
|
975
|
-
await turnLifecycle.fail({
|
|
976
|
-
conversationId,
|
|
977
|
-
turnId,
|
|
978
|
-
createdAtMs: Date.now(),
|
|
979
|
-
failureCode: "agent_run_failed"
|
|
980
|
-
});
|
|
981
|
-
lifecycleTerminalized = true;
|
|
982
|
-
await markDispatch({
|
|
983
|
-
dispatch,
|
|
984
|
-
status: "blocked",
|
|
985
|
-
errorMessage: "Dispatch requires authorization from an interactive user turn."
|
|
986
|
-
});
|
|
987
|
-
return;
|
|
1012
|
+
if (isTerminalDispatchStatus(dispatch.status)) {
|
|
1013
|
+
await acknowledge();
|
|
1014
|
+
return { status: "completed" };
|
|
988
1015
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
}
|
|
994
|
-
await scheduleCallback({
|
|
995
|
-
id: awaiting.id,
|
|
996
|
-
expectedVersion: awaiting.version
|
|
997
|
-
});
|
|
998
|
-
return;
|
|
1016
|
+
const durableResult = await readDispatchTurnResult(dispatch);
|
|
1017
|
+
if (durableResult.outcome === "blocked" || durableResult.outcome === "completed" || durableResult.outcome === "failed") {
|
|
1018
|
+
await projectDispatchTurnResult(dispatch.id, durableResult);
|
|
1019
|
+
await acknowledge();
|
|
1020
|
+
return { status: "completed" };
|
|
999
1021
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
const finalized = finalizeFailedTurnReplyWithEvent({
|
|
1005
|
-
reply,
|
|
1006
|
-
logException,
|
|
1007
|
-
context: {
|
|
1008
|
-
...logContext,
|
|
1009
|
-
modelId: reply.diagnostics.modelId
|
|
1010
|
-
}
|
|
1011
|
-
});
|
|
1012
|
-
reply = finalized.reply;
|
|
1013
|
-
modelFailureEventId = finalized.eventId;
|
|
1014
|
-
await deliverAssistantMessage({ text: reply.text });
|
|
1015
|
-
}
|
|
1016
|
-
failureCode = "persistence_failed";
|
|
1017
|
-
markConversationMessage(conversation, userMessageId, {
|
|
1018
|
-
replied: true,
|
|
1019
|
-
skippedReason: void 0
|
|
1020
|
-
});
|
|
1021
|
-
updateConversationStats(conversation);
|
|
1022
|
-
const nextArtifacts = reply.artifactStatePatch ? mergeArtifactsState(artifacts, reply.artifactStatePatch) : artifacts;
|
|
1023
|
-
let statePersisted = false;
|
|
1024
|
-
try {
|
|
1025
|
-
await persistWithRetry(async () => {
|
|
1026
|
-
await persistConversationMessages({ conversation, conversationId });
|
|
1027
|
-
await persistRuntimePatch({
|
|
1028
|
-
threadId: conversationId,
|
|
1029
|
-
conversation,
|
|
1030
|
-
artifacts: nextArtifacts,
|
|
1031
|
-
sandboxRef: reply.sandboxRef ?? sandboxRef
|
|
1032
|
-
});
|
|
1033
|
-
});
|
|
1034
|
-
statePersisted = true;
|
|
1035
|
-
} catch (persistError) {
|
|
1036
|
-
const eventId = logException(
|
|
1037
|
-
persistError,
|
|
1038
|
-
"agent_dispatch_post_delivery_persist_failed",
|
|
1039
|
-
logContext,
|
|
1040
|
-
{},
|
|
1041
|
-
"Failed to persist delivered dispatch state after Slack accepted the reply"
|
|
1022
|
+
if (Date.now() - dispatch.createdAtMs > AGENT_DISPATCH_MAX_AGE_MS) {
|
|
1023
|
+
await markDispatchFailed(
|
|
1024
|
+
dispatch.id,
|
|
1025
|
+
"Dispatch exceeded its maximum processing age"
|
|
1042
1026
|
);
|
|
1043
|
-
await
|
|
1044
|
-
|
|
1045
|
-
turnId,
|
|
1046
|
-
createdAtMs: Date.now(),
|
|
1047
|
-
failureCode: "persistence_failed",
|
|
1048
|
-
...eventId ? { eventId } : {}
|
|
1049
|
-
});
|
|
1050
|
-
lifecycleTerminalized = true;
|
|
1027
|
+
await acknowledge();
|
|
1028
|
+
return { status: "completed" };
|
|
1051
1029
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
sessionId: turnId,
|
|
1056
|
-
sliceId: 1,
|
|
1057
|
-
messages: reply.piMessages,
|
|
1058
|
-
modelId: reply.diagnostics.modelId,
|
|
1059
|
-
durationMs: reply.diagnostics.durationMs,
|
|
1060
|
-
usage: reply.diagnostics.usage,
|
|
1061
|
-
reasoningLevel: reply.diagnostics.reasoningLevel,
|
|
1062
|
-
destination: dispatch.destination,
|
|
1063
|
-
destinationVisibility: dispatch.destinationVisibility,
|
|
1064
|
-
source: dispatch.source,
|
|
1065
|
-
actor: dispatch.actor,
|
|
1066
|
-
surface: "api"
|
|
1067
|
-
});
|
|
1030
|
+
const runningDispatch = await markDispatchRunning(dispatch.id);
|
|
1031
|
+
if (!runningDispatch) {
|
|
1032
|
+
throw new Error(`Dispatch record disappeared for ${dispatch.id}`);
|
|
1068
1033
|
}
|
|
1069
|
-
if (
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1034
|
+
if (isTerminalDispatchStatus(runningDispatch.status)) {
|
|
1035
|
+
await acknowledge();
|
|
1036
|
+
return { status: "completed" };
|
|
1037
|
+
}
|
|
1038
|
+
try {
|
|
1039
|
+
const resumesDurableTurn = durableResult.hasResumableRun === true;
|
|
1040
|
+
if (resumesDurableTurn && context.attempt.messages.length > 0) {
|
|
1041
|
+
await acknowledge();
|
|
1042
|
+
}
|
|
1043
|
+
let result;
|
|
1044
|
+
if (resumesDurableTurn) {
|
|
1045
|
+
await options.resumeTurn(dispatch, {
|
|
1046
|
+
shouldYield: context.shouldYield
|
|
1077
1047
|
});
|
|
1048
|
+
result = await readDispatchTurnResult(dispatch);
|
|
1078
1049
|
} else {
|
|
1079
|
-
await
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
createdAtMs: Date.now(),
|
|
1083
|
-
outcome: assistantMessageDelivered ? "success" : "no_reply"
|
|
1050
|
+
const runtimeResult = await options.runTurn(dispatch, {
|
|
1051
|
+
ack: acknowledge,
|
|
1052
|
+
shouldYield: context.shouldYield
|
|
1084
1053
|
});
|
|
1054
|
+
result = runtimeResult.outcome ? runtimeResult : {
|
|
1055
|
+
...runtimeResult,
|
|
1056
|
+
...await readDispatchTurnResult(dispatch)
|
|
1057
|
+
};
|
|
1085
1058
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
dispatch,
|
|
1090
|
-
status: failure ? "failed" : "completed",
|
|
1091
|
-
...failure ? { errorMessage: failure } : {},
|
|
1092
|
-
resultMessageTs
|
|
1093
|
-
});
|
|
1094
|
-
if (!failure) {
|
|
1095
|
-
try {
|
|
1096
|
-
await scheduleCompletedTasks({
|
|
1097
|
-
conversationId,
|
|
1098
|
-
sessionId: turnId
|
|
1099
|
-
});
|
|
1100
|
-
} catch (error) {
|
|
1101
|
-
logException(
|
|
1102
|
-
error,
|
|
1103
|
-
"plugin_session_completed_task_schedule_failed",
|
|
1104
|
-
logContext,
|
|
1105
|
-
{},
|
|
1106
|
-
"Plugin session.completed task scheduling failed"
|
|
1059
|
+
if (!result.outcome) {
|
|
1060
|
+
throw new Error(
|
|
1061
|
+
`Dispatch turn ${dispatch.id} returned without a durable outcome`
|
|
1107
1062
|
);
|
|
1108
1063
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
if (lifecycleStarted && !lifecycleTerminalized) {
|
|
1113
|
-
await turnLifecycle.fail({
|
|
1114
|
-
conversationId,
|
|
1115
|
-
turnId,
|
|
1116
|
-
createdAtMs: Date.now(),
|
|
1117
|
-
failureCode: "agent_run_failed"
|
|
1118
|
-
});
|
|
1119
|
-
lifecycleTerminalized = true;
|
|
1064
|
+
await projectDispatchTurnResult(dispatch.id, result);
|
|
1065
|
+
if (result.outcome && !acknowledged) {
|
|
1066
|
+
await acknowledge();
|
|
1120
1067
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
return;
|
|
1127
|
-
}
|
|
1128
|
-
if (error instanceof PluginCredentialFailureError) {
|
|
1129
|
-
if (lifecycleStarted && !lifecycleTerminalized) {
|
|
1130
|
-
await turnLifecycle.fail({
|
|
1131
|
-
conversationId,
|
|
1132
|
-
turnId,
|
|
1133
|
-
createdAtMs: Date.now(),
|
|
1134
|
-
failureCode: "agent_run_failed"
|
|
1135
|
-
});
|
|
1136
|
-
lifecycleTerminalized = true;
|
|
1068
|
+
return result.outcome === "awaiting_resume" && context.shouldYield() ? { status: "yielded" } : { status: "completed" };
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
if (isCooperativeTurnYieldError(error)) {
|
|
1071
|
+
await markDispatchAwaitingResume(dispatch.id);
|
|
1072
|
+
return { status: "yielded" };
|
|
1137
1073
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
await turnLifecycle.fail({
|
|
1157
|
-
conversationId,
|
|
1158
|
-
turnId,
|
|
1159
|
-
createdAtMs: Date.now(),
|
|
1160
|
-
failureCode,
|
|
1161
|
-
...eventId ? { eventId } : {}
|
|
1162
|
-
});
|
|
1163
|
-
lifecycleTerminalized = true;
|
|
1074
|
+
if (isTurnInputCommitLostError(error)) {
|
|
1075
|
+
return { status: "lost_lease" };
|
|
1076
|
+
}
|
|
1077
|
+
const blockingError = getDispatchBlockingError(error);
|
|
1078
|
+
if (blockingError) {
|
|
1079
|
+
await persistBlockedDispatchTurn(dispatch, blockingError);
|
|
1080
|
+
await acknowledge();
|
|
1081
|
+
return { status: "completed" };
|
|
1082
|
+
}
|
|
1083
|
+
if (!context.attempt.isFinalAttempt) {
|
|
1084
|
+
throw error;
|
|
1085
|
+
}
|
|
1086
|
+
await markDispatchFailed(
|
|
1087
|
+
dispatch.id,
|
|
1088
|
+
error instanceof Error ? error.message : "Dispatch turn failed"
|
|
1089
|
+
);
|
|
1090
|
+
await acknowledge();
|
|
1091
|
+
return { status: "completed" };
|
|
1164
1092
|
}
|
|
1165
|
-
|
|
1166
|
-
dispatch,
|
|
1167
|
-
status: "failed",
|
|
1168
|
-
errorMessage: error instanceof Error ? error.message : String(error)
|
|
1169
|
-
});
|
|
1170
|
-
} finally {
|
|
1171
|
-
await stateAdapter.releaseLock(destinationLock);
|
|
1172
|
-
}
|
|
1093
|
+
};
|
|
1173
1094
|
}
|
|
1174
1095
|
|
|
1175
1096
|
// src/handlers/agent-dispatch.ts
|
|
@@ -1179,15 +1100,24 @@ async function POST(request, waitUntil, options) {
|
|
|
1179
1100
|
return new Response("Unauthorized", { status: 401 });
|
|
1180
1101
|
}
|
|
1181
1102
|
waitUntil(
|
|
1182
|
-
() =>
|
|
1183
|
-
|
|
1184
|
-
|
|
1103
|
+
(async () => {
|
|
1104
|
+
const dispatch = await getDispatchRecord(payload.id);
|
|
1105
|
+
if (!dispatch) {
|
|
1106
|
+
throw new Error(`Dispatch record is missing for ${payload.id}`);
|
|
1107
|
+
}
|
|
1108
|
+
if (isTerminalDispatchStatus(dispatch.status)) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
await enqueueAgentDispatch(dispatch, {
|
|
1112
|
+
queue: options.conversationWorkQueue
|
|
1113
|
+
});
|
|
1114
|
+
})().catch((error) => {
|
|
1185
1115
|
logException(
|
|
1186
1116
|
error,
|
|
1187
|
-
"
|
|
1117
|
+
"agent_dispatch_callback_failed",
|
|
1188
1118
|
{},
|
|
1189
1119
|
{ "app.dispatch.id": payload.id },
|
|
1190
|
-
"
|
|
1120
|
+
"Legacy dispatch callback failed"
|
|
1191
1121
|
);
|
|
1192
1122
|
})
|
|
1193
1123
|
);
|
|
@@ -1446,12 +1376,6 @@ async function verifyDispatchCredentialSubjectAccess(options, plugin) {
|
|
|
1446
1376
|
|
|
1447
1377
|
// src/chat/agent-dispatch/context.ts
|
|
1448
1378
|
var MAX_DISPATCHES_PER_HEARTBEAT = 25;
|
|
1449
|
-
function shouldScheduleDispatch(record, nowMs) {
|
|
1450
|
-
if (isTerminalDispatchStatus(record.status)) {
|
|
1451
|
-
return false;
|
|
1452
|
-
}
|
|
1453
|
-
return record.status !== "running" || typeof record.leaseExpiresAtMs !== "number" || record.leaseExpiresAtMs <= nowMs;
|
|
1454
|
-
}
|
|
1455
1379
|
function bindDispatchCredentialSubject(options, plugin) {
|
|
1456
1380
|
const { credentialSubject, ...baseOptions } = options;
|
|
1457
1381
|
if (!credentialSubject) {
|
|
@@ -1505,10 +1429,10 @@ function createHeartbeatContext(args) {
|
|
|
1505
1429
|
nowMs: args.nowMs
|
|
1506
1430
|
});
|
|
1507
1431
|
dispatchCount += 1;
|
|
1508
|
-
if (
|
|
1509
|
-
await
|
|
1510
|
-
|
|
1511
|
-
|
|
1432
|
+
if (!isTerminalDispatchStatus(result.record.status)) {
|
|
1433
|
+
await enqueueAgentDispatch(result.record, {
|
|
1434
|
+
queue: args.conversationWorkQueue,
|
|
1435
|
+
nowMs: args.nowMs
|
|
1512
1436
|
});
|
|
1513
1437
|
}
|
|
1514
1438
|
return {
|
|
@@ -1527,37 +1451,9 @@ function createHeartbeatContext(args) {
|
|
|
1527
1451
|
}
|
|
1528
1452
|
|
|
1529
1453
|
// src/chat/agent-dispatch/heartbeat.ts
|
|
1530
|
-
var DEFAULT_RECOVERY_LIMIT2 = 25;
|
|
1531
1454
|
var DEFAULT_PLUGIN_LIMIT = 25;
|
|
1532
|
-
var DISPATCH_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
1533
1455
|
var PLUGIN_HEARTBEAT_TIMEOUT_MS = 25e3;
|
|
1534
|
-
|
|
1535
|
-
if (args.record.status === "running") {
|
|
1536
|
-
return typeof args.record.leaseExpiresAtMs === "number" && args.record.leaseExpiresAtMs <= args.nowMs;
|
|
1537
|
-
}
|
|
1538
|
-
if (args.record.status === "awaiting_resume") {
|
|
1539
|
-
return typeof args.record.leaseExpiresAtMs !== "number" || args.record.leaseExpiresAtMs <= args.nowMs;
|
|
1540
|
-
}
|
|
1541
|
-
if (args.record.status === "pending") {
|
|
1542
|
-
return typeof args.record.lastCallbackAtMs !== "number" || args.record.lastCallbackAtMs + 6e4 <= args.nowMs;
|
|
1543
|
-
}
|
|
1544
|
-
return false;
|
|
1545
|
-
}
|
|
1546
|
-
async function failDispatch(args) {
|
|
1547
|
-
await withDispatchLock(args.record.id, async (state) => {
|
|
1548
|
-
const current = parseDispatchRecord(
|
|
1549
|
-
await state.get(getDispatchStorageKey(args.record.id))
|
|
1550
|
-
) ?? args.record;
|
|
1551
|
-
if (isTerminalDispatchStatus(current.status)) {
|
|
1552
|
-
return;
|
|
1553
|
-
}
|
|
1554
|
-
await updateDispatchRecord(state, {
|
|
1555
|
-
...current,
|
|
1556
|
-
errorMessage: args.errorMessage,
|
|
1557
|
-
status: "failed"
|
|
1558
|
-
});
|
|
1559
|
-
});
|
|
1560
|
-
}
|
|
1456
|
+
var DISPATCH_MAILBOX_APPEND_LIMIT = 100;
|
|
1561
1457
|
async function runWithTimeout(promise, timeoutMs) {
|
|
1562
1458
|
let timeout;
|
|
1563
1459
|
try {
|
|
@@ -1575,52 +1471,6 @@ async function runWithTimeout(promise, timeoutMs) {
|
|
|
1575
1471
|
}
|
|
1576
1472
|
}
|
|
1577
1473
|
}
|
|
1578
|
-
async function recoverStaleDispatches(args) {
|
|
1579
|
-
const ids = await listIncompleteDispatchIds();
|
|
1580
|
-
let recovered = 0;
|
|
1581
|
-
for (const id of ids) {
|
|
1582
|
-
if (recovered >= (args.limit ?? DEFAULT_RECOVERY_LIMIT2)) {
|
|
1583
|
-
break;
|
|
1584
|
-
}
|
|
1585
|
-
const record = await getDispatchRecord(id);
|
|
1586
|
-
if (!record || isTerminalDispatchStatus(record.status)) {
|
|
1587
|
-
continue;
|
|
1588
|
-
}
|
|
1589
|
-
try {
|
|
1590
|
-
if (!isStaleDispatch({ record, nowMs: args.nowMs })) {
|
|
1591
|
-
continue;
|
|
1592
|
-
}
|
|
1593
|
-
if (record.createdAtMs + DISPATCH_MAX_AGE_MS <= args.nowMs) {
|
|
1594
|
-
await failDispatch({
|
|
1595
|
-
record,
|
|
1596
|
-
errorMessage: "Dispatch expired before completion."
|
|
1597
|
-
});
|
|
1598
|
-
continue;
|
|
1599
|
-
}
|
|
1600
|
-
if (record.attempt >= record.maxAttempts) {
|
|
1601
|
-
await failDispatch({
|
|
1602
|
-
record,
|
|
1603
|
-
errorMessage: "Dispatch exceeded retry attempts."
|
|
1604
|
-
});
|
|
1605
|
-
continue;
|
|
1606
|
-
}
|
|
1607
|
-
await scheduleDispatchCallback({
|
|
1608
|
-
id: record.id,
|
|
1609
|
-
expectedVersion: record.version
|
|
1610
|
-
});
|
|
1611
|
-
recovered += 1;
|
|
1612
|
-
} catch (error) {
|
|
1613
|
-
logException(
|
|
1614
|
-
error,
|
|
1615
|
-
"agent_dispatch_recovery_failed",
|
|
1616
|
-
{ runId: record.id },
|
|
1617
|
-
{ "app.plugin.name": record.plugin },
|
|
1618
|
-
"Agent dispatch recovery failed"
|
|
1619
|
-
);
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
return recovered;
|
|
1623
|
-
}
|
|
1624
1474
|
async function runPluginHeartbeats(args) {
|
|
1625
1475
|
let count = 0;
|
|
1626
1476
|
for (const plugin of getPlugins()) {
|
|
@@ -1638,6 +1488,7 @@ async function runPluginHeartbeats(args) {
|
|
|
1638
1488
|
Promise.resolve(
|
|
1639
1489
|
heartbeat(
|
|
1640
1490
|
createHeartbeatContext({
|
|
1491
|
+
conversationWorkQueue: args.conversationWorkQueue,
|
|
1641
1492
|
plugin,
|
|
1642
1493
|
nowMs: args.nowMs
|
|
1643
1494
|
})
|
|
@@ -1667,13 +1518,55 @@ async function runPluginHeartbeats(args) {
|
|
|
1667
1518
|
}
|
|
1668
1519
|
}
|
|
1669
1520
|
}
|
|
1521
|
+
async function recoverPendingDispatchMailboxAppends(args) {
|
|
1522
|
+
const ids = (await listPendingDispatchMailboxAppends()).slice(
|
|
1523
|
+
0,
|
|
1524
|
+
DISPATCH_MAILBOX_APPEND_LIMIT
|
|
1525
|
+
);
|
|
1526
|
+
for (const id of ids) {
|
|
1527
|
+
try {
|
|
1528
|
+
const dispatch = await getDispatchRecord(id);
|
|
1529
|
+
if (!dispatch || isTerminalDispatchStatus(dispatch.status)) {
|
|
1530
|
+
await confirmDispatchMailboxAppend(id);
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
if (args.nowMs - dispatch.createdAtMs > AGENT_DISPATCH_MAX_AGE_MS) {
|
|
1534
|
+
await markDispatchFailed(
|
|
1535
|
+
id,
|
|
1536
|
+
"Dispatch expired before its conversation mailbox append completed"
|
|
1537
|
+
);
|
|
1538
|
+
await confirmDispatchMailboxAppend(id);
|
|
1539
|
+
continue;
|
|
1540
|
+
}
|
|
1541
|
+
await enqueueAgentDispatch(dispatch, {
|
|
1542
|
+
nowMs: args.nowMs,
|
|
1543
|
+
queue: args.conversationWorkQueue
|
|
1544
|
+
});
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
logException(
|
|
1547
|
+
error,
|
|
1548
|
+
"dispatch_mailbox_append_recovery_failed",
|
|
1549
|
+
{},
|
|
1550
|
+
{ "app.dispatch.id": id },
|
|
1551
|
+
"Pending dispatch mailbox append recovery failed"
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1670
1556
|
async function runHeartbeat(args) {
|
|
1557
|
+
const queue = args.conversationWorkQueue ?? getVercelConversationWorkQueue();
|
|
1671
1558
|
await recoverConversationWork({
|
|
1672
1559
|
nowMs: args.nowMs,
|
|
1673
|
-
queue
|
|
1560
|
+
queue
|
|
1561
|
+
});
|
|
1562
|
+
await recoverPendingDispatchMailboxAppends({
|
|
1563
|
+
conversationWorkQueue: queue,
|
|
1564
|
+
nowMs: args.nowMs
|
|
1565
|
+
});
|
|
1566
|
+
await runPluginHeartbeats({
|
|
1567
|
+
conversationWorkQueue: queue,
|
|
1568
|
+
nowMs: args.nowMs
|
|
1674
1569
|
});
|
|
1675
|
-
await recoverStaleDispatches({ nowMs: args.nowMs });
|
|
1676
|
-
await runPluginHeartbeats({ nowMs: args.nowMs });
|
|
1677
1570
|
}
|
|
1678
1571
|
|
|
1679
1572
|
// src/handlers/heartbeat.ts
|
|
@@ -2412,7 +2305,7 @@ import {
|
|
|
2412
2305
|
Message,
|
|
2413
2306
|
ThreadImpl
|
|
2414
2307
|
} from "chat";
|
|
2415
|
-
import { z as
|
|
2308
|
+
import { z as z5 } from "zod";
|
|
2416
2309
|
|
|
2417
2310
|
// src/chat/ingress/message-router.ts
|
|
2418
2311
|
function normalizeIncomingSlackThreadId(threadId, message) {
|
|
@@ -2583,14 +2476,14 @@ async function ensureSlackMessageActorIdentity(message, teamId, lookupSlackUser2
|
|
|
2583
2476
|
}
|
|
2584
2477
|
|
|
2585
2478
|
// src/chat/slack/user.ts
|
|
2586
|
-
import { z as
|
|
2587
|
-
var slackUserInfoSchema =
|
|
2588
|
-
ok:
|
|
2589
|
-
user:
|
|
2590
|
-
name:
|
|
2591
|
-
profile:
|
|
2592
|
-
email:
|
|
2593
|
-
real_name:
|
|
2479
|
+
import { z as z4 } from "zod";
|
|
2480
|
+
var slackUserInfoSchema = z4.object({
|
|
2481
|
+
ok: z4.literal(true),
|
|
2482
|
+
user: z4.object({
|
|
2483
|
+
name: z4.string().nullish(),
|
|
2484
|
+
profile: z4.object({
|
|
2485
|
+
email: z4.string().nullish(),
|
|
2486
|
+
real_name: z4.string().nullish()
|
|
2594
2487
|
}).optional()
|
|
2595
2488
|
})
|
|
2596
2489
|
});
|
|
@@ -2683,17 +2576,17 @@ function stripLeadingSteeringOverride(text) {
|
|
|
2683
2576
|
}
|
|
2684
2577
|
|
|
2685
2578
|
// src/chat/task-execution/slack-work.ts
|
|
2686
|
-
var slackConversationRouteSchema =
|
|
2579
|
+
var slackConversationRouteSchema = z5.enum(["mention", "subscribed"]);
|
|
2687
2580
|
function hasSteeringOverride(text) {
|
|
2688
2581
|
return stripLeadingSteeringOverride(text) !== text;
|
|
2689
2582
|
}
|
|
2690
|
-
var serializedDateSchema =
|
|
2691
|
-
var mdastPointSchema =
|
|
2692
|
-
column:
|
|
2693
|
-
line:
|
|
2694
|
-
offset:
|
|
2583
|
+
var serializedDateSchema = z5.iso.datetime();
|
|
2584
|
+
var mdastPointSchema = z5.object({
|
|
2585
|
+
column: z5.number().int().positive(),
|
|
2586
|
+
line: z5.number().int().positive(),
|
|
2587
|
+
offset: z5.number().int().nonnegative().optional()
|
|
2695
2588
|
}).strict();
|
|
2696
|
-
var mdastPositionSchema =
|
|
2589
|
+
var mdastPositionSchema = z5.object({
|
|
2697
2590
|
end: mdastPointSchema,
|
|
2698
2591
|
start: mdastPointSchema
|
|
2699
2592
|
}).strict();
|
|
@@ -2805,7 +2698,7 @@ function isFormattedContentNode(value) {
|
|
|
2805
2698
|
return false;
|
|
2806
2699
|
}
|
|
2807
2700
|
}
|
|
2808
|
-
var formattedContentSchema =
|
|
2701
|
+
var formattedContentSchema = z5.custom(
|
|
2809
2702
|
(value) => {
|
|
2810
2703
|
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
2811
2704
|
return false;
|
|
@@ -2815,79 +2708,79 @@ var formattedContentSchema = z3.custom(
|
|
|
2815
2708
|
},
|
|
2816
2709
|
"must be a valid formatted-content root"
|
|
2817
2710
|
);
|
|
2818
|
-
var serializedMessageSchema =
|
|
2819
|
-
_type:
|
|
2820
|
-
attachments:
|
|
2821
|
-
|
|
2822
|
-
type:
|
|
2823
|
-
url:
|
|
2824
|
-
name:
|
|
2825
|
-
mimeType:
|
|
2826
|
-
size:
|
|
2827
|
-
width:
|
|
2828
|
-
height:
|
|
2829
|
-
fetchMetadata:
|
|
2711
|
+
var serializedMessageSchema = z5.object({
|
|
2712
|
+
_type: z5.literal("chat:Message"),
|
|
2713
|
+
attachments: z5.array(
|
|
2714
|
+
z5.object({
|
|
2715
|
+
type: z5.enum(["image", "file", "video", "audio"]),
|
|
2716
|
+
url: z5.string().optional(),
|
|
2717
|
+
name: z5.string().optional(),
|
|
2718
|
+
mimeType: z5.string().optional(),
|
|
2719
|
+
size: z5.number().finite().optional(),
|
|
2720
|
+
width: z5.number().finite().optional(),
|
|
2721
|
+
height: z5.number().finite().optional(),
|
|
2722
|
+
fetchMetadata: z5.record(z5.string(), z5.string()).optional()
|
|
2830
2723
|
}).strict()
|
|
2831
2724
|
),
|
|
2832
|
-
author:
|
|
2833
|
-
userId:
|
|
2834
|
-
userName:
|
|
2835
|
-
fullName:
|
|
2836
|
-
isBot:
|
|
2837
|
-
isMe:
|
|
2725
|
+
author: z5.object({
|
|
2726
|
+
userId: z5.string(),
|
|
2727
|
+
userName: z5.string(),
|
|
2728
|
+
fullName: z5.string(),
|
|
2729
|
+
isBot: z5.union([z5.boolean(), z5.literal("unknown")]),
|
|
2730
|
+
isMe: z5.boolean()
|
|
2838
2731
|
}).strict(),
|
|
2839
2732
|
formatted: formattedContentSchema,
|
|
2840
|
-
id:
|
|
2841
|
-
isMention:
|
|
2842
|
-
links:
|
|
2843
|
-
|
|
2844
|
-
url:
|
|
2845
|
-
title:
|
|
2846
|
-
description:
|
|
2847
|
-
imageUrl:
|
|
2848
|
-
siteName:
|
|
2733
|
+
id: z5.string().min(1),
|
|
2734
|
+
isMention: z5.boolean().optional(),
|
|
2735
|
+
links: z5.array(
|
|
2736
|
+
z5.object({
|
|
2737
|
+
url: z5.string(),
|
|
2738
|
+
title: z5.string().optional(),
|
|
2739
|
+
description: z5.string().optional(),
|
|
2740
|
+
imageUrl: z5.string().optional(),
|
|
2741
|
+
siteName: z5.string().optional()
|
|
2849
2742
|
}).strict()
|
|
2850
2743
|
).optional(),
|
|
2851
|
-
metadata:
|
|
2744
|
+
metadata: z5.object({
|
|
2852
2745
|
dateSent: serializedDateSchema,
|
|
2853
|
-
edited:
|
|
2746
|
+
edited: z5.boolean(),
|
|
2854
2747
|
editedAt: serializedDateSchema.optional()
|
|
2855
2748
|
}).strict(),
|
|
2856
|
-
raw:
|
|
2857
|
-
text:
|
|
2858
|
-
threadId:
|
|
2749
|
+
raw: z5.unknown(),
|
|
2750
|
+
text: z5.string(),
|
|
2751
|
+
threadId: z5.string().min(1)
|
|
2859
2752
|
}).strict();
|
|
2860
|
-
var serializedThreadSchema =
|
|
2861
|
-
_type:
|
|
2862
|
-
adapterName:
|
|
2863
|
-
channelId:
|
|
2864
|
-
channelVisibility:
|
|
2753
|
+
var serializedThreadSchema = z5.object({
|
|
2754
|
+
_type: z5.literal("chat:Thread"),
|
|
2755
|
+
adapterName: z5.literal("slack"),
|
|
2756
|
+
channelId: z5.string().min(1),
|
|
2757
|
+
channelVisibility: z5.enum(["private", "workspace", "external", "unknown"]).optional(),
|
|
2865
2758
|
currentMessage: serializedMessageSchema.optional(),
|
|
2866
|
-
id:
|
|
2867
|
-
isDM:
|
|
2759
|
+
id: z5.string().min(1),
|
|
2760
|
+
isDM: z5.boolean()
|
|
2868
2761
|
}).strict();
|
|
2869
|
-
var slackInstallationSchema =
|
|
2870
|
-
enterpriseId:
|
|
2871
|
-
isEnterpriseInstall:
|
|
2872
|
-
teamId:
|
|
2762
|
+
var slackInstallationSchema = z5.object({
|
|
2763
|
+
enterpriseId: z5.string().optional(),
|
|
2764
|
+
isEnterpriseInstall: z5.boolean().optional(),
|
|
2765
|
+
teamId: z5.string().optional()
|
|
2873
2766
|
}).strict();
|
|
2874
|
-
var slackConversationMessageMetadataBaseSchema =
|
|
2767
|
+
var slackConversationMessageMetadataBaseSchema = z5.object({
|
|
2875
2768
|
installation: slackInstallationSchema.optional(),
|
|
2876
2769
|
message: serializedMessageSchema,
|
|
2877
|
-
platform:
|
|
2770
|
+
platform: z5.literal("slack"),
|
|
2878
2771
|
route: slackConversationRouteSchema,
|
|
2879
2772
|
thread: serializedThreadSchema
|
|
2880
2773
|
});
|
|
2881
|
-
var slackConversationMessageMetadataSchema =
|
|
2774
|
+
var slackConversationMessageMetadataSchema = z5.union([
|
|
2882
2775
|
slackConversationMessageMetadataBaseSchema.strict(),
|
|
2883
2776
|
slackConversationMessageMetadataBaseSchema.extend({
|
|
2884
|
-
kind:
|
|
2885
|
-
resourceEvent:
|
|
2886
|
-
eventKey:
|
|
2887
|
-
eventType:
|
|
2888
|
-
provider:
|
|
2889
|
-
resourceRef:
|
|
2890
|
-
subscriptionId:
|
|
2777
|
+
kind: z5.literal("resource_event"),
|
|
2778
|
+
resourceEvent: z5.object({
|
|
2779
|
+
eventKey: z5.string(),
|
|
2780
|
+
eventType: z5.string(),
|
|
2781
|
+
provider: z5.string(),
|
|
2782
|
+
resourceRef: z5.string(),
|
|
2783
|
+
subscriptionId: z5.string()
|
|
2891
2784
|
}).strict()
|
|
2892
2785
|
}).strict()
|
|
2893
2786
|
]);
|
|
@@ -4799,14 +4692,14 @@ function registerVercelPluginTaskDevConsumer() {
|
|
|
4799
4692
|
}
|
|
4800
4693
|
|
|
4801
4694
|
// src/chat/services/subscribed-decision.ts
|
|
4802
|
-
import { z as
|
|
4803
|
-
var replyDecisionSchema =
|
|
4804
|
-
should_reply:
|
|
4805
|
-
should_unsubscribe:
|
|
4695
|
+
import { z as z6 } from "zod";
|
|
4696
|
+
var replyDecisionSchema = z6.object({
|
|
4697
|
+
should_reply: z6.boolean().describe("Whether Junior should respond to this thread message."),
|
|
4698
|
+
should_unsubscribe: z6.boolean().describe(
|
|
4806
4699
|
"Whether Junior should unsubscribe from this thread because the user clearly asked it to stop participating."
|
|
4807
4700
|
),
|
|
4808
|
-
confidence:
|
|
4809
|
-
reason:
|
|
4701
|
+
confidence: z6.number().min(0).max(1).describe("Classifier confidence from 0 to 1."),
|
|
4702
|
+
reason: z6.string().describe("Short reason for the decision; use an empty string if none.")
|
|
4810
4703
|
}).strict();
|
|
4811
4704
|
var ROUTER_CONFIDENCE_THRESHOLD = 0.8;
|
|
4812
4705
|
var ROUTER_CLASSIFIER_MAX_TOKENS = 240;
|
|
@@ -6580,11 +6473,11 @@ function getSlackMessageTs(message) {
|
|
|
6580
6473
|
}
|
|
6581
6474
|
|
|
6582
6475
|
// src/chat/slack/action-token.ts
|
|
6583
|
-
import { z as
|
|
6584
|
-
var slackActionTokenSchema =
|
|
6585
|
-
var slackMessageEnvelopeSchema =
|
|
6586
|
-
raw:
|
|
6587
|
-
action_token:
|
|
6476
|
+
import { z as z7 } from "zod";
|
|
6477
|
+
var slackActionTokenSchema = z7.string().trim().min(1).brand();
|
|
6478
|
+
var slackMessageEnvelopeSchema = z7.object({
|
|
6479
|
+
raw: z7.object({
|
|
6480
|
+
action_token: z7.unknown().optional()
|
|
6588
6481
|
}).optional()
|
|
6589
6482
|
});
|
|
6590
6483
|
function readSlackActionToken(message) {
|
|
@@ -6869,14 +6762,14 @@ function createReplyToThread(deps) {
|
|
|
6869
6762
|
}
|
|
6870
6763
|
const threadId = getThreadId(thread, message);
|
|
6871
6764
|
const channelId = getChannelId(thread, message);
|
|
6872
|
-
const channelName = channelId ? await resolveChannelName(thread) : void 0;
|
|
6765
|
+
const channelName = !options.execution && channelId ? await resolveChannelName(thread) : void 0;
|
|
6873
6766
|
const slackChannelType = resolveSlackChannelTypeFromMessage(message);
|
|
6874
6767
|
const slackConversation = resolveSlackConversationContext({
|
|
6875
6768
|
channelId,
|
|
6876
6769
|
channelName,
|
|
6877
6770
|
channelType: slackChannelType
|
|
6878
6771
|
});
|
|
6879
|
-
const destinationVisibility = conversationVisibilityFromSlackChannelType(slackChannelType);
|
|
6772
|
+
const destinationVisibility = options.execution?.destinationVisibility ?? conversationVisibilityFromSlackChannelType(slackChannelType);
|
|
6880
6773
|
const threadTs = getThreadTs(threadId);
|
|
6881
6774
|
const assistantThreadContext = getAssistantThreadContext(message);
|
|
6882
6775
|
const messageTs = getMessageTs(message);
|
|
@@ -6885,7 +6778,7 @@ function createReplyToThread(deps) {
|
|
|
6885
6778
|
"Slack reply execution"
|
|
6886
6779
|
);
|
|
6887
6780
|
const teamId = destination.teamId;
|
|
6888
|
-
const source = createSlackSource({
|
|
6781
|
+
const source = options.execution?.source ?? createSlackSource({
|
|
6889
6782
|
channelId: channelId ?? destination.channelId,
|
|
6890
6783
|
messageTs,
|
|
6891
6784
|
teamId,
|
|
@@ -6893,7 +6786,7 @@ function createReplyToThread(deps) {
|
|
|
6893
6786
|
type: destinationVisibility === "public" ? "pub" : "priv"
|
|
6894
6787
|
});
|
|
6895
6788
|
const slackActionToken = readSlackActionToken(message);
|
|
6896
|
-
const runId = getRunId(thread, message);
|
|
6789
|
+
const runId = options.execution?.dispatch?.id ?? getRunId(thread, message);
|
|
6897
6790
|
const conversationId = threadId ?? runId;
|
|
6898
6791
|
if (!conversationId) {
|
|
6899
6792
|
throw new Error("Slack reply execution requires a conversation id");
|
|
@@ -6935,7 +6828,7 @@ function createReplyToThread(deps) {
|
|
|
6935
6828
|
)
|
|
6936
6829
|
);
|
|
6937
6830
|
const effectiveUserText = currentText.userText;
|
|
6938
|
-
const credentialContext = resourceEventCredentialContext(message) ?? {
|
|
6831
|
+
const credentialContext = options.execution?.credentialContext ?? resourceEventCredentialContext(message) ?? {
|
|
6939
6832
|
actor: { type: "user", userId: message.author.userId }
|
|
6940
6833
|
};
|
|
6941
6834
|
const actor = "type" in credentialContext.actor ? await ensureSlackMessageActorIdentity(
|
|
@@ -6953,15 +6846,17 @@ function createReplyToThread(deps) {
|
|
|
6953
6846
|
options.explicitMention || message.isMention
|
|
6954
6847
|
),
|
|
6955
6848
|
queuedMessages: options.queuedMessages,
|
|
6849
|
+
channelConfiguration: options.execution?.channelConfiguration,
|
|
6956
6850
|
context: {
|
|
6957
6851
|
threadId,
|
|
6958
6852
|
actorId: slackActorId,
|
|
6959
6853
|
channelId,
|
|
6960
6854
|
runId
|
|
6961
|
-
}
|
|
6855
|
+
},
|
|
6856
|
+
skipBackfill: options.skipBackfill
|
|
6962
6857
|
});
|
|
6963
6858
|
const slackMessageTs = getSlackMessageTs(message);
|
|
6964
|
-
const turnId = buildDeterministicTurnId(message.id);
|
|
6859
|
+
const turnId = options.execution?.turnId ?? buildDeterministicTurnId(message.id);
|
|
6965
6860
|
const turnTraceContext = {
|
|
6966
6861
|
conversationId,
|
|
6967
6862
|
messageConversationId: threadId,
|
|
@@ -7109,6 +7004,11 @@ function createReplyToThread(deps) {
|
|
|
7109
7004
|
return drainParkedInputToEventLog(parkedPairs);
|
|
7110
7005
|
};
|
|
7111
7006
|
if (preparedState.userMessageAlreadyReplied) {
|
|
7007
|
+
const deliveredMessage = [...preparedState.conversation.messages].reverse().find(
|
|
7008
|
+
(candidate) => candidate.role === "assistant" && candidate.id.startsWith(`${turnId}:assistant:`) && candidate.meta?.replied === true
|
|
7009
|
+
);
|
|
7010
|
+
options.onTurnDeliveryAccepted?.(deliveredMessage?.meta?.slackTs);
|
|
7011
|
+
options.onTurnOutcome?.({ outcome: "completed" });
|
|
7112
7012
|
await persistThreadState(thread, {
|
|
7113
7013
|
conversation: preparedState.conversation
|
|
7114
7014
|
});
|
|
@@ -7185,7 +7085,7 @@ function createReplyToThread(deps) {
|
|
|
7185
7085
|
}
|
|
7186
7086
|
}
|
|
7187
7087
|
}
|
|
7188
|
-
const configReply = await maybeApplyProviderDefaultConfigRequest({
|
|
7088
|
+
const configReply = options.execution?.skipProviderDefaultConfig ? void 0 : await maybeApplyProviderDefaultConfigRequest({
|
|
7189
7089
|
channelConfiguration: preparedState.channelConfiguration,
|
|
7190
7090
|
actorId: actor?.userId,
|
|
7191
7091
|
text: effectiveUserText
|
|
@@ -7238,7 +7138,7 @@ function createReplyToThread(deps) {
|
|
|
7238
7138
|
preparedState.userMessageId
|
|
7239
7139
|
])
|
|
7240
7140
|
],
|
|
7241
|
-
surface: "slack",
|
|
7141
|
+
surface: options.execution?.surface ?? "slack",
|
|
7242
7142
|
turnId
|
|
7243
7143
|
});
|
|
7244
7144
|
}
|
|
@@ -7251,7 +7151,8 @@ function createReplyToThread(deps) {
|
|
|
7251
7151
|
sliceId: 1,
|
|
7252
7152
|
startedAtMs: turnStartedAtMs,
|
|
7253
7153
|
state: "running",
|
|
7254
|
-
surface: "slack",
|
|
7154
|
+
surface: options.execution?.surface ?? "slack",
|
|
7155
|
+
dispatchId: options.execution?.dispatch?.id,
|
|
7255
7156
|
actor: executionActor,
|
|
7256
7157
|
destination,
|
|
7257
7158
|
destinationVisibility,
|
|
@@ -7323,13 +7224,36 @@ function createReplyToThread(deps) {
|
|
|
7323
7224
|
let shouldPersistFailureState = true;
|
|
7324
7225
|
let runResultHandled = false;
|
|
7325
7226
|
let assistantMessageDelivered = false;
|
|
7227
|
+
let acceptedDeliveryId;
|
|
7326
7228
|
let lifecycleTerminalized = false;
|
|
7327
7229
|
let turnCompletionNotified = false;
|
|
7230
|
+
const recordDispatchOutcome = async (dispatchOutcome, state) => {
|
|
7231
|
+
const dispatchId = options.execution?.dispatch?.id;
|
|
7232
|
+
if (!dispatchId) {
|
|
7233
|
+
return;
|
|
7234
|
+
}
|
|
7235
|
+
await recordAgentTurnSessionSummary({
|
|
7236
|
+
channelName,
|
|
7237
|
+
conversationId,
|
|
7238
|
+
destination,
|
|
7239
|
+
destinationVisibility,
|
|
7240
|
+
dispatchId,
|
|
7241
|
+
dispatchOutcome,
|
|
7242
|
+
...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : {},
|
|
7243
|
+
sessionId: turnId,
|
|
7244
|
+
sliceId: 1,
|
|
7245
|
+
source,
|
|
7246
|
+
startedAtMs: message.metadata.dateSent.getTime(),
|
|
7247
|
+
state,
|
|
7248
|
+
surface: options.execution?.surface ?? "slack"
|
|
7249
|
+
});
|
|
7250
|
+
};
|
|
7328
7251
|
let latestArtifacts = preparedState.artifacts;
|
|
7329
7252
|
let assistantTitleArtifacts = {};
|
|
7330
7253
|
let agentContinueScheduleError;
|
|
7331
7254
|
let boundaryFailureCode = "agent_run_failed";
|
|
7332
7255
|
let finalizedFailureEventId;
|
|
7256
|
+
let terminalDispatchFailureOutcome;
|
|
7333
7257
|
const notifyTurnCompleted = async () => {
|
|
7334
7258
|
if (turnCompletionNotified) {
|
|
7335
7259
|
return;
|
|
@@ -7337,7 +7261,7 @@ function createReplyToThread(deps) {
|
|
|
7337
7261
|
await options.onTurnCompleted?.();
|
|
7338
7262
|
turnCompletionNotified = true;
|
|
7339
7263
|
};
|
|
7340
|
-
const deliverAssistantMessage = async (assistantMessage) => {
|
|
7264
|
+
const deliverAssistantMessage = async (assistantMessage, terminalDispatchOutcome) => {
|
|
7341
7265
|
if (!assistantMessage.text.trim()) {
|
|
7342
7266
|
return;
|
|
7343
7267
|
}
|
|
@@ -7345,12 +7269,12 @@ function createReplyToThread(deps) {
|
|
|
7345
7269
|
let slackTs;
|
|
7346
7270
|
await beforeFirstResponsePost();
|
|
7347
7271
|
try {
|
|
7348
|
-
if (channelId &&
|
|
7272
|
+
if (channelId && thread.adapter.name === "slack") {
|
|
7349
7273
|
slackTs = await sendSlackReply({
|
|
7350
7274
|
channelId,
|
|
7351
7275
|
conversationId,
|
|
7352
7276
|
text: assistantMessage.text,
|
|
7353
|
-
threadTs
|
|
7277
|
+
...threadTs ? { threadTs } : {}
|
|
7354
7278
|
});
|
|
7355
7279
|
} else {
|
|
7356
7280
|
for (const text of splitSlackReplyText(assistantMessage.text)) {
|
|
@@ -7379,6 +7303,8 @@ function createReplyToThread(deps) {
|
|
|
7379
7303
|
});
|
|
7380
7304
|
}
|
|
7381
7305
|
assistantMessageDelivered = true;
|
|
7306
|
+
acceptedDeliveryId = slackTs;
|
|
7307
|
+
options.onTurnDeliveryAccepted?.(slackTs);
|
|
7382
7308
|
const recordedMessageId = recordDeliveredAssistantMessage({
|
|
7383
7309
|
conversation: preparedState.conversation,
|
|
7384
7310
|
sessionId: turnId,
|
|
@@ -7410,6 +7336,35 @@ function createReplyToThread(deps) {
|
|
|
7410
7336
|
"Failed to persist an accepted Slack assistant message"
|
|
7411
7337
|
);
|
|
7412
7338
|
}
|
|
7339
|
+
if (slackTs && options.execution?.dispatch?.id) {
|
|
7340
|
+
try {
|
|
7341
|
+
await persistWithRetry(
|
|
7342
|
+
() => recordAgentTurnSessionSummary({
|
|
7343
|
+
channelName,
|
|
7344
|
+
conversationId,
|
|
7345
|
+
destination,
|
|
7346
|
+
destinationVisibility,
|
|
7347
|
+
dispatchId: options.execution?.dispatch?.id,
|
|
7348
|
+
resultMessageId: slackTs,
|
|
7349
|
+
sessionId: turnId,
|
|
7350
|
+
sliceId: 1,
|
|
7351
|
+
source,
|
|
7352
|
+
startedAtMs: message.metadata.dateSent.getTime(),
|
|
7353
|
+
...terminalDispatchOutcome ? { dispatchOutcome: terminalDispatchOutcome } : {},
|
|
7354
|
+
state: terminalDispatchOutcome ? "failed" : "running",
|
|
7355
|
+
surface: options.execution?.surface ?? "slack"
|
|
7356
|
+
})
|
|
7357
|
+
);
|
|
7358
|
+
} catch (error) {
|
|
7359
|
+
logException(
|
|
7360
|
+
error,
|
|
7361
|
+
"agent_turn_delivery_receipt_persist_failed",
|
|
7362
|
+
turnTraceContext,
|
|
7363
|
+
{},
|
|
7364
|
+
"Failed to persist accepted turn delivery receipt"
|
|
7365
|
+
);
|
|
7366
|
+
}
|
|
7367
|
+
}
|
|
7413
7368
|
boundaryFailureCode = "agent_run_failed";
|
|
7414
7369
|
};
|
|
7415
7370
|
try {
|
|
@@ -7570,19 +7525,20 @@ function createReplyToThread(deps) {
|
|
|
7570
7525
|
},
|
|
7571
7526
|
routing: {
|
|
7572
7527
|
credentialContext,
|
|
7573
|
-
actor,
|
|
7528
|
+
actor: options.execution ? executionActor : actor,
|
|
7574
7529
|
slackConversation,
|
|
7575
7530
|
source,
|
|
7576
7531
|
destination,
|
|
7577
7532
|
destinationVisibility,
|
|
7578
|
-
surface: "slack",
|
|
7533
|
+
surface: options.execution?.surface ?? "slack",
|
|
7534
|
+
dispatch: options.execution?.dispatch,
|
|
7579
7535
|
toolChannelId,
|
|
7580
7536
|
slackActionToken
|
|
7581
7537
|
},
|
|
7582
7538
|
policy: {
|
|
7583
7539
|
configuration: preparedState.configuration,
|
|
7584
7540
|
channelConfiguration: preparedState.channelConfiguration,
|
|
7585
|
-
authorizationFlowMode: message.author.isBot === true ? "disabled" : void 0,
|
|
7541
|
+
authorizationFlowMode: options.execution?.authorizationFlowMode ?? (message.author.isBot === true ? "disabled" : void 0),
|
|
7586
7542
|
turnDeadlineAtMs: getTurnRequestDeadline()?.deadlineAtMs
|
|
7587
7543
|
},
|
|
7588
7544
|
state: {
|
|
@@ -7624,6 +7580,8 @@ function createReplyToThread(deps) {
|
|
|
7624
7580
|
}
|
|
7625
7581
|
});
|
|
7626
7582
|
if (outcome.status === "awaiting_auth") {
|
|
7583
|
+
await recordDispatchOutcome("blocked", "failed");
|
|
7584
|
+
options.onTurnOutcome?.({ outcome: "blocked" });
|
|
7627
7585
|
if (!actor) {
|
|
7628
7586
|
const authFailureEventId = logException(
|
|
7629
7587
|
new Error(
|
|
@@ -7675,6 +7633,7 @@ function createReplyToThread(deps) {
|
|
|
7675
7633
|
return;
|
|
7676
7634
|
}
|
|
7677
7635
|
if (outcome.status === "suspended") {
|
|
7636
|
+
options.onTurnOutcome?.({ outcome: "awaiting_resume" });
|
|
7678
7637
|
if (options.shouldYield?.()) {
|
|
7679
7638
|
shouldPersistFailureState = false;
|
|
7680
7639
|
throw new CooperativeTurnYieldError();
|
|
@@ -7730,6 +7689,10 @@ function createReplyToThread(deps) {
|
|
|
7730
7689
|
finalizedFailureEventId = finalized.eventId;
|
|
7731
7690
|
await deliverAssistantMessage({ text: reply.text });
|
|
7732
7691
|
}
|
|
7692
|
+
const turnResult = reply.diagnostics.outcome === "success" ? { outcome: "completed" } : {
|
|
7693
|
+
errorMessage: reply.diagnostics.errorMessage ?? `Agent turn ended with ${reply.diagnostics.outcome}.`,
|
|
7694
|
+
outcome: "failed"
|
|
7695
|
+
};
|
|
7733
7696
|
runResultHandled = true;
|
|
7734
7697
|
shouldPersistFailureState = false;
|
|
7735
7698
|
boundaryFailureCode = "agent_run_failed";
|
|
@@ -7761,10 +7724,14 @@ function createReplyToThread(deps) {
|
|
|
7761
7724
|
source,
|
|
7762
7725
|
sessionId: turnId,
|
|
7763
7726
|
sliceId: 1,
|
|
7727
|
+
dispatchOutcome: reply.diagnostics.outcome === "success" ? "completed" : "failed",
|
|
7728
|
+
...options.execution?.dispatch && turnResult.errorMessage ? { errorMessage: turnResult.errorMessage } : {},
|
|
7729
|
+
...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : {},
|
|
7764
7730
|
messages: reply.piMessages,
|
|
7765
7731
|
modelId: reply.diagnostics.modelId,
|
|
7766
7732
|
actor: executionActor,
|
|
7767
|
-
surface: "slack"
|
|
7733
|
+
surface: options.execution?.surface ?? "slack",
|
|
7734
|
+
dispatchId: options.execution?.dispatch?.id
|
|
7768
7735
|
});
|
|
7769
7736
|
} else if (conversationId) {
|
|
7770
7737
|
await recordAgentTurnSessionSummary({
|
|
@@ -7780,6 +7747,10 @@ function createReplyToThread(deps) {
|
|
|
7780
7747
|
destination,
|
|
7781
7748
|
destinationVisibility,
|
|
7782
7749
|
source,
|
|
7750
|
+
surface: options.execution?.surface ?? "slack",
|
|
7751
|
+
dispatchId: options.execution?.dispatch?.id,
|
|
7752
|
+
dispatchOutcome: reply.diagnostics.outcome === "success" ? "completed" : "failed",
|
|
7753
|
+
...acceptedDeliveryId ? { resultMessageId: acceptedDeliveryId } : {},
|
|
7783
7754
|
traceId: getActiveTraceId()
|
|
7784
7755
|
});
|
|
7785
7756
|
}
|
|
@@ -7816,6 +7787,7 @@ function createReplyToThread(deps) {
|
|
|
7816
7787
|
}
|
|
7817
7788
|
preparedState.conversation = completedState.conversation;
|
|
7818
7789
|
persistedAtLeastOnce = true;
|
|
7790
|
+
options.onTurnOutcome?.(turnResult);
|
|
7819
7791
|
if (!lifecycleTerminalized && conversationId) {
|
|
7820
7792
|
if (reply.diagnostics.outcome === "success") {
|
|
7821
7793
|
await deps.services.turnLifecycle.complete({
|
|
@@ -7901,6 +7873,9 @@ function createReplyToThread(deps) {
|
|
|
7901
7873
|
shouldPersistFailureState = true;
|
|
7902
7874
|
const classifiedFailure = getConversationTurnBoundaryError(error);
|
|
7903
7875
|
const failureCause = classifiedFailure?.cause ?? error;
|
|
7876
|
+
if (failureCause instanceof AuthorizationFlowDisabledError || failureCause instanceof PluginCredentialFailureError) {
|
|
7877
|
+
terminalDispatchFailureOutcome = "blocked";
|
|
7878
|
+
}
|
|
7904
7879
|
const failureCode = classifiedFailure?.failureCode ?? boundaryFailureCode;
|
|
7905
7880
|
const failureEventId = classifiedFailure?.eventId ?? logException(
|
|
7906
7881
|
failureCause,
|
|
@@ -7914,8 +7889,31 @@ function createReplyToThread(deps) {
|
|
|
7914
7889
|
after: latestArtifacts
|
|
7915
7890
|
});
|
|
7916
7891
|
if (createdCanvasUrl) {
|
|
7892
|
+
const dispatchOutcome = terminalDispatchFailureOutcome ?? "failed";
|
|
7893
|
+
const errorMessage = failureCause instanceof Error ? failureCause.message : "Agent turn failed after creating a canvas";
|
|
7917
7894
|
const recoveryText = buildCanvasRecoveryReply(createdCanvasUrl);
|
|
7918
|
-
await deliverAssistantMessage(
|
|
7895
|
+
await deliverAssistantMessage(
|
|
7896
|
+
{ text: recoveryText },
|
|
7897
|
+
dispatchOutcome
|
|
7898
|
+
);
|
|
7899
|
+
if (conversationId) {
|
|
7900
|
+
const sessionRecord = await getAgentTurnSessionRecord(
|
|
7901
|
+
conversationId,
|
|
7902
|
+
turnId
|
|
7903
|
+
);
|
|
7904
|
+
if (sessionRecord) {
|
|
7905
|
+
await failAgentTurnSessionRecord({
|
|
7906
|
+
conversationId,
|
|
7907
|
+
expectedVersion: sessionRecord.version,
|
|
7908
|
+
sessionId: turnId,
|
|
7909
|
+
errorMessage
|
|
7910
|
+
});
|
|
7911
|
+
}
|
|
7912
|
+
}
|
|
7913
|
+
options.onTurnOutcome?.({
|
|
7914
|
+
errorMessage,
|
|
7915
|
+
outcome: dispatchOutcome
|
|
7916
|
+
});
|
|
7919
7917
|
markTurnClosed({
|
|
7920
7918
|
conversation: preparedState.conversation,
|
|
7921
7919
|
nowMs: Date.now(),
|
|
@@ -7970,6 +7968,11 @@ function createReplyToThread(deps) {
|
|
|
7970
7968
|
destination,
|
|
7971
7969
|
destinationVisibility,
|
|
7972
7970
|
source,
|
|
7971
|
+
surface: options.execution?.surface ?? "slack",
|
|
7972
|
+
dispatchId: options.execution?.dispatch?.id,
|
|
7973
|
+
...options.execution?.dispatch && terminalDispatchFailureOutcome ? {
|
|
7974
|
+
dispatchOutcome: terminalDispatchFailureOutcome
|
|
7975
|
+
} : {},
|
|
7973
7976
|
traceId: getActiveTraceId()
|
|
7974
7977
|
});
|
|
7975
7978
|
const sessionRecord = await getAgentTurnSessionRecord(
|
|
@@ -8171,12 +8174,14 @@ function createPrepareTurnState(deps) {
|
|
|
8171
8174
|
const conversation = coerceThreadConversationState(existingState);
|
|
8172
8175
|
const conversationId = args.context.threadId ?? args.context.runId;
|
|
8173
8176
|
await hydrateConversationMessages({ conversation, conversationId });
|
|
8174
|
-
const channelConfiguration = getChannelConfigurationService(args.thread);
|
|
8177
|
+
const channelConfiguration = args.channelConfiguration ?? getChannelConfigurationService(args.thread);
|
|
8175
8178
|
const configuration = await channelConfiguration.resolveValues();
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8179
|
+
if (!args.skipBackfill) {
|
|
8180
|
+
await seedConversationBackfill(args.thread, conversation, {
|
|
8181
|
+
messageId: args.message.id,
|
|
8182
|
+
messageCreatedAtMs: args.message.metadata.dateSent.getTime()
|
|
8183
|
+
});
|
|
8184
|
+
}
|
|
8180
8185
|
for (const queued of args.queuedMessages ?? []) {
|
|
8181
8186
|
const queuedMessage = toConversationMessage({
|
|
8182
8187
|
entry: queued.message,
|
|
@@ -8243,6 +8248,94 @@ function createPrepareTurnState(deps) {
|
|
|
8243
8248
|
};
|
|
8244
8249
|
}
|
|
8245
8250
|
|
|
8251
|
+
// src/chat/slack/dispatch-turn.ts
|
|
8252
|
+
import { Message as Message3, ThreadImpl as ThreadImpl3 } from "chat";
|
|
8253
|
+
function createSlackDispatchTurnRunner(options) {
|
|
8254
|
+
return async function runSlackDispatchTurn(dispatch, hooks) {
|
|
8255
|
+
const state = getStateAdapter();
|
|
8256
|
+
await state.connect();
|
|
8257
|
+
const conversationId = getDispatchConversationId(dispatch);
|
|
8258
|
+
const adapter = options.getSlackAdapter();
|
|
8259
|
+
const message = new Message3({
|
|
8260
|
+
id: getDispatchInputMessageId(dispatch.id),
|
|
8261
|
+
threadId: conversationId,
|
|
8262
|
+
text: dispatch.input,
|
|
8263
|
+
attachments: [],
|
|
8264
|
+
formatted: {
|
|
8265
|
+
type: "root",
|
|
8266
|
+
children: [
|
|
8267
|
+
{
|
|
8268
|
+
type: "paragraph",
|
|
8269
|
+
children: [{ type: "text", value: dispatch.input }]
|
|
8270
|
+
}
|
|
8271
|
+
]
|
|
8272
|
+
},
|
|
8273
|
+
metadata: {
|
|
8274
|
+
dateSent: new Date(dispatch.createdAtMs),
|
|
8275
|
+
edited: false
|
|
8276
|
+
},
|
|
8277
|
+
raw: {
|
|
8278
|
+
channel: dispatch.destination.channelId,
|
|
8279
|
+
team: dispatch.destination.teamId
|
|
8280
|
+
},
|
|
8281
|
+
author: {
|
|
8282
|
+
userId: dispatch.actor.name,
|
|
8283
|
+
userName: dispatch.actor.name,
|
|
8284
|
+
fullName: dispatch.actor.name,
|
|
8285
|
+
isBot: true,
|
|
8286
|
+
isMe: false
|
|
8287
|
+
}
|
|
8288
|
+
});
|
|
8289
|
+
message.isMention = true;
|
|
8290
|
+
const thread = new ThreadImpl3({
|
|
8291
|
+
adapter,
|
|
8292
|
+
stateAdapter: state,
|
|
8293
|
+
id: conversationId,
|
|
8294
|
+
channelId: dispatch.destination.channelId,
|
|
8295
|
+
channelVisibility: dispatch.destinationVisibility === "public" ? "workspace" : "private",
|
|
8296
|
+
currentMessage: message,
|
|
8297
|
+
initialMessage: message,
|
|
8298
|
+
isDM: dispatch.destination.channelId.startsWith("D"),
|
|
8299
|
+
isSubscribedContext: true
|
|
8300
|
+
});
|
|
8301
|
+
let outcome;
|
|
8302
|
+
let errorMessage;
|
|
8303
|
+
let resultMessageTs;
|
|
8304
|
+
const routing = buildDispatchRoutingContext(dispatch);
|
|
8305
|
+
await options.replyToThread(thread, message, {
|
|
8306
|
+
ack: hooks.ack,
|
|
8307
|
+
destination: dispatch.destination,
|
|
8308
|
+
execution: {
|
|
8309
|
+
authorizationFlowMode: "disabled",
|
|
8310
|
+
channelConfiguration: options.getChannelConfiguration(
|
|
8311
|
+
dispatch.destination.channelId
|
|
8312
|
+
),
|
|
8313
|
+
credentialContext: routing.credentialContext,
|
|
8314
|
+
destinationVisibility: dispatch.destinationVisibility,
|
|
8315
|
+
dispatch: routing.dispatch,
|
|
8316
|
+
skipProviderDefaultConfig: true,
|
|
8317
|
+
source: dispatch.source,
|
|
8318
|
+
surface: routing.surface,
|
|
8319
|
+
turnId: getDispatchTurnId(dispatch.id)
|
|
8320
|
+
},
|
|
8321
|
+
onTurnDeliveryAccepted: (messageId) => {
|
|
8322
|
+
resultMessageTs = messageId;
|
|
8323
|
+
},
|
|
8324
|
+
onTurnOutcome: (result) => {
|
|
8325
|
+
outcome = result.outcome;
|
|
8326
|
+
errorMessage = result.errorMessage;
|
|
8327
|
+
},
|
|
8328
|
+
shouldYield: hooks.shouldYield,
|
|
8329
|
+
skipBackfill: true
|
|
8330
|
+
});
|
|
8331
|
+
return {
|
|
8332
|
+
...errorMessage ? { errorMessage } : {},
|
|
8333
|
+
...outcome ? { outcome } : {},
|
|
8334
|
+
...resultMessageTs ? { resultMessageTs } : {}
|
|
8335
|
+
};
|
|
8336
|
+
};
|
|
8337
|
+
}
|
|
8338
|
+
|
|
8246
8339
|
// src/chat/app/factory.ts
|
|
8247
8340
|
async function persistAssistantContextChannelId(args) {
|
|
8248
8341
|
const currentArtifacts = coerceThreadArtifactsState(
|
|
@@ -8287,7 +8380,7 @@ function createSlackRuntime(options) {
|
|
|
8287
8380
|
resolveUserAttachments: services.visionContext.resolveUserAttachments,
|
|
8288
8381
|
services: services.replyExecutor
|
|
8289
8382
|
});
|
|
8290
|
-
|
|
8383
|
+
const runtime = createSlackTurnRuntime({
|
|
8291
8384
|
assistantUserName: botConfig.userName,
|
|
8292
8385
|
cancelEventSubscriptions: cancelSubscriptions,
|
|
8293
8386
|
modelId: standardModelId(botConfig),
|
|
@@ -8406,6 +8499,14 @@ function createSlackRuntime(options) {
|
|
|
8406
8499
|
});
|
|
8407
8500
|
}
|
|
8408
8501
|
});
|
|
8502
|
+
return {
|
|
8503
|
+
...runtime,
|
|
8504
|
+
runDispatchTurn: createSlackDispatchTurnRunner({
|
|
8505
|
+
getChannelConfiguration: getChannelConfigurationServiceById,
|
|
8506
|
+
getSlackAdapter: options.getSlackAdapter,
|
|
8507
|
+
replyToThread
|
|
8508
|
+
})
|
|
8509
|
+
};
|
|
8409
8510
|
}
|
|
8410
8511
|
|
|
8411
8512
|
// src/chat/slack/adapter.ts
|
|
@@ -8464,7 +8565,7 @@ async function failSessionRecordBestEffort(args) {
|
|
|
8464
8565
|
);
|
|
8465
8566
|
}
|
|
8466
8567
|
}
|
|
8467
|
-
async function persistFailedReplyState(sessionRecord) {
|
|
8568
|
+
async function persistFailedReplyState(sessionRecord, errorMessage = "Paused agent run failed while continuing") {
|
|
8468
8569
|
const currentState = await getPersistedThreadState(
|
|
8469
8570
|
sessionRecord.conversationId
|
|
8470
8571
|
);
|
|
@@ -8484,7 +8585,7 @@ async function persistFailedReplyState(sessionRecord) {
|
|
|
8484
8585
|
});
|
|
8485
8586
|
await failSessionRecordBestEffort({
|
|
8486
8587
|
sessionRecord,
|
|
8487
|
-
errorMessage
|
|
8588
|
+
errorMessage
|
|
8488
8589
|
});
|
|
8489
8590
|
await persistThreadStateById(sessionRecord.conversationId, {
|
|
8490
8591
|
conversation
|
|
@@ -8542,22 +8643,35 @@ async function failUnresumableContinuation(args) {
|
|
|
8542
8643
|
sessionId: args.summary.sessionId,
|
|
8543
8644
|
errorMessage: args.errorMessage
|
|
8544
8645
|
});
|
|
8646
|
+
if (args.summary.dispatchId) {
|
|
8647
|
+
await recordAgentTurnSessionSummary({
|
|
8648
|
+
actor: args.summary.actor,
|
|
8649
|
+
conversationId: args.conversationId,
|
|
8650
|
+
destination: args.summary.destination,
|
|
8651
|
+
dispatchId: args.summary.dispatchId,
|
|
8652
|
+
dispatchOutcome: "failed",
|
|
8653
|
+
sessionId: args.summary.sessionId,
|
|
8654
|
+
sliceId: args.summary.sliceId,
|
|
8655
|
+
source: args.summary.source,
|
|
8656
|
+
state: "failed",
|
|
8657
|
+
surface: args.summary.surface
|
|
8658
|
+
});
|
|
8659
|
+
}
|
|
8545
8660
|
}
|
|
8546
8661
|
async function continueSlackAgentRun(payload, options, runOptions = {}) {
|
|
8547
8662
|
const thread = parseSlackThreadId(payload.conversationId);
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
}
|
|
8663
|
+
const destination = requireSlackDestination(
|
|
8664
|
+
payload.destination,
|
|
8665
|
+
"Agent continuation"
|
|
8666
|
+
);
|
|
8553
8667
|
const scheduleAgentContinue2 = options.scheduleAgentContinue ?? scheduleAgentContinue;
|
|
8554
8668
|
const resumeTurn = options.resumeTurn ?? resumeSlackTurn;
|
|
8555
8669
|
return await resumeTurn({
|
|
8556
8670
|
messageText: "",
|
|
8557
8671
|
conversationId: payload.conversationId,
|
|
8558
8672
|
turnId: payload.sessionId,
|
|
8559
|
-
channelId: thread.channelId,
|
|
8560
|
-
threadTs: thread.threadTs,
|
|
8673
|
+
channelId: thread?.channelId ?? destination.channelId,
|
|
8674
|
+
...thread?.threadTs ? { threadTs: thread.threadTs } : {},
|
|
8561
8675
|
lockKey: payload.conversationId,
|
|
8562
8676
|
agentRunner: options.agentRunner,
|
|
8563
8677
|
scheduleSessionCompletedPluginTasks: options.scheduleSessionCompletedPluginTasks,
|
|
@@ -8581,8 +8695,14 @@ async function continueSlackAgentRun(payload, options, runOptions = {}) {
|
|
|
8581
8695
|
conversationId: payload.conversationId
|
|
8582
8696
|
});
|
|
8583
8697
|
const artifacts = coerceThreadArtifactsState(currentState);
|
|
8584
|
-
const
|
|
8585
|
-
|
|
8698
|
+
const dispatchId = activeSessionRecord.dispatchId ?? options.routingContext?.dispatch?.id;
|
|
8699
|
+
const dispatchUserMessage = dispatchId ? conversation.messages.find(
|
|
8700
|
+
(message) => message.role === "user" && options.inputMessageIds?.includes(message.id)
|
|
8701
|
+
) : void 0;
|
|
8702
|
+
const userMessage = getTurnUserMessage(conversation, payload.sessionId) ?? dispatchUserMessage;
|
|
8703
|
+
const systemActor = activeSessionRecord.actor?.platform === "system" ? activeSessionRecord.actor : void 0;
|
|
8704
|
+
const userActorId = userMessage?.author?.userId;
|
|
8705
|
+
if (!userMessage || !systemActor && !userActorId) {
|
|
8586
8706
|
throw new Error(
|
|
8587
8707
|
`Unable to locate the persisted user message for agent continuation session "${payload.sessionId}"`
|
|
8588
8708
|
);
|
|
@@ -8591,27 +8711,24 @@ async function continueSlackAgentRun(payload, options, runOptions = {}) {
|
|
|
8591
8711
|
return false;
|
|
8592
8712
|
}
|
|
8593
8713
|
const channelConfiguration = getChannelConfigurationServiceById(
|
|
8594
|
-
|
|
8714
|
+
destination.channelId
|
|
8595
8715
|
);
|
|
8596
|
-
const conversationContext = buildConversationContext(conversation, {
|
|
8716
|
+
const conversationContext = dispatchId ? void 0 : buildConversationContext(conversation, {
|
|
8597
8717
|
excludeMessageId: userMessage.id
|
|
8598
8718
|
});
|
|
8599
8719
|
const sandboxRef = getPersistedSandboxState(currentState);
|
|
8600
|
-
const destination = requireSlackDestination(
|
|
8601
|
-
payload.destination,
|
|
8602
|
-
"Slack continuation"
|
|
8603
|
-
);
|
|
8604
|
-
const systemActor = activeSessionRecord.actor?.platform === "system" ? activeSessionRecord.actor : void 0;
|
|
8605
8720
|
let actor;
|
|
8606
8721
|
let credentialContext;
|
|
8607
|
-
if (
|
|
8722
|
+
if (options.routingContext?.credentialContext) {
|
|
8723
|
+
credentialContext = options.routingContext.credentialContext;
|
|
8724
|
+
} else if (systemActor) {
|
|
8608
8725
|
credentialContext = { actor: systemActor };
|
|
8609
8726
|
} else {
|
|
8610
8727
|
actor = await resolveContinuationActor({
|
|
8611
8728
|
conversationId: payload.conversationId,
|
|
8612
8729
|
sessionRecordActor: activeSessionRecord.actor,
|
|
8613
8730
|
teamId: destination.teamId,
|
|
8614
|
-
userId:
|
|
8731
|
+
userId: userActorId
|
|
8615
8732
|
});
|
|
8616
8733
|
if (!actor) {
|
|
8617
8734
|
await failStrandedSessionWithFallback({
|
|
@@ -8640,27 +8757,49 @@ async function continueSlackAgentRun(payload, options, runOptions = {}) {
|
|
|
8640
8757
|
const turnMessages = activeSessionRecord.turnStartMessageIndex === void 0 ? [] : activeSessionRecord.piMessages.slice(
|
|
8641
8758
|
activeSessionRecord.turnStartMessageIndex
|
|
8642
8759
|
);
|
|
8760
|
+
const recordDispatchOutcome = async (dispatchOutcome) => {
|
|
8761
|
+
const dispatchId2 = options.routingContext?.dispatch?.id;
|
|
8762
|
+
if (!dispatchId2) {
|
|
8763
|
+
return;
|
|
8764
|
+
}
|
|
8765
|
+
await recordAgentTurnSessionSummary({
|
|
8766
|
+
conversationId: payload.conversationId,
|
|
8767
|
+
destination: payload.destination,
|
|
8768
|
+
destinationVisibility: options.routingContext?.destinationVisibility,
|
|
8769
|
+
dispatchId: dispatchId2,
|
|
8770
|
+
dispatchOutcome,
|
|
8771
|
+
sessionId: payload.sessionId,
|
|
8772
|
+
sliceId: activeSessionRecord.sliceId,
|
|
8773
|
+
source: activeSessionRecord.source,
|
|
8774
|
+
state: "failed",
|
|
8775
|
+
surface: options.routingContext?.surface ?? "slack"
|
|
8776
|
+
});
|
|
8777
|
+
};
|
|
8643
8778
|
return {
|
|
8644
8779
|
messageText: userMessage.text,
|
|
8780
|
+
sliceId: activeSessionRecord.sliceId,
|
|
8645
8781
|
messageTs: getTurnUserSlackMessageTs(userMessage),
|
|
8646
8782
|
inputMessageIds: [userMessage.id],
|
|
8647
8783
|
initialStatus: latestReportedProgress(turnMessages),
|
|
8648
8784
|
replyContext: {
|
|
8649
8785
|
input: {
|
|
8650
|
-
conversationContext,
|
|
8786
|
+
...conversationContext ? { conversationContext } : {},
|
|
8651
8787
|
// Pi history is SQL-authoritative: the resumed run reads its
|
|
8652
|
-
//
|
|
8653
|
-
|
|
8788
|
+
// exact dispatch session so unrelated conversation input cannot
|
|
8789
|
+
// gain system authority. Interactive turns retain their merged
|
|
8790
|
+
// projection so queued steering remains visible.
|
|
8791
|
+
piMessages: dispatchId ? activeSessionRecord.piMessages : await loadProjection({
|
|
8654
8792
|
conversationId: payload.conversationId
|
|
8655
8793
|
}),
|
|
8656
8794
|
...getTurnUserReplyAttachmentContext(userMessage)
|
|
8657
8795
|
},
|
|
8658
8796
|
routing: {
|
|
8797
|
+
...options.routingContext,
|
|
8659
8798
|
credentialContext,
|
|
8660
|
-
...actor ? { actor } : {},
|
|
8799
|
+
...options.routingContext?.actor ?? actor ? { actor: options.routingContext?.actor ?? actor } : {},
|
|
8661
8800
|
destination: payload.destination,
|
|
8662
8801
|
source: activeSessionRecord.source,
|
|
8663
|
-
toolChannelId: artifacts.assistantContextChannelId ??
|
|
8802
|
+
toolChannelId: artifacts.assistantContextChannelId ?? destination.channelId
|
|
8664
8803
|
},
|
|
8665
8804
|
policy: {
|
|
8666
8805
|
channelConfiguration
|
|
@@ -8686,8 +8825,12 @@ async function continueSlackAgentRun(payload, options, runOptions = {}) {
|
|
|
8686
8825
|
reply
|
|
8687
8826
|
});
|
|
8688
8827
|
},
|
|
8689
|
-
onFailure: async () => {
|
|
8690
|
-
await persistFailedReplyState(
|
|
8828
|
+
onFailure: async (error) => {
|
|
8829
|
+
await persistFailedReplyState(
|
|
8830
|
+
activeSessionRecord,
|
|
8831
|
+
error instanceof Error ? error.message : String(error)
|
|
8832
|
+
);
|
|
8833
|
+
await recordDispatchOutcome("failed");
|
|
8691
8834
|
},
|
|
8692
8835
|
onPostDeliveryCommitFailure: async () => {
|
|
8693
8836
|
await failAgentTurnSessionRecord({
|
|
@@ -8696,12 +8839,14 @@ async function continueSlackAgentRun(payload, options, runOptions = {}) {
|
|
|
8696
8839
|
sessionId: activeSessionRecord.sessionId,
|
|
8697
8840
|
errorMessage: "Continued agent reply was delivered but completion state did not persist"
|
|
8698
8841
|
});
|
|
8842
|
+
await recordDispatchOutcome("failed");
|
|
8699
8843
|
},
|
|
8700
8844
|
onAuthPause: async () => {
|
|
8701
8845
|
await persistAuthPauseTurnState({
|
|
8702
8846
|
sessionId: payload.sessionId,
|
|
8703
8847
|
threadStateId: payload.conversationId
|
|
8704
8848
|
});
|
|
8849
|
+
await recordDispatchOutcome("blocked");
|
|
8705
8850
|
logWarn(
|
|
8706
8851
|
"agent_continue_reparked_for_auth",
|
|
8707
8852
|
{},
|
|
@@ -8739,6 +8884,20 @@ async function failStrandedSessionWithFallback(args) {
|
|
|
8739
8884
|
sessionId: args.sessionRecord.sessionId,
|
|
8740
8885
|
errorMessage: args.errorMessage
|
|
8741
8886
|
});
|
|
8887
|
+
if (args.sessionRecord.dispatchId) {
|
|
8888
|
+
await recordAgentTurnSessionSummary({
|
|
8889
|
+
actor: args.sessionRecord.actor,
|
|
8890
|
+
conversationId: args.conversationId,
|
|
8891
|
+
destination: args.sessionRecord.destination,
|
|
8892
|
+
dispatchId: args.sessionRecord.dispatchId,
|
|
8893
|
+
dispatchOutcome: "failed",
|
|
8894
|
+
sessionId: args.sessionRecord.sessionId,
|
|
8895
|
+
sliceId: args.sessionRecord.sliceId,
|
|
8896
|
+
source: args.sessionRecord.source,
|
|
8897
|
+
state: "failed",
|
|
8898
|
+
surface: args.sessionRecord.surface
|
|
8899
|
+
});
|
|
8900
|
+
}
|
|
8742
8901
|
const currentState = await getPersistedThreadState(args.conversationId);
|
|
8743
8902
|
const conversation = coerceThreadConversationState(currentState);
|
|
8744
8903
|
await hydrateConversationMessages({
|
|
@@ -8757,10 +8916,6 @@ async function failStrandedSessionWithFallback(args) {
|
|
|
8757
8916
|
updateConversationStats
|
|
8758
8917
|
});
|
|
8759
8918
|
await persistThreadStateById(args.conversationId, { conversation });
|
|
8760
|
-
const thread = parseSlackThreadId(args.conversationId);
|
|
8761
|
-
if (!thread) {
|
|
8762
|
-
return;
|
|
8763
|
-
}
|
|
8764
8919
|
const eventName = "agent_turn_stranded_session_failed";
|
|
8765
8920
|
const eventId = logException(
|
|
8766
8921
|
new Error(args.errorMessage),
|
|
@@ -8772,9 +8927,14 @@ async function failStrandedSessionWithFallback(args) {
|
|
|
8772
8927
|
},
|
|
8773
8928
|
"Stranded running agent session terminally failed"
|
|
8774
8929
|
);
|
|
8930
|
+
const thread = parseSlackThreadId(args.conversationId);
|
|
8931
|
+
const channelId = thread?.channelId ?? requireSlackDestination(
|
|
8932
|
+
args.sessionRecord.destination,
|
|
8933
|
+
"Stranded agent continuation"
|
|
8934
|
+
).channelId;
|
|
8775
8935
|
await postSlackMessage({
|
|
8776
|
-
channelId
|
|
8777
|
-
threadTs: thread.threadTs,
|
|
8936
|
+
channelId,
|
|
8937
|
+
...thread?.threadTs ? { threadTs: thread.threadTs } : {},
|
|
8778
8938
|
text: buildTurnFailureResponse(
|
|
8779
8939
|
requireTurnFailureEventId(eventId, eventName)
|
|
8780
8940
|
)
|
|
@@ -8984,21 +9144,41 @@ function createProductionConversationWorkOptions(options) {
|
|
|
8984
9144
|
getSlackAdapter: getProductionSlackAdapter,
|
|
8985
9145
|
services
|
|
8986
9146
|
});
|
|
8987
|
-
|
|
9147
|
+
const queue = getVercelConversationWorkQueue();
|
|
9148
|
+
const slackWorker = createSlackConversationWorker({
|
|
9149
|
+
getSlackAdapter: getProductionSlackAdapter,
|
|
8988
9150
|
conversationStore,
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
9151
|
+
resumeAwaitingContinuation: async (conversationId, runOptions) => await resumeAwaitingSlackContinuation(
|
|
9152
|
+
conversationId,
|
|
9153
|
+
{
|
|
9154
|
+
agentRunner,
|
|
9155
|
+
scheduleSessionCompletedPluginTasks: services.replyExecutor?.scheduleSessionCompletedPluginTasks
|
|
9156
|
+
},
|
|
9157
|
+
runOptions
|
|
9158
|
+
),
|
|
9159
|
+
runtime
|
|
9160
|
+
});
|
|
9161
|
+
const dispatchWorker = createAgentDispatchConversationWorker({
|
|
9162
|
+
resumeTurn: async (dispatch, hooks) => {
|
|
9163
|
+
await resumeAwaitingSlackContinuation(
|
|
9164
|
+
getDispatchConversationId(dispatch),
|
|
8995
9165
|
{
|
|
8996
9166
|
agentRunner,
|
|
9167
|
+
inputMessageIds: getDispatchInputMessageIds(dispatch.id),
|
|
9168
|
+
routingContext: buildDispatchRoutingContext(dispatch),
|
|
8997
9169
|
scheduleSessionCompletedPluginTasks: services.replyExecutor?.scheduleSessionCompletedPluginTasks
|
|
8998
9170
|
},
|
|
8999
|
-
|
|
9000
|
-
)
|
|
9001
|
-
|
|
9171
|
+
{ shouldYield: hooks.shouldYield }
|
|
9172
|
+
);
|
|
9173
|
+
},
|
|
9174
|
+
runTurn: runtime.runDispatchTurn
|
|
9175
|
+
});
|
|
9176
|
+
return {
|
|
9177
|
+
conversationStore,
|
|
9178
|
+
queue,
|
|
9179
|
+
run: createAgentDispatchWorkRouter({
|
|
9180
|
+
dispatchWorker,
|
|
9181
|
+
fallbackWorker: slackWorker
|
|
9002
9182
|
})
|
|
9003
9183
|
};
|
|
9004
9184
|
}
|
|
@@ -9501,7 +9681,9 @@ async function createApp(options) {
|
|
|
9501
9681
|
return receiveLocalOAuthCredential(c.req.raw);
|
|
9502
9682
|
});
|
|
9503
9683
|
app.post("/api/internal/agent-dispatch", (c) => {
|
|
9504
|
-
return POST(c.req.raw, waitUntil, {
|
|
9684
|
+
return POST(c.req.raw, waitUntil, {
|
|
9685
|
+
conversationWorkQueue: getVercelConversationWorkQueue()
|
|
9686
|
+
});
|
|
9505
9687
|
});
|
|
9506
9688
|
let agentContinuePOST;
|
|
9507
9689
|
let pluginTaskPOST;
|