@sentry/junior 0.265.0 → 0.267.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.
Files changed (76) hide show
  1. package/dist/{actor-tlxzqaRe.js → actor-DZSokpwj.js} +8 -1
  2. package/dist/{adapter-cUkPH3Om.js → adapter-BybMEezj.js} +2 -2
  3. package/dist/{admin-Cf0rDmWV.js → admin-DOs1_EiW.js} +1 -1
  4. package/dist/{agent-runner-C6MwHJEs.js → agent-runner-DgNATN4D.js} +874 -228
  5. package/dist/api/schema.d.ts +202 -23
  6. package/dist/api/schema.js +6 -5
  7. package/dist/api.d.ts +6 -1
  8. package/dist/api.js +130 -1205
  9. package/dist/{app-Csbi9dW7.d.ts → app-DAB3fzyp.d.ts} +21 -3
  10. package/dist/app.d.ts +2 -2
  11. package/dist/app.js +1611 -353
  12. package/dist/{assistant-reply-UPA8vKlC.js → assistant-reply-CT7VcXQe.js} +1 -1
  13. package/dist/{automation-2RbKzpMT.js → automation-CmeFBSEh.js} +42 -14
  14. package/dist/{briefs-DRjx8kQ9.js → briefs-CWKjka7v.js} +5 -5
  15. package/dist/{catalog-runtime-D3qbJlYz.js → catalog-runtime-cMlAI5LY.js} +1 -1
  16. package/dist/cli/chat.js +15 -15
  17. package/dist/cli/check.js +3 -3
  18. package/dist/cli/init.js +0 -1
  19. package/dist/cli/main.js +3 -3
  20. package/dist/cli/plugins.js +4 -4
  21. package/dist/cli/snapshot-create.js +4 -4
  22. package/dist/cli/upgrade.d.ts +1 -1
  23. package/dist/cli/upgrade.js +3 -3
  24. package/dist/{client-DX34VNtH.js → client-BjgbMawj.js} +2 -2
  25. package/dist/{config-BayQ9hHH.js → config-CdB6yJj5.js} +32 -10
  26. package/dist/{connections-mpN5Lsnj.js → connections-RE_65jzK.js} +13 -12
  27. package/dist/{conversation-B0baEwoA.js → conversation-DSo310Ps.js} +14 -3
  28. package/dist/{conversation-privacy-frjQCJa9.js → conversation-privacy-BzgJC5k0.js} +1 -1
  29. package/dist/{credential-sync-CZmyDNdR.js → credential-sync-DhEGEpgq.js} +15 -15
  30. package/dist/{credentialed-C6pKlB1p.js → credentialed-CIGwoLU2.js} +2913 -1026
  31. package/dist/{db-BnRqQ7G5.d.ts → db-Bg-zC3r5.d.ts} +1 -1
  32. package/dist/{executor-CAr7xQr2.js → executor-VYrx_K4w.js} +7 -4
  33. package/dist/{history-AY8rs-FR.js → history-C84mlAwO.js} +2 -1
  34. package/dist/{images-B2yA3HWE.js → images-CecjXAAz.js} +3 -3
  35. package/dist/instrumentation.js +6 -2
  36. package/dist/{job-queue-Cjo6Yg_b.js → job-queue-C0BZSa7r.js} +128 -3
  37. package/dist/{web-input-Y6aXvmfa.js → list-1XQP-o6K.js} +1374 -71
  38. package/dist/{logging-D2QBriUo.js → logging-DuZ1A4ab.js} +11 -3
  39. package/dist/{message-C9ymnpBS.js → message-NKSbbnA6.js} +4 -4
  40. package/dist/nitro.d.ts +1 -1
  41. package/dist/nitro.js +3 -3
  42. package/dist/{oauth-callback-server-BtMf3TPZ.js → oauth-callback-server-qAHLPtml.js} +2 -2
  43. package/dist/{prompt-CvaDdSX3.js → prompt-BW9jFsqZ.js} +1 -1
  44. package/dist/{registry-COTeZ2cV.js → registry-FQutO926.js} +2 -2
  45. package/dist/{resolve-g9PKz9pH.js → resolve-Bk4yeuHy.js} +3 -3
  46. package/dist/{result-COnC42GR.js → result-B9e9ATOU.js} +9 -9
  47. package/dist/{runner-BnTNS7x_.js → runner-C3S2ObH-.js} +11 -11
  48. package/dist/{sandbox-egress-signals-C_IIdLFk.js → sandbox-egress-signals-DqpGzzMB.js} +1 -1
  49. package/dist/{schema-D5Amlzkp.js → schema-X7Fj4fv1.js} +3 -2
  50. package/dist/{service-account-CwvToidT.js → service-account-BQasl-qq.js} +3 -3
  51. package/dist/{service-account-YdYftwV2.js → service-account-Do30HUNv.js} +3 -3
  52. package/dist/{session-stop-CpCUy1ZJ.js → session-stop-DkOjt46_.js} +45 -128
  53. package/dist/{skills-Bb2KaJ3m.js → skills-CQHrwfOa.js} +2 -2
  54. package/dist/{spawn-mbTcrLb3.js → spawn-BHuqN14f.js} +2 -2
  55. package/dist/{store-CqRnaZSq.js → store-6EetVP-m.js} +3 -3
  56. package/dist/{store-CG0VJyye.d.ts → store-B8CABbmR.d.ts} +10 -2
  57. package/dist/store-DvNZIsCr.js +295 -0
  58. package/dist/{task-queue-8XjDIORF.js → task-queue-B0DLgd2P.js} +10 -5
  59. package/dist/{task-runner-6mLmG6g_.js → task-runner-B-PD_JOj.js} +26 -14
  60. package/dist/{tasks-DGbrur6x.js → tasks-0ilmuCux.js} +1440 -46
  61. package/dist/{thread-state-D45lz-oC.js → thread-state-DRmdJ2QN.js} +5 -5
  62. package/dist/{turn-failure-response-CaEPLSBn.js → turn-failure-response-G11cs2gE.js} +9 -9
  63. package/dist/{turn-lifecycle-C35IyRGp.js → turn-lifecycle-OzLIwvi1.js} +2 -2
  64. package/dist/{turn-session-routing-BKx6rfVd.js → turn-session-routing-Dyut9l7Y.js} +1 -1
  65. package/dist/user-input-request-BDFBdF8r.js +116 -0
  66. package/dist/{validation-C0X3CX51.js → validation-CxEmt1id.js} +1 -1
  67. package/dist/{version-BPHgJwnm.js → version-Bapedn4A.js} +1 -1
  68. package/dist/version.js +1 -1
  69. package/dist/{viewer-B2wXNspU.js → viewer-DrN3oC2X.js} +22 -11
  70. package/dist/{work-DMbqK0kT.js → work-DUQoNcqP.js} +52 -51
  71. package/dist/{worker-B-rDJ3XN.js → worker-D1l4Ugk0.js} +30 -19
  72. package/migrations/0064_any_resource_event_triggers.sql +2 -0
  73. package/migrations/meta/0064_snapshot.json +3505 -0
  74. package/migrations/meta/_journal.json +7 -0
  75. package/package.json +20 -5
  76. package/dist/store-DdT74Lvq.js +0 -1424
@@ -1,22 +1,22 @@
1
- import { a as completeText, f as unwrapCurrentInstruction, p as escapeXml, s as resolveGatewayModel, y as isProviderRetryError } from "./client-DX34VNtH.js";
2
- import { n as botConfig } from "./config-BayQ9hHH.js";
3
- import { V as toOptionalString, f as logException, m as logWarn, o as getActiveTraceId, t as buildTurnFailureResponse, y as setSpanAttributes, z as isRecord } from "./logging-D2QBriUo.js";
4
- import { c as toStoredSlackActor, g as parseSlackMessageTs } from "./actor-tlxzqaRe.js";
5
- import { S as bindProviderConversation, b as startLocationThread, d as getConversationStore, f as getDb, h as withConversationEventLock, l as getConversationEventStore, m as createSqlConversationEventStore, p as getSqlExecutor, w as addAgentTurnUsage } from "./viewer-B2wXNspU.js";
1
+ import { a as completeText, f as unwrapCurrentInstruction, p as escapeXml, s as resolveGatewayModel, y as isProviderRetryError } from "./client-BjgbMawj.js";
2
+ import { i as getChatConfig, n as botConfig } from "./config-CdB6yJj5.js";
3
+ import { B as isRecord, H as toOptionalString, V as toOptionalNumber, b as setSpanAttributes, h as logWarn, o as getActiveTraceContext, p as logException, t as buildTurnFailureResponse } from "./logging-DuZ1A4ab.js";
4
+ import { c as toStoredSlackActor, p as parseSlackMessageTs, s as parseStoredSlackActor } from "./actor-DZSokpwj.js";
5
+ import { B as parseDestination, C as bindProviderConversation, H as sameDestination, T as addAgentTurnUsage, f as getConversationStore, g as withConversationEventLock, h as createSqlConversationEventStore, m as getSqlExecutor, p as getDb, u as getConversationEventStore, x as startLocationThread } from "./viewer-DrN3oC2X.js";
6
6
  import { d as juniorDestinations } from "./agent-invocations-CxKldAeB.js";
7
- import { a as juniorAutomationRuns, l as juniorLocationConfigurations, o as juniorAutomationVersions, s as juniorAutomations } from "./executor-CAr7xQr2.js";
8
- import { d as contextProvenance, f as instructionActors, n as agentHistoryItemSchema, p as instructionProvenanceFor } from "./history-AY8rs-FR.js";
9
- import { f as credentialContextForActor, h as credentialSubjectSchema } from "./registry-COTeZ2cV.js";
10
- import { n as pluginCatalogRuntime } from "./catalog-runtime-D3qbJlYz.js";
11
- import { An as createPluginLogger, Q as parseSandboxEgressAuthRequiredSignal, ar as agentsInstructionsUpdatedEvent, at as formatProviderLabel, ct as startOAuthFlow, ir as JUNIOR_NATIVE_EVENT_NAMESPACE, mn as ToolInputError, nr as createPluginCache, nt as resolveSandboxEgressProviderForHost, on as sleep, or as authenticationLinkedEvent, p as getPlugins, sr as authenticationUnlinkedEvent, t as executeCredentialedEgressRequest } from "./credentialed-C6pKlB1p.js";
12
- import { i as MUTATION_LOCK_TTL_MS, o as fenceLock, r as getStateAdapter, s as withLock } from "./adapter-cUkPH3Om.js";
13
- import { N as messageCardRef } from "./conversation-B0baEwoA.js";
14
- import { A as eventAutomationSchema, E as scheduledAutomationSchema, S as automationVersionSchema } from "./automation-2RbKzpMT.js";
7
+ import { a as juniorAutomationRuns, l as juniorLocationConfigurations, o as juniorAutomationVersions, s as juniorAutomations } from "./executor-VYrx_K4w.js";
8
+ import { d as contextProvenance, f as instructionActors, n as agentHistoryItemSchema, p as instructionProvenanceFor } from "./history-C84mlAwO.js";
9
+ import { f as credentialContextForActor, h as credentialSubjectSchema } from "./registry-FQutO926.js";
10
+ import { n as pluginCatalogRuntime } from "./catalog-runtime-cMlAI5LY.js";
11
+ import { H as startOAuthFlow, Nn as createPluginLogger, St as resolveSandboxEgressProviderForHost, ar as createPluginCache, cn as sleep$1, cr as agentsInstructionsUpdatedEvent, dn as JUNIOR_THREAD_STATE_TTL_MS, gn as ToolInputError, lr as authenticationLinkedEvent, p as getPlugins, sr as JUNIOR_NATIVE_EVENT_NAMESPACE, t as executeCredentialedEgressRequest, ur as authenticationUnlinkedEvent, vt as parseSandboxEgressAuthRequiredSignal, z as formatProviderLabel } from "./credentialed-CIGwoLU2.js";
12
+ import { a as MUTATION_LOCK_TTL_MS, c as withLock, i as getStateAdapter, r as getDefaultRedisStateAdapterFor, s as fenceLock } from "./adapter-BybMEezj.js";
13
+ import { L as messageCardRef } from "./conversation-DSo310Ps.js";
14
+ import { A as eventAutomationSchema, E as scheduledAutomationSchema, S as automationVersionSchema } from "./automation-CmeFBSEh.js";
15
15
  import { isDeepStrictEqual } from "node:util";
16
16
  import { z } from "zod";
17
17
  import { createHash, randomUUID } from "node:crypto";
18
- import { and, asc, desc, eq, inArray, ne, or, sql } from "drizzle-orm";
19
- import { actorSchema, eventMatches, sourceSchema } from "@sentry/junior-plugin-api";
18
+ import { and, asc, desc, eq, inArray, isNull, ne, or, sql } from "drizzle-orm";
19
+ import { actorSchema, anyResourceMatchFields, describeEventTrigger, destinationSchema, eventMatchSchema, eventMatches, eventTriggerChoices, eventTriggerProblems, normalizeEventIdentifier, normalizeEventMatch, sourceSchema } from "@sentry/junior-plugin-api";
20
20
  // src/chat/configuration/defaults.ts
21
21
  let installDefaults = {};
22
22
  function cloneDefaults(defaults) {
@@ -52,7 +52,7 @@ function authorizationObservationMessage(data, createdAtMs) {
52
52
  role: "user",
53
53
  content: [{
54
54
  type: "text",
55
- text: `${label} completed for provider "${data.provider}". Continue the request that waited for it and retry the provider operation. If the person dropped that request since, do not continue it; say only that ${data.provider} is connected.`
55
+ text: `${label} completed for provider "${data.provider}". Continue the newest request that waited for it and retry the provider operation. If a later message from the person dropped that request, do not continue it; say only that ${data.provider} is connected.`
56
56
  }],
57
57
  timestamp: createdAtMs
58
58
  });
@@ -1038,7 +1038,7 @@ async function persistWithRetry(persist) {
1038
1038
  return;
1039
1039
  } catch (error) {
1040
1040
  lastError = error;
1041
- if (attempt < PERSIST_ATTEMPTS) await sleep(attempt * 100);
1041
+ if (attempt < PERSIST_ATTEMPTS) await sleep$1(attempt * 100);
1042
1042
  }
1043
1043
  throw lastError;
1044
1044
  }
@@ -1105,6 +1105,1161 @@ function buildTurnLimitResponse(eventId) {
1105
1105
  function buildTurnErrorResponse(error, eventId) {
1106
1106
  return isTurnExecutionLimitExceededError(error) ? buildTurnLimitResponse(eventId) : buildTurnFailureResponse(eventId);
1107
1107
  }
1108
+ // src/chat/task-execution/state.ts
1109
+ /**
1110
+ * Expiring conversation mailbox and execution index store.
1111
+ *
1112
+ * The conversation record owns pending inbound work, execution status, and the
1113
+ * lease. `conversation:active` feeds heartbeat recovery and contains every
1114
+ * non-idle conversation until the lease/status path makes it idle.
1115
+ * `conversation:by-activity` supports legacy SQL import only; SQL owns durable
1116
+ * conversation metadata in configured runtime.
1117
+ */
1118
+ const CONVERSATION_PREFIX = "junior:conversation:v2";
1119
+ const CONVERSATION_SCHEMA_VERSION = 2;
1120
+ const CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH = 1e4;
1121
+ const CONVERSATION_INDEX_LOCK_TTL_MS = 1e4;
1122
+ const CONVERSATION_INDEX_LOCK_WAIT_MS = 2e3;
1123
+ const CONVERSATION_INDEX_LOCK_RETRY_MS = 25;
1124
+ const CONVERSATION_MUTATION_LOCK_TTL_MS = 1e4;
1125
+ const CONVERSATION_MUTATION_WAIT_MS = 1e4;
1126
+ const CONVERSATION_MUTATION_RETRY_MS = 25;
1127
+ var InvalidConversationRecordError = class extends Error {
1128
+ constructor(conversationId) {
1129
+ super(`Conversation record is invalid for ${conversationId}`);
1130
+ this.name = "InvalidConversationRecordError";
1131
+ }
1132
+ };
1133
+ /** Return whether an error means a stored conversation record failed validation. */
1134
+ function isInvalidConversationRecordError(error) {
1135
+ return error instanceof InvalidConversationRecordError;
1136
+ }
1137
+ var ConversationMutationFencedError = class extends Error {
1138
+ constructor(conversationId) {
1139
+ super(`Conversation mutation lock was lost before write for ${conversationId}`);
1140
+ this.name = "ConversationMutationFencedError";
1141
+ }
1142
+ };
1143
+ const CONVERSATION_BY_ACTIVITY_INDEX_KEY = `${CONVERSATION_PREFIX}:by-activity`;
1144
+ const CONVERSATION_ACTIVE_INDEX_KEY = `${CONVERSATION_PREFIX}:active`;
1145
+ const CONVERSATION_WORK_LEASE_TTL_MS = 9e4;
1146
+ const CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15e3;
1147
+ const CONVERSATION_WORK_STALE_ENQUEUE_MS = 6e4;
1148
+ const inboundMessageSourceSchema = z.enum([
1149
+ "api",
1150
+ "internal",
1151
+ "local",
1152
+ "plugin",
1153
+ "event",
1154
+ "scheduler",
1155
+ "slack",
1156
+ "web"
1157
+ ]);
1158
+ const agentInputSchema = z.object({
1159
+ attachments: z.array(z.unknown()).optional(),
1160
+ authorId: z.string().min(1).optional(),
1161
+ metadata: z.record(z.string(), z.unknown()).optional(),
1162
+ text: z.string()
1163
+ }).strict().refine((input) => input.text.trim() || input.attachments?.length, { message: "agent input requires text or attachments" });
1164
+ /** Durable delivery modes for pending inbound mailbox work. */
1165
+ const inboundMessageDeliverySchema = z.enum(["defer", "interrupt"]);
1166
+ /** Sentry trace propagation headers. A malformed value is dropped, not rejected. */
1167
+ const traceContextSchema = z.object({
1168
+ baggage: z.string().optional(),
1169
+ sentryTrace: z.string().min(1)
1170
+ }).optional().catch(void 0);
1171
+ /** Redis mailbox shape kept for deployed workers that still require this field. */
1172
+ const storedInboundMessageSchema = z.object({
1173
+ attemptCount: z.number().finite().optional(),
1174
+ conversationId: z.string().refine((value) => value.trim().length > 0),
1175
+ createdAtMs: z.number().finite(),
1176
+ delivery: inboundMessageDeliverySchema,
1177
+ destination: destinationSchema.optional(),
1178
+ inboundMessageId: z.string().refine((value) => value.trim().length > 0),
1179
+ injectedAtMs: z.number().finite().optional(),
1180
+ input: agentInputSchema,
1181
+ receivedAtMs: z.number().finite(),
1182
+ source: inboundMessageSourceSchema,
1183
+ traceContext: traceContextSchema
1184
+ }).strict().extend({ publishExternally: z.boolean() });
1185
+ function conversationKey(conversationId) {
1186
+ return `${CONVERSATION_PREFIX}:${conversationId}`;
1187
+ }
1188
+ function conversationStopKey(conversationId) {
1189
+ return `${CONVERSATION_PREFIX}:stop:${conversationId}`;
1190
+ }
1191
+ function indexLockKey(indexKey) {
1192
+ return `${indexKey}:lock`;
1193
+ }
1194
+ function mutationLockKey(conversationId) {
1195
+ return `${CONVERSATION_PREFIX}:mutation:${conversationId}`;
1196
+ }
1197
+ function now() {
1198
+ return Date.now();
1199
+ }
1200
+ function compareMessages(left, right) {
1201
+ return left.createdAtMs - right.createdAtMs || left.receivedAtMs - right.receivedAtMs || left.inboundMessageId.localeCompare(right.inboundMessageId);
1202
+ }
1203
+ function inputHasAttachments(input) {
1204
+ return Array.isArray(input.attachments) && input.attachments.length > 0;
1205
+ }
1206
+ /**
1207
+ * Upgrade a still-pending payload when a duplicate source event carries file
1208
+ * attachments the stored copy lacks (Slack `app_mention`/`message` twins),
1209
+ * keeping the stored ordering timestamps and delivery attempts.
1210
+ */
1211
+ function upgradedPendingMessage(stored, duplicate) {
1212
+ const promotesInterrupt = stored.delivery === "defer" && duplicate.delivery === "interrupt";
1213
+ const delivery = promotesInterrupt ? "interrupt" : stored.delivery;
1214
+ const input = promotesInterrupt ? {
1215
+ ...duplicate.input,
1216
+ ...inputHasAttachments(duplicate.input) || !inputHasAttachments(stored.input) ? void 0 : { attachments: stored.input.attachments }
1217
+ } : inputHasAttachments(duplicate.input) && !inputHasAttachments(stored.input) ? duplicate.input : stored.input;
1218
+ if (input === stored.input && delivery === stored.delivery) return stored;
1219
+ return {
1220
+ ...stored,
1221
+ delivery,
1222
+ input
1223
+ };
1224
+ }
1225
+ function compareIndexDescending(left, right) {
1226
+ return right.score - left.score || right.conversationId.localeCompare(left.conversationId);
1227
+ }
1228
+ function compareIndexAscending(left, right) {
1229
+ return left.score - right.score || left.conversationId.localeCompare(right.conversationId);
1230
+ }
1231
+ function uniqueStrings(values) {
1232
+ return [...new Set(values)];
1233
+ }
1234
+ function normalizeSource(value) {
1235
+ const parsed = inboundMessageSourceSchema.safeParse(value);
1236
+ return parsed.success ? parsed.data : void 0;
1237
+ }
1238
+ function normalizeExecutionStatus(value) {
1239
+ if (value === "paused") return value;
1240
+ if (value === "failed" || value === "idle" || value === "pending" || value === "running") return value;
1241
+ }
1242
+ function normalizeMessage(value) {
1243
+ const parsed = storedInboundMessageSchema.safeParse(value);
1244
+ if (!parsed.success) return;
1245
+ const { publishExternally: _publishExternally, traceContext, ...message } = parsed.data;
1246
+ return traceContext ? {
1247
+ ...message,
1248
+ traceContext
1249
+ } : message;
1250
+ }
1251
+ function storeMessage(message, publishExternally) {
1252
+ return {
1253
+ ...message,
1254
+ publishExternally
1255
+ };
1256
+ }
1257
+ /** Whether this is the final attempt before an unacked message is dead-lettered. */
1258
+ function isFinalAttempt(message) {
1259
+ return (message.attemptCount ?? 0) >= 4;
1260
+ }
1261
+ function normalizeActor(value) {
1262
+ return parseStoredSlackActor(value);
1263
+ }
1264
+ function normalizeLease(value) {
1265
+ if (!isRecord(value)) return;
1266
+ const token = toOptionalString(value.token);
1267
+ const acquiredAtMs = toOptionalNumber(value.acquiredAtMs);
1268
+ const lastCheckInAtMs = toOptionalNumber(value.lastCheckInAtMs);
1269
+ const expiresAtMs = toOptionalNumber(value.expiresAtMs);
1270
+ if (!token || typeof acquiredAtMs !== "number" || typeof lastCheckInAtMs !== "number" || typeof expiresAtMs !== "number") return;
1271
+ return {
1272
+ token,
1273
+ acquiredAtMs,
1274
+ lastCheckInAtMs,
1275
+ expiresAtMs
1276
+ };
1277
+ }
1278
+ function normalizeStop(value) {
1279
+ if (!isRecord(value)) return;
1280
+ const inboundMessageIds = Array.isArray(value.inboundMessageIds) ? uniqueStrings(value.inboundMessageIds.filter((id) => typeof id === "string")) : [];
1281
+ const runId = toOptionalString(value.runId);
1282
+ return runId ? {
1283
+ inboundMessageIds,
1284
+ runId
1285
+ } : void 0;
1286
+ }
1287
+ /** Decode execution state and repair idle records that still own work. */
1288
+ function normalizeExecution(conversationId, value) {
1289
+ if (!isRecord(value)) return;
1290
+ const status = normalizeExecutionStatus(value.status);
1291
+ if (!status) return;
1292
+ const pendingMessages = [];
1293
+ if (Array.isArray(value.pendingMessages)) {
1294
+ for (const rawMessage of value.pendingMessages) {
1295
+ const message = normalizeMessage(rawMessage);
1296
+ if (!message || message.conversationId !== conversationId) return;
1297
+ if (message.injectedAtMs !== void 0) continue;
1298
+ pendingMessages.push(message);
1299
+ }
1300
+ pendingMessages.sort(compareMessages);
1301
+ }
1302
+ const inboundMessageIds = Array.isArray(value.inboundMessageIds) ? uniqueStrings(value.inboundMessageIds.map((id) => typeof id === "string" ? id : void 0).filter((id) => Boolean(id))) : [];
1303
+ const lease = normalizeLease(value.lease);
1304
+ const runId = toOptionalString(value.runId);
1305
+ return {
1306
+ status: status === "idle" && lease ? "running" : status === "idle" && pendingMessages.length > 0 ? "pending" : status,
1307
+ inboundMessageIds: uniqueStrings([...inboundMessageIds, ...pendingMessages.map((message) => message.inboundMessageId)]),
1308
+ pendingCount: pendingMessages.length,
1309
+ pendingMessages,
1310
+ lease,
1311
+ lastCheckpointAtMs: toOptionalNumber(value.lastCheckpointAtMs),
1312
+ lastEnqueuedAtMs: toOptionalNumber(value.lastEnqueuedAtMs),
1313
+ lastProgressAtMs: toOptionalNumber(value.lastProgressAtMs),
1314
+ retryCount: toOptionalNumber(value.retryCount),
1315
+ runId,
1316
+ updatedAtMs: toOptionalNumber(value.updatedAtMs)
1317
+ };
1318
+ }
1319
+ /** Decode current conversation records. */
1320
+ function normalizeConversation(conversationId, value) {
1321
+ if (!isRecord(value) || value.schemaVersion !== CONVERSATION_SCHEMA_VERSION) return;
1322
+ const storedConversationId = toOptionalString(value.conversationId);
1323
+ const createdAtMs = toOptionalNumber(value.createdAtMs);
1324
+ const lastActivityAtMs = toOptionalNumber(value.lastActivityAtMs);
1325
+ const updatedAtMs = toOptionalNumber(value.updatedAtMs);
1326
+ const execution = normalizeExecution(conversationId, value.execution);
1327
+ const destination = value.destination === void 0 ? void 0 : parseDestination(value.destination);
1328
+ if (storedConversationId !== conversationId || typeof createdAtMs !== "number" || typeof lastActivityAtMs !== "number" || typeof updatedAtMs !== "number" || !execution || value.destination !== void 0 && !destination) return;
1329
+ if (execution.pendingMessages.length > 0 && execution.pendingMessages.some((message) => {
1330
+ if (!message.destination) return false;
1331
+ return !destination || !sameDestination(message.destination, destination);
1332
+ })) return;
1333
+ return {
1334
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
1335
+ conversationId,
1336
+ createdAtMs,
1337
+ lastActivityAtMs,
1338
+ updatedAtMs,
1339
+ execution,
1340
+ ...destination ? { destination } : void 0,
1341
+ ...toOptionalString(value.title) ? { title: toOptionalString(value.title) } : void 0,
1342
+ ...toOptionalString(value.channelName) ? { channelName: toOptionalString(value.channelName) } : void 0,
1343
+ ...normalizeActor(value.actor) ? { actor: normalizeActor(value.actor) } : void 0,
1344
+ ...normalizeSource(value.source) ? { source: normalizeSource(value.source) } : void 0
1345
+ };
1346
+ }
1347
+ function emptyConversation(args) {
1348
+ return {
1349
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
1350
+ conversationId: args.conversationId,
1351
+ createdAtMs: args.nowMs,
1352
+ lastActivityAtMs: args.nowMs,
1353
+ updatedAtMs: args.nowMs,
1354
+ ...args.destination ? { destination: args.destination } : void 0,
1355
+ ...args.source ? { source: args.source } : void 0,
1356
+ execution: {
1357
+ status: "idle",
1358
+ inboundMessageIds: [],
1359
+ pendingCount: 0,
1360
+ pendingMessages: [],
1361
+ updatedAtMs: args.nowMs
1362
+ }
1363
+ };
1364
+ }
1365
+ function isLeaseActive(lease, nowMs) {
1366
+ return Boolean(lease && lease.expiresAtMs > nowMs);
1367
+ }
1368
+ function pendingMessages(conversation) {
1369
+ return [...conversation.execution.pendingMessages].sort(compareMessages);
1370
+ }
1371
+ function isRunnableStatus(status) {
1372
+ return status !== "failed" && status !== "idle";
1373
+ }
1374
+ function hasRunnableWork(conversation) {
1375
+ return isRunnableStatus(conversation.execution.status) || pendingMessages(conversation).length > 0;
1376
+ }
1377
+ function executionWithPendingMessages(execution, pending) {
1378
+ const pendingMessages = [...pending].sort(compareMessages);
1379
+ const status = execution.status === "idle" && execution.lease ? "running" : execution.status === "idle" && pendingMessages.length > 0 ? "pending" : execution.status;
1380
+ return {
1381
+ ...execution,
1382
+ status,
1383
+ inboundMessageIds: uniqueStrings([...execution.inboundMessageIds, ...pendingMessages.map((message) => message.inboundMessageId)]),
1384
+ pendingMessages,
1385
+ pendingCount: pendingMessages.length
1386
+ };
1387
+ }
1388
+ function withExecutionUpdate(conversation, execution, nowMs) {
1389
+ return {
1390
+ ...conversation,
1391
+ updatedAtMs: nowMs,
1392
+ execution: {
1393
+ ...executionWithPendingMessages(execution, execution.pendingMessages),
1394
+ updatedAtMs: nowMs
1395
+ }
1396
+ };
1397
+ }
1398
+ async function getConnectedState(stateAdapter) {
1399
+ const state = stateAdapter ?? getStateAdapter();
1400
+ await state.connect();
1401
+ return state;
1402
+ }
1403
+ async function sleep(ms) {
1404
+ await new Promise((resolve) => {
1405
+ setTimeout(resolve, ms).unref?.();
1406
+ });
1407
+ }
1408
+ async function withIndexLock(state, indexKey, callback) {
1409
+ const startedAtMs = now();
1410
+ let lock;
1411
+ while (true) {
1412
+ lock = await state.acquireLock(indexLockKey(indexKey), CONVERSATION_INDEX_LOCK_TTL_MS);
1413
+ if (lock) break;
1414
+ if (now() - startedAtMs >= CONVERSATION_INDEX_LOCK_WAIT_MS) throw new Error(`Could not acquire conversation index lock for ${indexKey}`);
1415
+ await sleep(CONVERSATION_INDEX_LOCK_RETRY_MS);
1416
+ }
1417
+ try {
1418
+ return await callback();
1419
+ } finally {
1420
+ await state.releaseLock(lock);
1421
+ }
1422
+ }
1423
+ function normalizeIndexEntry(value) {
1424
+ if (!isRecord(value)) return;
1425
+ const conversationId = toOptionalString(value.conversationId);
1426
+ const score = toOptionalNumber(value.score);
1427
+ if (!conversationId || typeof score !== "number") return;
1428
+ return {
1429
+ conversationId,
1430
+ score
1431
+ };
1432
+ }
1433
+ function uniqueIndexEntries(value) {
1434
+ if (!Array.isArray(value)) return [];
1435
+ const entries = /* @__PURE__ */ new Map();
1436
+ for (const item of value) {
1437
+ const entry = normalizeIndexEntry(item);
1438
+ if (!entry) continue;
1439
+ const existing = entries.get(entry.conversationId);
1440
+ if (!existing || entry.score > existing.score) entries.set(entry.conversationId, entry);
1441
+ }
1442
+ return [...entries.values()];
1443
+ }
1444
+ function retainedIndexEntries(indexKey, entries) {
1445
+ if (indexKey === "junior:conversation:v2:by-activity") return entries.sort(compareIndexDescending).slice(0, CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH);
1446
+ if (indexKey === "junior:conversation:v2:active") return entries.sort(compareIndexAscending);
1447
+ throw new Error(`Unknown conversation index ${indexKey}`);
1448
+ }
1449
+ function redisIndexKey(indexKey) {
1450
+ const prefix = getChatConfig().state.keyPrefix;
1451
+ return [...prefix ? [prefix] : [], indexKey].join(":");
1452
+ }
1453
+ function parseRedisIndexEntries(values) {
1454
+ if (!Array.isArray(values)) return [];
1455
+ const entries = [];
1456
+ for (let index = 0; index < values.length; index += 2) {
1457
+ const conversationId = toOptionalString(values[index]);
1458
+ const score = typeof values[index + 1] === "number" ? values[index + 1] : Number(values[index + 1]);
1459
+ if (!conversationId || !Number.isFinite(score)) continue;
1460
+ entries.push({
1461
+ conversationId,
1462
+ score
1463
+ });
1464
+ }
1465
+ return entries;
1466
+ }
1467
+ function redisConversationIndexStore(client) {
1468
+ const upsertBoundedActivityScript = `
1469
+ redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
1470
+ redis.call("PEXPIRE", KEYS[1], ARGV[3])
1471
+ local extra = redis.call("ZCARD", KEYS[1]) - tonumber(ARGV[4])
1472
+ if extra > 0 then
1473
+ redis.call("ZREMRANGEBYRANK", KEYS[1], 0, extra - 1)
1474
+ end
1475
+ return 1
1476
+ `;
1477
+ return {
1478
+ async list(args) {
1479
+ const key = redisIndexKey(args.indexKey);
1480
+ const limit = args.limit;
1481
+ const offset = Math.max(0, args.offset ?? 0);
1482
+ if (limit === 0) return [];
1483
+ return parseRedisIndexEntries(args.scoreMax !== void 0 ? await client.sendCommand([
1484
+ "ZRANGEBYSCORE",
1485
+ key,
1486
+ "-inf",
1487
+ String(args.scoreMax),
1488
+ "WITHSCORES",
1489
+ ...limit !== void 0 || offset > 0 ? [
1490
+ "LIMIT",
1491
+ String(offset),
1492
+ String(limit ?? 1e9)
1493
+ ] : []
1494
+ ]) : await client.sendCommand([
1495
+ args.order === "asc" ? "ZRANGE" : "ZREVRANGE",
1496
+ key,
1497
+ String(offset),
1498
+ String(limit === void 0 ? -1 : offset + Math.max(0, limit - 1)),
1499
+ "WITHSCORES"
1500
+ ]));
1501
+ },
1502
+ async remove(args) {
1503
+ await client.sendCommand([
1504
+ "ZREM",
1505
+ redisIndexKey(args.indexKey),
1506
+ args.conversationId
1507
+ ]);
1508
+ },
1509
+ async upsert(args) {
1510
+ const key = redisIndexKey(args.indexKey);
1511
+ if (args.indexKey === "junior:conversation:v2:by-activity") {
1512
+ await client.sendCommand([
1513
+ "EVAL",
1514
+ upsertBoundedActivityScript,
1515
+ "1",
1516
+ key,
1517
+ String(args.score),
1518
+ args.conversationId,
1519
+ String(JUNIOR_THREAD_STATE_TTL_MS),
1520
+ String(CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH)
1521
+ ]);
1522
+ return;
1523
+ }
1524
+ if (args.indexKey === "junior:conversation:v2:active") {
1525
+ await client.sendCommand([
1526
+ "ZADD",
1527
+ key,
1528
+ String(args.score),
1529
+ args.conversationId
1530
+ ]);
1531
+ await client.sendCommand([
1532
+ "PEXPIRE",
1533
+ key,
1534
+ String(JUNIOR_THREAD_STATE_TTL_MS)
1535
+ ]);
1536
+ return;
1537
+ }
1538
+ throw new Error(`Unknown conversation index ${args.indexKey}`);
1539
+ }
1540
+ };
1541
+ }
1542
+ function emulatedConversationIndexStore(state) {
1543
+ const readIndex = async (indexKey) => uniqueIndexEntries(await state.get(indexKey));
1544
+ const writeIndex = async (indexKey, entries) => {
1545
+ await state.set(indexKey, entries, JUNIOR_THREAD_STATE_TTL_MS);
1546
+ };
1547
+ return {
1548
+ async list(args) {
1549
+ const entries = (await readIndex(args.indexKey)).filter((entry) => args.scoreMax === void 0 ? true : entry.score <= args.scoreMax).sort(args.order === "asc" ? compareIndexAscending : compareIndexDescending);
1550
+ const offset = Math.max(0, args.offset ?? 0);
1551
+ return entries.slice(offset, args.limit === void 0 ? entries.length : offset + args.limit);
1552
+ },
1553
+ async remove(args) {
1554
+ await withIndexLock(state, args.indexKey, async () => {
1555
+ const entries = await readIndex(args.indexKey);
1556
+ const next = entries.filter((entry) => entry.conversationId !== args.conversationId);
1557
+ if (next.length === entries.length) return;
1558
+ await writeIndex(args.indexKey, next);
1559
+ });
1560
+ },
1561
+ async upsert(args) {
1562
+ await withIndexLock(state, args.indexKey, async () => {
1563
+ const withoutCurrent = (await readIndex(args.indexKey)).filter((entry) => entry.conversationId !== args.conversationId);
1564
+ const next = retainedIndexEntries(args.indexKey, [...withoutCurrent, {
1565
+ conversationId: args.conversationId,
1566
+ score: args.score
1567
+ }]);
1568
+ await writeIndex(args.indexKey, next);
1569
+ });
1570
+ }
1571
+ };
1572
+ }
1573
+ async function getConversationIndexStore(state) {
1574
+ const redisStateAdapter = await getDefaultRedisStateAdapterFor(state);
1575
+ if (redisStateAdapter) return redisConversationIndexStore(redisStateAdapter.getClient());
1576
+ return emulatedConversationIndexStore(state);
1577
+ }
1578
+ async function upsertIndexEntry(args) {
1579
+ await (await getConversationIndexStore(args.state)).upsert({
1580
+ conversationId: args.conversationId,
1581
+ indexKey: args.indexKey,
1582
+ score: args.score
1583
+ });
1584
+ }
1585
+ async function removeIndexEntry(args) {
1586
+ await (await getConversationIndexStore(args.state)).remove({
1587
+ conversationId: args.conversationId,
1588
+ indexKey: args.indexKey
1589
+ });
1590
+ }
1591
+ async function acquireMutationLock(state, conversationId) {
1592
+ const startedAtMs = now();
1593
+ while (true) {
1594
+ const lock = await state.acquireLock(mutationLockKey(conversationId), CONVERSATION_MUTATION_LOCK_TTL_MS);
1595
+ if (lock) return lock;
1596
+ if (now() - startedAtMs >= CONVERSATION_MUTATION_WAIT_MS) throw new Error(`Could not acquire conversation mutation lock for ${conversationId}`);
1597
+ await sleep(CONVERSATION_MUTATION_RETRY_MS);
1598
+ }
1599
+ }
1600
+ async function withConversationMutation(args, callback) {
1601
+ const state = await getConnectedState(args.state);
1602
+ const lock = await acquireMutationLock(state, args.conversationId);
1603
+ try {
1604
+ return await callback(state, lock);
1605
+ } finally {
1606
+ await state.releaseLock(lock);
1607
+ }
1608
+ }
1609
+ async function readConversation(state, conversationId) {
1610
+ const raw = await state.get(conversationKey(conversationId));
1611
+ if (raw == null) return;
1612
+ const conversation = normalizeConversation(conversationId, raw);
1613
+ if (!conversation) throw new InvalidConversationRecordError(conversationId);
1614
+ return conversation;
1615
+ }
1616
+ /**
1617
+ * Persist a conversation and refresh its reporting and active-recovery indexes.
1618
+ *
1619
+ * Conversation writes are lock-serialized blind whole-record writes, so the
1620
+ * mutation lock is re-validated (and extended) immediately before the write:
1621
+ * a stalled mutator that lost its lock must fail loudly instead of clobbering
1622
+ * a newer record with a stale copy.
1623
+ */
1624
+ async function writeConversation(state, lock, conversation) {
1625
+ const execution = executionWithPendingMessages(conversation.execution, conversation.execution.pendingMessages);
1626
+ const next = {
1627
+ ...conversation,
1628
+ execution
1629
+ };
1630
+ await fenceConversationMutation(state, lock, next.conversationId);
1631
+ const hasSlackDestination = next.destination?.platform === "slack";
1632
+ const stored = {
1633
+ ...next,
1634
+ execution: {
1635
+ ...next.execution,
1636
+ pendingMessages: next.execution.pendingMessages.map((message) => storeMessage(message, hasSlackDestination && (message.source === "slack" || message.source === "event" || message.source === "plugin" || message.source === "scheduler")))
1637
+ }
1638
+ };
1639
+ await state.set(conversationKey(next.conversationId), stored, JUNIOR_THREAD_STATE_TTL_MS);
1640
+ await upsertIndexEntry({
1641
+ state,
1642
+ indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
1643
+ conversationId: next.conversationId,
1644
+ score: next.lastActivityAtMs
1645
+ });
1646
+ if (!hasRunnableWork(next)) {
1647
+ await removeIndexEntry({
1648
+ state,
1649
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
1650
+ conversationId: next.conversationId
1651
+ });
1652
+ return;
1653
+ }
1654
+ await upsertIndexEntry({
1655
+ state,
1656
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
1657
+ conversationId: next.conversationId,
1658
+ score: next.execution.updatedAtMs ?? next.updatedAtMs
1659
+ });
1660
+ }
1661
+ function assertSameOptionalConversationDestination(args) {
1662
+ if (!args.next || !args.current) return;
1663
+ if (sameDestination(args.current, args.next)) return;
1664
+ throw new Error(`Conversation destination changed for ${args.conversationId}`);
1665
+ }
1666
+ function conversationWorkState(conversation) {
1667
+ const lease = conversation.execution.lease;
1668
+ return {
1669
+ ...conversation,
1670
+ lastEnqueuedAtMs: conversation.execution.lastEnqueuedAtMs,
1671
+ ...lease ? { lease: {
1672
+ acquiredAtMs: lease.acquiredAtMs,
1673
+ lastCheckInAtMs: lease.lastCheckInAtMs,
1674
+ leaseExpiresAtMs: lease.expiresAtMs,
1675
+ leaseToken: lease.token
1676
+ } } : void 0,
1677
+ messages: pendingMessages(conversation),
1678
+ needsRun: hasRunnableWork(conversation)
1679
+ };
1680
+ }
1681
+ /** Return a persisted conversation record, if one exists. */
1682
+ async function getConversation(args) {
1683
+ return await readConversation(await getConnectedState(args.state), args.conversationId);
1684
+ }
1685
+ /** Return a persisted conversation work record, if one exists. */
1686
+ async function getConversationWorkState(args) {
1687
+ const conversation = await getConversation(args);
1688
+ return conversation ? conversationWorkState(conversation) : void 0;
1689
+ }
1690
+ /** Return whether a conversation has pending or resumable execution work. */
1691
+ function hasRunnableConversationWork(conversation) {
1692
+ return hasRunnableWork(conversation);
1693
+ }
1694
+ async function appendInboundMessageWithAdmission(args) {
1695
+ const nowMs = args.nowMs ?? now();
1696
+ return await withConversationMutation({
1697
+ conversationId: args.message.conversationId,
1698
+ state: args.state
1699
+ }, async (state, lock) => {
1700
+ const existing = await readConversation(state, args.message.conversationId);
1701
+ const current = existing ?? emptyConversation({
1702
+ conversationId: args.message.conversationId,
1703
+ destination: args.message.destination,
1704
+ nowMs,
1705
+ source: args.message.source
1706
+ });
1707
+ if (existing) assertSameOptionalConversationDestination({
1708
+ conversationId: args.message.conversationId,
1709
+ current: current.destination,
1710
+ next: args.message.destination
1711
+ });
1712
+ const existingPending = current.execution.pendingMessages.some((message) => message.inboundMessageId === args.message.inboundMessageId);
1713
+ if (current.execution.inboundMessageIds.includes(args.message.inboundMessageId)) {
1714
+ if (!existingPending) return { status: "duplicate" };
1715
+ const nextStatus = current.execution.status === "idle" ? "pending" : current.execution.status;
1716
+ await writeConversation(state, lock, withExecutionUpdate(current, {
1717
+ ...current.execution,
1718
+ status: nextStatus,
1719
+ pendingMessages: current.execution.pendingMessages.map((message) => message.inboundMessageId === args.message.inboundMessageId ? upgradedPendingMessage(message, args.message) : message)
1720
+ }, nowMs));
1721
+ return { status: "duplicate" };
1722
+ }
1723
+ if (args.exclusive && hasRunnableWork(current)) return { status: "active" };
1724
+ let status;
1725
+ if (current.execution.lease) status = current.execution.status === "running" ? "running" : "paused";
1726
+ else if (current.execution.status === "paused" && args.message.delivery === "defer") status = "paused";
1727
+ else status = "pending";
1728
+ await writeConversation(state, lock, withExecutionUpdate({
1729
+ ...current,
1730
+ destination: current.destination ?? args.message.destination,
1731
+ source: current.source ?? args.message.source,
1732
+ lastActivityAtMs: nowMs
1733
+ }, {
1734
+ ...current.execution,
1735
+ status,
1736
+ inboundMessageIds: [...current.execution.inboundMessageIds, args.message.inboundMessageId],
1737
+ pendingMessages: [...current.execution.pendingMessages, args.message].sort(compareMessages)
1738
+ }, nowMs));
1739
+ return { status: "appended" };
1740
+ });
1741
+ }
1742
+ /** Persist one inbound message idempotently in its conversation mailbox. */
1743
+ async function appendInboundMessage(args) {
1744
+ const result = await appendInboundMessageWithAdmission({
1745
+ ...args,
1746
+ exclusive: false
1747
+ });
1748
+ if (result.status === "active") throw new Error("Non-exclusive mailbox append returned active");
1749
+ return result;
1750
+ }
1751
+ /** Append only when the Conversation has no other runnable work. */
1752
+ async function appendExclusiveInboundMessage(args) {
1753
+ return await appendInboundMessageWithAdmission({
1754
+ ...args,
1755
+ exclusive: true
1756
+ });
1757
+ }
1758
+ /** Mark a conversation runnable when there is no new mailbox message. */
1759
+ async function requestConversationWork(args) {
1760
+ const nowMs = args.nowMs ?? now();
1761
+ return await withConversationMutation(args, async (state, lock) => {
1762
+ const existing = await readConversation(state, args.conversationId);
1763
+ if (existing) assertSameOptionalConversationDestination({
1764
+ conversationId: args.conversationId,
1765
+ current: existing.destination,
1766
+ next: args.destination
1767
+ });
1768
+ const current = existing ?? emptyConversation({
1769
+ conversationId: args.conversationId,
1770
+ destination: args.destination,
1771
+ nowMs
1772
+ });
1773
+ const status = current.execution.lease ? "paused" : "pending";
1774
+ await writeConversation(state, lock, withExecutionUpdate({
1775
+ ...current,
1776
+ destination: current.destination ?? args.destination
1777
+ }, {
1778
+ ...current.execution,
1779
+ status
1780
+ }, nowMs));
1781
+ return { status: existing === void 0 ? "created" : "updated" };
1782
+ });
1783
+ }
1784
+ /** Record that a wake-up nudge was accepted for the conversation. */
1785
+ async function markConversationWorkEnqueued(args) {
1786
+ const nowMs = args.nowMs ?? now();
1787
+ await withConversationMutation(args, async (state, lock) => {
1788
+ const current = await readConversation(state, args.conversationId);
1789
+ if (!current) return;
1790
+ await writeConversation(state, lock, withExecutionUpdate(current, {
1791
+ ...current.execution,
1792
+ lastEnqueuedAtMs: nowMs
1793
+ }, nowMs));
1794
+ });
1795
+ }
1796
+ /** Clear a wake marker after its queue delivery finds no runnable work. */
1797
+ async function clearConsumedConversationWake(args) {
1798
+ const nowMs = args.nowMs ?? now();
1799
+ return await withConversationMutation(args, async (state, lock) => {
1800
+ const current = await readConversation(state, args.conversationId);
1801
+ if (!current || hasRunnableWork(current) || current.execution.lastEnqueuedAtMs === void 0) return false;
1802
+ await writeConversation(state, lock, withExecutionUpdate(current, {
1803
+ ...current.execution,
1804
+ lastEnqueuedAtMs: void 0
1805
+ }, nowMs));
1806
+ return true;
1807
+ });
1808
+ }
1809
+ /** Try to acquire the durable execution lease for one conversation. */
1810
+ async function startConversationWork(args) {
1811
+ const nowMs = args.nowMs ?? now();
1812
+ return await withConversationMutation(args, async (state, lock) => {
1813
+ const current = await readConversation(state, args.conversationId);
1814
+ if (!current) return { status: "no_work" };
1815
+ if (isLeaseActive(current.execution.lease, nowMs)) return {
1816
+ status: "active",
1817
+ leaseExpiresAtMs: current.execution.lease.expiresAtMs
1818
+ };
1819
+ if (!hasRunnableWork(current)) return { status: "no_work" };
1820
+ const lease = {
1821
+ token: randomUUID(),
1822
+ acquiredAtMs: nowMs,
1823
+ lastCheckInAtMs: nowMs,
1824
+ expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
1825
+ };
1826
+ const startsNewRun = current.execution.runId === void 0;
1827
+ await writeConversation(state, lock, withExecutionUpdate(current, {
1828
+ ...current.execution,
1829
+ lease,
1830
+ status: current.execution.status === "paused" ? "paused" : "running",
1831
+ runId: current.execution.runId ?? randomUUID(),
1832
+ lastEnqueuedAtMs: void 0,
1833
+ retryCount: startsNewRun ? 0 : current.execution.retryCount
1834
+ }, nowMs));
1835
+ return {
1836
+ status: "acquired",
1837
+ leaseToken: lease.token,
1838
+ leaseExpiresAtMs: lease.expiresAtMs
1839
+ };
1840
+ });
1841
+ }
1842
+ /** Extend the durable execution lease when the worker checks in. */
1843
+ async function checkInConversationWork(args) {
1844
+ const nowMs = args.nowMs ?? now();
1845
+ return await withConversationMutation(args, async (state, lock) => {
1846
+ const current = await readConversation(state, args.conversationId);
1847
+ if (!current || current.execution.lease?.token !== args.leaseToken) return false;
1848
+ await writeConversation(state, lock, withExecutionUpdate(current, {
1849
+ ...current.execution,
1850
+ lease: {
1851
+ ...current.execution.lease,
1852
+ lastCheckInAtMs: nowMs,
1853
+ expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
1854
+ }
1855
+ }, nowMs));
1856
+ return true;
1857
+ });
1858
+ }
1859
+ /**
1860
+ * Resolve pending mailbox entries after the caller accepts responsibility.
1861
+ *
1862
+ * Returning ids acknowledges only that subset; returning nothing acknowledges
1863
+ * every pending entry passed to the handler.
1864
+ */
1865
+ async function drainConversationMailbox(args) {
1866
+ const nowMs = args.nowMs ?? now();
1867
+ const pending = await withConversationMutation(args, async (state) => {
1868
+ const current = await readConversation(state, args.conversationId);
1869
+ if (!current || current.execution.lease?.token !== args.leaseToken) throw new Error(`Conversation lease is not held for ${args.conversationId}`);
1870
+ return pendingMessages(current);
1871
+ });
1872
+ if (pending.length === 0) return {
1873
+ changed: false,
1874
+ messages: []
1875
+ };
1876
+ const result = await args.handle(pending);
1877
+ const pendingIds = new Set(pending.map((message) => message.inboundMessageId));
1878
+ const acknowledgedIds = new Set(result ?? pending.map((message) => message.inboundMessageId));
1879
+ for (const inboundMessageId of acknowledgedIds) if (!pendingIds.has(inboundMessageId)) throw new Error(`Conversation mailbox drain result is not pending for ${args.conversationId}`);
1880
+ if (acknowledgedIds.size === 0) return {
1881
+ changed: false,
1882
+ messages: []
1883
+ };
1884
+ await withConversationMutation(args, async (state, lock) => {
1885
+ const current = await readConversation(state, args.conversationId);
1886
+ if (!current || current.execution.lease?.token !== args.leaseToken) throw new Error(`Conversation lease is not held for ${args.conversationId}`);
1887
+ const pendingMessages = current.execution.pendingMessages.filter((message) => !acknowledgedIds.has(message.inboundMessageId));
1888
+ await writeConversation(state, lock, withExecutionUpdate(current, {
1889
+ ...current.execution,
1890
+ status: current.execution.status === "pending" && pendingMessages.length === 0 ? "running" : current.execution.status,
1891
+ lastProgressAtMs: nowMs,
1892
+ retryCount: 0,
1893
+ pendingMessages
1894
+ }, nowMs));
1895
+ });
1896
+ return {
1897
+ changed: true,
1898
+ messages: pending.filter((message) => acknowledgedIds.has(message.inboundMessageId))
1899
+ };
1900
+ }
1901
+ /** Acknowledge leased mailbox entries after the handler accepts responsibility. */
1902
+ async function ackMessages(args) {
1903
+ const nowMs = args.nowMs ?? now();
1904
+ const inboundMessageIds = new Set(args.inboundMessageIds);
1905
+ return await withConversationMutation(args, async (state, lock) => {
1906
+ const current = await readConversation(state, args.conversationId);
1907
+ if (!current || current.execution.lease?.token !== args.leaseToken) return false;
1908
+ if (inboundMessageIds.size === 0) return true;
1909
+ const pendingMessages = current.execution.pendingMessages.filter((message) => !inboundMessageIds.has(message.inboundMessageId));
1910
+ if (pendingMessages.length === current.execution.pendingMessages.length) return true;
1911
+ await writeConversation(state, lock, withExecutionUpdate(current, {
1912
+ ...current.execution,
1913
+ lastProgressAtMs: nowMs,
1914
+ retryCount: 0,
1915
+ pendingMessages
1916
+ }, nowMs));
1917
+ return true;
1918
+ });
1919
+ }
1920
+ function isHumanFacingMessage(message) {
1921
+ return message.source === "web" || message.source === "slack";
1922
+ }
1923
+ async function readConversationStop(state, conversationId) {
1924
+ return normalizeStop(await state.get(conversationStopKey(conversationId)));
1925
+ }
1926
+ async function fenceConversationMutation(state, lock, conversationId) {
1927
+ if (!await state.extendLock(lock, CONVERSATION_MUTATION_LOCK_TTL_MS)) throw new ConversationMutationFencedError(conversationId);
1928
+ }
1929
+ /** Return whether one Conversation run has a durable stop request. */
1930
+ async function hasConversationStop(args) {
1931
+ return (await readConversationStop(await getConnectedState(args.state), args.conversationId))?.runId === args.runId;
1932
+ }
1933
+ /** Persist a stop request for the current run without process affinity. */
1934
+ async function stopConversationWork(args) {
1935
+ const nowMs = args.nowMs ?? now();
1936
+ return await withConversationMutation(args, async (state, lock) => {
1937
+ const current = await readConversation(state, args.conversationId);
1938
+ if (!current || !hasRunnableWork(current)) return { status: "no_work" };
1939
+ const runId = current.execution.runId ?? randomUUID();
1940
+ const pendingMessages = current.execution.pendingMessages.filter(isHumanFacingMessage);
1941
+ const inboundMessageIds = pendingMessages.map((message) => message.inboundMessageId);
1942
+ if (current.execution.runId === void 0) await writeConversation(state, lock, withExecutionUpdate(current, {
1943
+ ...current.execution,
1944
+ runId
1945
+ }, nowMs));
1946
+ await fenceConversationMutation(state, lock, args.conversationId);
1947
+ await state.set(conversationStopKey(args.conversationId), {
1948
+ inboundMessageIds,
1949
+ runId
1950
+ }, JUNIOR_THREAD_STATE_TTL_MS);
1951
+ return {
1952
+ runId,
1953
+ status: "requested",
1954
+ pendingMessages
1955
+ };
1956
+ });
1957
+ }
1958
+ /** Clear one observed stop request and discard older human-facing mailbox work. */
1959
+ async function completeConversationStop(args) {
1960
+ const nowMs = args.nowMs ?? now();
1961
+ return await withConversationMutation(args, async (state, lock) => {
1962
+ const current = await readConversation(state, args.conversationId);
1963
+ if (!current || current.execution.lease?.token !== args.leaseToken) return {
1964
+ status: "lost_lease",
1965
+ removedInboundMessageIds: []
1966
+ };
1967
+ const stop = await readConversationStop(state, args.conversationId);
1968
+ if (!stop || stop.runId !== args.runId) return {
1969
+ status: "none",
1970
+ removedInboundMessageIds: []
1971
+ };
1972
+ const stoppedInboundMessageIds = new Set(stop.inboundMessageIds);
1973
+ const removedInboundMessageIds = [];
1974
+ const pendingMessages = current.execution.pendingMessages.filter((message) => {
1975
+ const shouldStop = stoppedInboundMessageIds.has(message.inboundMessageId);
1976
+ if (shouldStop) removedInboundMessageIds.push(message.inboundMessageId);
1977
+ return !shouldStop;
1978
+ });
1979
+ await writeConversation(state, lock, withExecutionUpdate(current, {
1980
+ ...current.execution,
1981
+ lastEnqueuedAtMs: pendingMessages.length === 0 ? void 0 : current.execution.lastEnqueuedAtMs,
1982
+ pendingMessages
1983
+ }, nowMs));
1984
+ await fenceConversationMutation(state, lock, args.conversationId);
1985
+ await state.delete(conversationStopKey(args.conversationId));
1986
+ return {
1987
+ status: "cleared",
1988
+ removedInboundMessageIds
1989
+ };
1990
+ });
1991
+ }
1992
+ /** Cancel human-facing pending mailbox rows without requiring a worker lease. */
1993
+ async function cancelHumanFacingPendingMessages(args) {
1994
+ const nowMs = args.nowMs ?? now();
1995
+ const requestedIds = args.inboundMessageIds === void 0 ? void 0 : new Set(args.inboundMessageIds);
1996
+ return await withConversationMutation(args, async (state, lock) => {
1997
+ const current = await readConversation(state, args.conversationId);
1998
+ if (!current) return {
1999
+ cancelledInboundMessageIds: [],
2000
+ cancelledMessages: []
2001
+ };
2002
+ const cancelledInboundMessageIds = [];
2003
+ const cancelledMessages = [];
2004
+ const pendingMessages = [];
2005
+ for (const message of current.execution.pendingMessages) {
2006
+ const isRequested = requestedIds === void 0 || requestedIds.has(message.inboundMessageId);
2007
+ const isInSnapshot = args.receivedBeforeMs === void 0 || message.receivedAtMs <= args.receivedBeforeMs;
2008
+ if (isHumanFacingMessage(message) && isRequested && isInSnapshot) {
2009
+ cancelledInboundMessageIds.push(message.inboundMessageId);
2010
+ cancelledMessages.push(message);
2011
+ continue;
2012
+ }
2013
+ pendingMessages.push(message);
2014
+ }
2015
+ if (cancelledInboundMessageIds.length === 0) return {
2016
+ cancelledInboundMessageIds,
2017
+ cancelledMessages
2018
+ };
2019
+ const becomesIdle = current.execution.status === "pending" && pendingMessages.length === 0;
2020
+ await writeConversation(state, lock, withExecutionUpdate(current, {
2021
+ ...current.execution,
2022
+ lastEnqueuedAtMs: pendingMessages.length === 0 ? void 0 : current.execution.lastEnqueuedAtMs,
2023
+ pendingMessages,
2024
+ retryCount: becomesIdle ? 0 : current.execution.retryCount,
2025
+ runId: becomesIdle ? void 0 : current.execution.runId,
2026
+ status: becomesIdle ? "idle" : current.execution.status
2027
+ }, nowMs));
2028
+ return {
2029
+ cancelledInboundMessageIds,
2030
+ cancelledMessages
2031
+ };
2032
+ });
2033
+ }
2034
+ /**
2035
+ * Mark one author's queued web messages as interrupts without a worker lease.
2036
+ *
2037
+ * The active Turn drains them at its next model boundary when it belongs to the
2038
+ * same author. Otherwise they start the next Turn ahead of deferred input.
2039
+ */
2040
+ async function steerWebPendingMessages(args) {
2041
+ const nowMs = args.nowMs ?? now();
2042
+ const requestedIds = new Set(args.inboundMessageIds);
2043
+ return await withConversationMutation(args, async (state, lock) => {
2044
+ const current = await readConversation(state, args.conversationId);
2045
+ if (!current) return { steeredInboundMessageIds: [] };
2046
+ const steeredInboundMessageIds = [];
2047
+ const pendingMessages = current.execution.pendingMessages.map((message) => {
2048
+ if (message.source !== "web" || message.delivery !== "defer" || message.input.authorId !== args.authorId || !requestedIds.has(message.inboundMessageId)) return message;
2049
+ steeredInboundMessageIds.push(message.inboundMessageId);
2050
+ return {
2051
+ ...message,
2052
+ delivery: "interrupt"
2053
+ };
2054
+ });
2055
+ if (steeredInboundMessageIds.length === 0) return { steeredInboundMessageIds };
2056
+ await writeConversation(state, lock, withExecutionUpdate(current, {
2057
+ ...current.execution,
2058
+ pendingMessages
2059
+ }, nowMs));
2060
+ return { steeredInboundMessageIds };
2061
+ });
2062
+ }
2063
+ /** Mark the leased conversation as needing another queue-delivered slice. */
2064
+ async function requestAnotherSlice(args) {
2065
+ const nowMs = args.nowMs ?? now();
2066
+ return await withConversationMutation(args, async (state, lock) => {
2067
+ const current = await readConversation(state, args.conversationId);
2068
+ if (!current || current.execution.lease?.token !== args.leaseToken) return false;
2069
+ assertSameOptionalConversationDestination({
2070
+ conversationId: args.conversationId,
2071
+ current: current.destination,
2072
+ next: args.destination
2073
+ });
2074
+ await writeConversation(state, lock, withExecutionUpdate(current, {
2075
+ ...current.execution,
2076
+ status: "paused"
2077
+ }, nowMs));
2078
+ return true;
2079
+ });
2080
+ }
2081
+ /** Begin a requested turn resume under the worker's existing lease. */
2082
+ async function beginConversationResume(args) {
2083
+ const nowMs = args.nowMs ?? now();
2084
+ return await withConversationMutation(args, async (state, lock) => {
2085
+ const current = await readConversation(state, args.conversationId);
2086
+ if (!current || current.execution.lease?.token !== args.leaseToken || current.execution.status !== "paused") return false;
2087
+ await writeConversation(state, lock, withExecutionUpdate(current, {
2088
+ ...current.execution,
2089
+ status: "running"
2090
+ }, nowMs));
2091
+ return true;
2092
+ });
2093
+ }
2094
+ /** Release the durable execution lease without changing completion state. */
2095
+ async function releaseConversationWork(args) {
2096
+ const nowMs = args.nowMs ?? now();
2097
+ return await withConversationMutation(args, async (state, lock) => {
2098
+ const current = await readConversation(state, args.conversationId);
2099
+ if (!current || current.execution.lease?.token !== args.leaseToken) return false;
2100
+ await writeConversation(state, lock, withExecutionUpdate(current, {
2101
+ ...current.execution,
2102
+ lease: void 0,
2103
+ status: current.execution.status === "running" ? "pending" : current.execution.status
2104
+ }, nowMs));
2105
+ return true;
2106
+ });
2107
+ }
2108
+ /** Count one failed execution attempt and stop the conversation when retries are exhausted. */
2109
+ async function recordConversationRetry(args) {
2110
+ const nowMs = args.nowMs ?? now();
2111
+ return await withConversationMutation(args, async (state, lock) => {
2112
+ const current = await readConversation(state, args.conversationId);
2113
+ if (!current || current.execution.lease?.token !== args.leaseToken) return "lost_lease";
2114
+ const count = (current.execution.retryCount ?? 0) + 1;
2115
+ const stopped = count >= 5;
2116
+ await writeConversation(state, lock, withExecutionUpdate(current, {
2117
+ ...current.execution,
2118
+ lastEnqueuedAtMs: void 0,
2119
+ lease: stopped ? void 0 : current.execution.lease,
2120
+ retryCount: count,
2121
+ pendingMessages: stopped ? [] : current.execution.pendingMessages,
2122
+ runId: stopped ? void 0 : current.execution.runId,
2123
+ status: stopped ? "failed" : "paused"
2124
+ }, nowMs));
2125
+ return stopped ? "stopped" : "recorded";
2126
+ });
2127
+ }
2128
+ /** Finish a leased conversation and report whether runnable work remains. */
2129
+ async function completeConversationWork(args) {
2130
+ const nowMs = args.nowMs ?? now();
2131
+ return await withConversationMutation(args, async (state, lock) => {
2132
+ const current = await readConversation(state, args.conversationId);
2133
+ if (!current || current.execution.lease?.token !== args.leaseToken) return "lost_lease";
2134
+ const hasPending = pendingMessages(current).length > 0;
2135
+ const stopped = args.resumeIfStopped === true && (await readConversationStop(state, args.conversationId))?.runId === current.execution.runId;
2136
+ const needsRun = current.execution.status === "paused" || stopped;
2137
+ const runnable = needsRun || hasPending;
2138
+ await writeConversation(state, lock, withExecutionUpdate(current, {
2139
+ ...current.execution,
2140
+ lease: void 0,
2141
+ status: needsRun ? "paused" : hasPending ? "pending" : "idle",
2142
+ lastProgressAtMs: args.madeProgress === false ? current.execution.lastProgressAtMs : nowMs,
2143
+ retryCount: runnable || args.madeProgress === false ? current.execution.retryCount : 0,
2144
+ runId: runnable ? current.execution.runId : void 0
2145
+ }, nowMs));
2146
+ return runnable ? "pending" : "completed";
2147
+ });
2148
+ }
2149
+ /**
2150
+ * Record one failed execution attempt for pending messages, dead-lettering
2151
+ * the conversation when its retry budget or the legacy message delivery limit
2152
+ * is exhausted.
2153
+ *
2154
+ * Retries are counted only when the run made no durable progress: if any
2155
+ * attempted message left the mailbox, the remaining pending entries may be
2156
+ * deliberate deferrals and are left untouched. Consumed message ids stay in
2157
+ * `inboundMessageIds` so source retries remain duplicates.
2158
+ */
2159
+ async function recordAttemptFailure(args) {
2160
+ const nowMs = args.nowMs ?? now();
2161
+ return await withConversationMutation(args, async (state, lock) => {
2162
+ const current = await readConversation(state, args.conversationId);
2163
+ if (!current || current.execution.lease?.token !== args.leaseToken) return {
2164
+ status: "lost_lease",
2165
+ pendingCount: 0,
2166
+ deadLetteredMessages: [],
2167
+ retryCount: 0
2168
+ };
2169
+ const pendingIds = new Set(current.execution.pendingMessages.map((message) => message.inboundMessageId));
2170
+ if (args.inboundMessageIds.length === 0 || args.inboundMessageIds.some((id) => !pendingIds.has(id))) return {
2171
+ status: "skipped",
2172
+ pendingCount: current.execution.pendingMessages.length,
2173
+ deadLetteredMessages: [],
2174
+ retryCount: current.execution.retryCount ?? 0
2175
+ };
2176
+ const retryCount = (current.execution.retryCount ?? 0) + 1;
2177
+ const retriesExhausted = retryCount >= 5;
2178
+ const attemptedIds = new Set(args.inboundMessageIds);
2179
+ const deadLetteredMessages = [];
2180
+ const pendingMessages = [];
2181
+ for (const message of current.execution.pendingMessages) {
2182
+ if (!attemptedIds.has(message.inboundMessageId)) {
2183
+ if (retriesExhausted) deadLetteredMessages.push(message);
2184
+ else pendingMessages.push(message);
2185
+ continue;
2186
+ }
2187
+ const attempted = {
2188
+ ...message,
2189
+ attemptCount: (message.attemptCount ?? 0) + 1
2190
+ };
2191
+ if (retriesExhausted || attempted.attemptCount >= 5) {
2192
+ deadLetteredMessages.push(attempted);
2193
+ continue;
2194
+ }
2195
+ pendingMessages.push(attempted);
2196
+ }
2197
+ await writeConversation(state, lock, withExecutionUpdate(current, {
2198
+ ...current.execution,
2199
+ retryCount,
2200
+ pendingMessages
2201
+ }, nowMs));
2202
+ return {
2203
+ status: "recorded",
2204
+ pendingCount: pendingMessages.length,
2205
+ deadLetteredMessages,
2206
+ retryCount
2207
+ };
2208
+ });
2209
+ }
2210
+ /** Record a terminal failure completion for a leased conversation. */
2211
+ async function deadLetterAttempt(args) {
2212
+ const nowMs = args.nowMs ?? now();
2213
+ return await withConversationMutation(args, async (state, lock) => {
2214
+ const current = await readConversation(state, args.conversationId);
2215
+ if (!current || current.execution.lease?.token !== args.leaseToken) return "lost_lease";
2216
+ const runnable = pendingMessages(current).length > 0;
2217
+ await writeConversation(state, lock, withExecutionUpdate(current, {
2218
+ ...current.execution,
2219
+ lease: void 0,
2220
+ status: runnable ? "pending" : "failed",
2221
+ runId: runnable ? current.execution.runId : void 0
2222
+ }, nowMs));
2223
+ return runnable ? "pending" : "failed";
2224
+ });
2225
+ }
2226
+ /** Recover an expired lease, consuming the retry budget under the mutation lock. */
2227
+ async function clearExpiredConversationLease(args) {
2228
+ const nowMs = args.nowMs ?? now();
2229
+ return await withConversationMutation(args, async (state, lock) => {
2230
+ const current = await readConversation(state, args.conversationId);
2231
+ if (!current?.execution.lease || current.execution.lease.expiresAtMs > nowMs) return "not_expired";
2232
+ const retryCount = (current.execution.retryCount ?? 0) + 1;
2233
+ const stopped = retryCount >= 5;
2234
+ await writeConversation(state, lock, withExecutionUpdate(current, {
2235
+ ...current.execution,
2236
+ lastEnqueuedAtMs: void 0,
2237
+ lease: void 0,
2238
+ retryCount,
2239
+ pendingMessages: stopped ? [] : current.execution.pendingMessages,
2240
+ runId: stopped ? void 0 : current.execution.runId,
2241
+ status: stopped ? "failed" : "paused"
2242
+ }, nowMs));
2243
+ return stopped ? "stopped" : "requeued";
2244
+ });
2245
+ }
2246
+ /** Remove one conversation from the active index after it is missing or idle. */
2247
+ async function removeActiveConversation(args) {
2248
+ await removeIndexEntry({
2249
+ state: await getConnectedState(args.state),
2250
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
2251
+ conversationId: args.conversationId
2252
+ });
2253
+ }
2254
+ /** List active conversation ids by oldest execution update first. */
2255
+ async function listActiveConversationIds(args = {}) {
2256
+ return (await (await getConversationIndexStore(await getConnectedState(args.state))).list({
2257
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
2258
+ limit: args.limit,
2259
+ order: "asc",
2260
+ scoreMax: args.staleBeforeMs
2261
+ })).map((entry) => entry.conversationId);
2262
+ }
1108
2263
  // src/chat/task-execution/turn-cursor-keys.ts
1109
2264
  /** Redis namespace for the current turn cursor format. */
1110
2265
  const TURN_CURSOR_PREFIX = "junior:turn_cursor:v2";
@@ -1216,7 +2371,7 @@ const storedTurnRecordSchema = z.object({
1216
2371
  startedAtMs: nonNegativeNumberSchema,
1217
2372
  state: turnStatusSchema,
1218
2373
  surface: turnSurfaceSchema.optional(),
1219
- traceId: z.string().optional(),
2374
+ traceContext: traceContextSchema,
1220
2375
  updatedAtMs: nonNegativeNumberSchema,
1221
2376
  committedSeq: seqCursorSchema,
1222
2377
  historyVersion: z.number().int().nonnegative().optional(),
@@ -1331,7 +2486,7 @@ function materializeTurnRecord(stored, piProjection, turnStartMessageIndex, runt
1331
2486
  resultMessageId: stored.resultMessageId,
1332
2487
  resumedFromSliceId: stored.resumedFromSliceId,
1333
2488
  surface: stored.surface,
1334
- traceId: stored.traceId,
2489
+ traceContext: stored.traceContext,
1335
2490
  turnStartMessageIndex
1336
2491
  })
1337
2492
  };
@@ -1404,7 +2559,7 @@ function buildStoredRecord(args) {
1404
2559
  resultMessageId: args.resultMessageId,
1405
2560
  resumedFromSliceId: args.resumedFromSliceId,
1406
2561
  surface: args.surface,
1407
- traceId: args.traceId,
2562
+ traceContext: args.traceContext,
1408
2563
  turnStartSeq: args.turnStartSeq
1409
2564
  })
1410
2565
  };
@@ -1482,7 +2637,7 @@ async function updateTurnState(args) {
1482
2637
  resultMessageId: args.resultMessageId ?? args.existing.resultMessageId,
1483
2638
  resumedFromSliceId: args.existing.resumedFromSliceId,
1484
2639
  surface: args.existing.surface,
1485
- traceId: args.existing.traceId,
2640
+ traceContext: args.existing.traceContext,
1486
2641
  turnStartSeq: parsed.turnStartSeq
1487
2642
  })
1488
2643
  })
@@ -1559,7 +2714,7 @@ async function upsertTurnRecordLocked(args, fence) {
1559
2714
  resumedFromSliceId: args.resumedFromSliceId,
1560
2715
  startedAtMs: existingRecord?.startedAtMs,
1561
2716
  surface: args.surface ?? existingRecord?.surface,
1562
- traceId: args.traceId ?? existingRecord?.traceId,
2717
+ traceContext: args.traceContext ?? existingRecord?.traceContext,
1563
2718
  turnStartSeq
1564
2719
  })
1565
2720
  })
@@ -1728,7 +2883,7 @@ function sharedWrite(args, latest) {
1728
2883
  dispatchId: args.dispatchId ?? latest?.dispatchId,
1729
2884
  source: args.source,
1730
2885
  surface: args.surface ?? latest?.surface,
1731
- traceId: getActiveTraceId() ?? latest?.traceId,
2886
+ traceContext: latest?.traceContext ?? getActiveTraceContext(),
1732
2887
  turnStartMessageIndex: args.turnStartMessageIndex ?? latest?.turnStartMessageIndex,
1733
2888
  cumulativeToolCallCount: args.cumulativeToolCallCount ?? latest?.cumulativeToolCallCount,
1734
2889
  turnLimitWrappedUp: args.turnLimitWrappedUp ?? latest?.turnLimitWrappedUp,
@@ -3265,6 +4420,138 @@ async function recordGuardianActionReviewed(args) {
3265
4420
  createdAtMs: Date.now()
3266
4421
  }]);
3267
4422
  }
4423
+ // src/chat/events/catalog.ts
4424
+ /** A source with nothing enabled, for a namespace that the catalog lacks. */
4425
+ const NO_SOURCE = {
4426
+ resourceTypes: [],
4427
+ events: {},
4428
+ anyResourceFields: {}
4429
+ };
4430
+ /**
4431
+ * Build one catalog entry. It merges the filters of every resource type here,
4432
+ * once, so trigger checks for each event copy and each row do not merge them
4433
+ * again.
4434
+ */
4435
+ function eventCatalogEntry(registration) {
4436
+ return {
4437
+ ...registration,
4438
+ anyResourceFields: anyResourceMatchFields(registration.resourceTypes)
4439
+ };
4440
+ }
4441
+ /** Core-owned event namespace. Not a plugin publisher. */
4442
+ const CORE_EVENT_NAMESPACE = "junior";
4443
+ /** True when the catalog has any enabled registration. */
4444
+ function hasEventCatalogEntries(catalog) {
4445
+ return Object.keys(catalog).length > 0;
4446
+ }
4447
+ /**
4448
+ * Catalog entries that durable event automations may select.
4449
+ *
4450
+ * Resource types with `automation: false` stay out of create/update schemas
4451
+ * and trigger availability. A namespace with no such resource type left is
4452
+ * removed.
4453
+ */
4454
+ function automationEventCatalog(catalog) {
4455
+ return Object.fromEntries(Object.entries(catalog).flatMap(([namespace, registration]) => {
4456
+ const resourceTypes = registration.resourceTypes.filter((resourceType) => registration.limits?.[resourceType.type]?.automation !== false);
4457
+ if (resourceTypes.length === 0) return [];
4458
+ if (resourceTypes.length === registration.resourceTypes.length) return [[namespace, registration]];
4459
+ const supported = new Set(resourceTypes.flatMap((resourceType) => resourceType.supportedEvents));
4460
+ return [[namespace, eventCatalogEntry({
4461
+ ...registration,
4462
+ resourceTypes,
4463
+ events: Object.fromEntries(Object.entries(registration.events).filter(([eventType]) => supported.has(eventType)))
4464
+ })]];
4465
+ }));
4466
+ }
4467
+ /** Return why watchEvents cannot watch one resource type, if it cannot. */
4468
+ function eventWatchUnavailable(catalog, namespace, resourceType) {
4469
+ const limits = catalog[namespace]?.limits?.[resourceType];
4470
+ return limits?.watch === false ? limits.watchUnavailableMessage : void 0;
4471
+ }
4472
+ function enumSchema(values, unavailableMessage) {
4473
+ if (values.length === 0) return z.string().refine(() => false, unavailableMessage);
4474
+ return z.enum(values);
4475
+ }
4476
+ /** Build the model-facing namespace enum from enabled event plugins. */
4477
+ function eventNamespaceSchema(catalog) {
4478
+ return enumSchema(Object.keys(catalog).sort(), "No event namespaces are enabled.");
4479
+ }
4480
+ /** Build the model-facing event enum from enabled plugin registrations. */
4481
+ function registeredEventTypeSchema(catalog) {
4482
+ return enumSchema([...new Set(Object.values(catalog).flatMap((registration) => registration.resourceTypes.flatMap((resourceType) => resourceType.supportedEvents)))].sort(), "No event types are enabled.");
4483
+ }
4484
+ /** Build the model-facing resource type enum from enabled plugin registrations. */
4485
+ function registeredResourceTypeSchema(catalog) {
4486
+ return enumSchema([...new Set(Object.values(catalog).flatMap((registration) => registration.resourceTypes.map((resourceType) => resourceType.type)))].sort(), "No resource types are enabled.");
4487
+ }
4488
+ /**
4489
+ * Return app guidance for one event of a namespace. The source declares it
4490
+ * once for the event, so every resource type and a trigger with no resource
4491
+ * type get the same guidance.
4492
+ */
4493
+ function eventGuidance(catalog, namespace, eventType) {
4494
+ return catalog[namespace]?.events[eventType]?.guidance;
4495
+ }
4496
+ /** Build the match object the tools accept for one resource type. */
4497
+ function registeredEventMatchSchema() {
4498
+ return eventMatchSchema.optional().describe("Optional filters: exact values for the resource type matchFields. A single value must equal the event data. A list means the event value may be any one entry. For a field with list: true, the event value is a list, and it matches when it includes the value or any entry. A field with events is set only on those events, so every selected event must be in that list. Omit a key to ignore it. Unmatched events are dropped with no agent turn.");
4499
+ }
4500
+ /**
4501
+ * List why a selection cannot work with the enabled plugins. Use it for new
4502
+ * selections and for saved triggers, which can go stale after a plugin renames
4503
+ * or removes a resource type, event, or filter.
4504
+ */
4505
+ function registeredEventTriggerProblems(catalog, selection, options) {
4506
+ return eventTriggerProblems(catalog[selection.namespace] ?? NO_SOURCE, selection, options);
4507
+ }
4508
+ /**
4509
+ * Describe a selection with the enabled plugins, for the dashboard and Slack.
4510
+ * Its problems are the same as `registeredEventTriggerProblems`.
4511
+ */
4512
+ function describeRegisteredEventTrigger(catalog, selection) {
4513
+ return describeEventTrigger(catalog[selection.namespace] ?? NO_SOURCE, selection);
4514
+ }
4515
+ function uniqueMatchValues(match) {
4516
+ if (!match || Object.keys(match).length === 0) return void 0;
4517
+ return Object.fromEntries(Object.entries(match).map(([key, value]) => [key, Array.isArray(value) ? [...new Set(value)] : value]));
4518
+ }
4519
+ /**
4520
+ * Reject an event automation trigger or a Watch that cannot work, with every
4521
+ * problem in one message for the agent. Return its match as it is saved:
4522
+ * each text value in the case that its plugin declares, with no repeated
4523
+ * list values.
4524
+ */
4525
+ function requireValidEventTrigger(catalog, selection, target) {
4526
+ const fields = eventTriggerChoices(catalog[selection.namespace] ?? NO_SOURCE, selection)?.fields ?? {};
4527
+ const normalized = {
4528
+ ...selection,
4529
+ match: normalizeEventMatch(fields, selection.match)
4530
+ };
4531
+ const problems = registeredEventTriggerProblems(catalog, normalized, { inputKeys: true });
4532
+ if (problems.length > 0) {
4533
+ const source = catalog[selection.namespace]?.displayName ?? selection.namespace;
4534
+ const resource = selection.resourceType ? `${source} ${selection.resourceType.replaceAll("_", " ")}` : source;
4535
+ throw new ToolInputError([target === "watch" ? `This ${resource} watch cannot be created.` : `This ${resource} trigger cannot be saved.`, ...problems.map((problem) => problem.message)].join(" "));
4536
+ }
4537
+ return uniqueMatchValues(normalized.match);
4538
+ }
4539
+ /**
4540
+ * Normalize one selector with the convention declared by its plugin.
4541
+ *
4542
+ * Reject a selector that does not fit its resource type. Resource types can
4543
+ * share event types, so only the identifier shows which one a selector names.
4544
+ */
4545
+ function requireEventIdentifier(catalog, input) {
4546
+ const registration = catalog[input.namespace];
4547
+ const identifier = normalizeEventIdentifier(registration, input.identifier);
4548
+ const resourceTypes = registration?.resourceTypes ?? [];
4549
+ const fits = (resourceType) => resourceType.identifier !== void 0 && new RegExp(resourceType.identifier.pattern).test(identifier);
4550
+ const resourceType = resourceTypes.find((candidate) => candidate.type === input.resourceType);
4551
+ if (!resourceType?.identifier || fits(resourceType)) return identifier;
4552
+ const fitting = resourceTypes.filter(fits).map((candidate) => candidate.type);
4553
+ throw new ToolInputError([`Identifier "${identifier}" does not fit resourceType "${input.resourceType}" in namespace "${input.namespace}". Expected format: "${resourceType.identifier.format}".`, ...fitting.length > 0 ? [`If the identifier is correct, use one of these resourceType values: ${fitting.map((type) => `"${type}"`).join(", ")}.`] : []].join(" "));
4554
+ }
3268
4555
  // src/chat/automations/revision.ts
3269
4556
  /** Storage revisions prevent stale edits from replacing newer values. */
3270
4557
  var AutomationConflictError = class extends ToolInputError {
@@ -3407,6 +4694,14 @@ function eventAutomationRunLimitReason() {
3407
4694
  const window = describeRunWindow(botConfig.eventAutomationRunWindowMs);
3408
4695
  return `This automation reached its limit of ${botConfig.maxEventAutomationRuns} runs per ${window}. Check that its trigger is not too broad, then resume it.`;
3409
4696
  }
4697
+ function hasRunKey(entries, runKey) {
4698
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
4699
+ const entry = entries[index];
4700
+ if ("reset" in entry) return false;
4701
+ if (entry.id === runKey) return true;
4702
+ }
4703
+ return false;
4704
+ }
3410
4705
  /**
3411
4706
  * Count one run of an Event automation, or refuse when its window is full.
3412
4707
  *
@@ -3415,22 +4710,29 @@ function eventAutomationRunLimitReason() {
3415
4710
  */
3416
4711
  async function admitEventAutomationRun(args) {
3417
4712
  const state = args.state ?? getStateAdapter();
3418
- await appendRunEntry(state, args.automationId, {
4713
+ await state.connect();
4714
+ const readEntries = async () => {
4715
+ const values = await state.getList(runListKey(args.automationId));
4716
+ return {
4717
+ entries: values.flatMap((value) => {
4718
+ const entry = runEntrySchema.safeParse(value);
4719
+ return entry.success ? [entry.data] : [];
4720
+ }),
4721
+ length: values.length
4722
+ };
4723
+ };
4724
+ if (!hasRunKey((await readEntries()).entries, args.runKey)) await appendRunEntry(state, args.automationId, {
3419
4725
  atMs: args.nowMs,
3420
4726
  id: args.runKey
3421
4727
  });
3422
4728
  const windowStartMs = args.nowMs - botConfig.eventAutomationRunWindowMs;
3423
- const values = await state.getList(runListKey(args.automationId));
3424
- const entries = values.flatMap((value) => {
3425
- const entry = runEntrySchema.safeParse(value);
3426
- return entry.success ? [entry.data] : [];
3427
- });
4729
+ const { entries, length } = await readEntries();
3428
4730
  let lastReset = -1;
3429
4731
  entries.forEach((entry, index) => {
3430
4732
  if ("reset" in entry) lastReset = index;
3431
4733
  });
3432
4734
  const oldest = entries[0];
3433
- const full = lastReset === -1 && values.length >= runListMaxLength() && oldest !== void 0 && "atMs" in oldest && oldest.atMs > windowStartMs;
4735
+ const full = lastReset === -1 && length >= runListMaxLength() && oldest !== void 0 && "atMs" in oldest && oldest.atMs > windowStartMs;
3434
4736
  const runKeys = /* @__PURE__ */ new Set();
3435
4737
  for (const entry of entries.slice(lastReset + 1)) {
3436
4738
  if ("reset" in entry) continue;
@@ -3461,7 +4763,7 @@ function eventAutomationColumns(task) {
3461
4763
  channelId: record.destination.channelId,
3462
4764
  creatorIdentityId,
3463
4765
  creatorSlackUserId: record.createdBy.slackUserId,
3464
- identifier: record.trigger.identifier,
4766
+ identifier: record.trigger.identifier ?? null,
3465
4767
  instruction: record.task.text,
3466
4768
  namespace: record.trigger.namespace,
3467
4769
  record,
@@ -3469,14 +4771,32 @@ function eventAutomationColumns(task) {
3469
4771
  title: title?.trim() || null
3470
4772
  };
3471
4773
  }
3472
- /** Decode an event Automation row with its SQL-backed fields. */
3473
- function parseEventAutomationRow(row) {
3474
- const task = eventAutomationSchema.parse({
4774
+ function eventAutomationRowInput(row) {
4775
+ return {
3475
4776
  ...row.record,
3476
4777
  createdAtMs: row.createdAtMs,
3477
4778
  creatorIdentityId: row.creatorIdentityId,
3478
4779
  id: row.id
4780
+ };
4781
+ }
4782
+ /** Decode an event Automation row with its SQL-backed fields. */
4783
+ function parseEventAutomationRow(row) {
4784
+ return withRowFields(row, eventAutomationSchema.parse(eventAutomationRowInput(row)));
4785
+ }
4786
+ /**
4787
+ * Decode a row for Event matching. Log and skip a row that does not parse,
4788
+ * so one bad row cannot stop matching for its whole namespace.
4789
+ */
4790
+ function readMatchableEventAutomationRow(row) {
4791
+ const parsed = eventAutomationSchema.safeParse(eventAutomationRowInput(row));
4792
+ if (parsed.success) return withRowFields(row, parsed.data);
4793
+ logWarn("event_automation.row.invalid", {
4794
+ "app.task.id": row.id,
4795
+ "app.task.type": "event",
4796
+ "error.message": parsed.error.message
3479
4797
  });
4798
+ }
4799
+ function withRowFields(row, task) {
3480
4800
  const title = row.title?.trim();
3481
4801
  return {
3482
4802
  ...task,
@@ -3562,25 +4882,99 @@ async function listDeletedEventAutomationsCreatedBy(db, user, limit) {
3562
4882
  return (await db.select().from(juniorAutomations).where(and(inArray(juniorAutomations.creatorIdentityId, identityIds), eq(juniorAutomations.kind, "event"), eq(juniorAutomations.status, "deleted"))).orderBy(desc(juniorAutomations.createdAtMs), desc(juniorAutomations.id)).limit(limit)).map(parseEventAutomationRow);
3563
4883
  }
3564
4884
  /**
3565
- * Collect match keys from live event automations for these identifiers and event types.
3566
- * Does not evaluate match values — only reports keys that filters use.
4885
+ * Select live rows whose identifier is one of these, and rows with no
4886
+ * identifier. A row with no identifier runs for every resource of its
4887
+ * namespace, so it gets every copy of an Event.
4888
+ */
4889
+ function eventIdentifierWhere(identifiers) {
4890
+ return or(isNull(juniorAutomations.identifier), ...identifiers.length > 0 ? [inArray(juniorAutomations.identifier, [...identifiers])] : []);
4891
+ }
4892
+ /** SQL for rows whose trigger selects any of these event types. */
4893
+ function eventTypesWhere(eventTypes) {
4894
+ return or(...eventTypes.map((eventType) => sql`${juniorAutomations.record}->'trigger'->'events' @> ${JSON.stringify([eventType])}::jsonb`));
4895
+ }
4896
+ /**
4897
+ * Return why a row must not run for an Event with one of these identifiers,
4898
+ * or nothing when it may.
4899
+ *
4900
+ * The identifier column and the saved trigger must agree. A row with no
4901
+ * identifier must also have a trigger that the enabled `catalog` accepts. A
4902
+ * plugin can later require a filter or stop letting an event run for every
4903
+ * resource, and the row must not stay broad.
4904
+ */
4905
+ function eventAutomationGateProblem(row, task, identifiers, catalog) {
4906
+ const identifier = task.trigger.identifier;
4907
+ if (identifier !== void 0) return identifier === row.identifier && identifiers.includes(identifier) ? void 0 : "The saved trigger has a different identifier from its row.";
4908
+ if (row.identifier !== null) return "The saved trigger has no identifier, but its row has one.";
4909
+ const problems = registeredEventTriggerProblems(catalog, task.trigger);
4910
+ return problems.length > 0 ? problems.map((problem) => problem.message).join(" ") : void 0;
4911
+ }
4912
+ /**
4913
+ * Read the rows that can match an Event: live rows of one team and
4914
+ * namespace whose identifier is one of these, or that have no identifier,
4915
+ * and that select one of these event types. Skip and log a row that does
4916
+ * not parse or that the gate rejects.
4917
+ */
4918
+ async function readGatedEventAutomations(db, input) {
4919
+ const rows = await db.select().from(juniorAutomations).where(and(eq(juniorAutomations.kind, "event"), eq(juniorAutomations.teamId, input.teamId), eq(juniorAutomations.namespace, input.namespace), eventIdentifierWhere(input.identifiers), eventTypesWhere(input.eventTypes), eq(juniorAutomations.status, "active"))).orderBy(asc(juniorAutomations.createdAtMs), asc(juniorAutomations.id));
4920
+ const plugins = automationEventCatalog(input.catalog);
4921
+ return rows.flatMap((row) => {
4922
+ const task = readMatchableEventAutomationRow(row);
4923
+ if (!task) return [];
4924
+ const problem = eventAutomationGateProblem(row, task, input.identifiers, plugins);
4925
+ if (problem === void 0) return [task];
4926
+ logWarn("event_automation.gate.rejected", {
4927
+ "app.task.id": task.id,
4928
+ "app.task.type": "event",
4929
+ "app.event_automation.skip_reason": problem
4930
+ });
4931
+ return [];
4932
+ });
4933
+ }
4934
+ /** The filters of a match whose keys are in this data. */
4935
+ function knownMatch(match, data) {
4936
+ if (!match) return void 0;
4937
+ return Object.fromEntries(Object.entries(match).filter(([key]) => data[key] !== void 0));
4938
+ }
4939
+ /**
4940
+ * Collect match keys from live event automations for these identifiers and
4941
+ * event types, and from automations with no identifier. Does not evaluate
4942
+ * match values, except for automations with no identifier: one whose
4943
+ * filters on the keys in `data` fail adds no keys. So a broad trigger for
4944
+ * one repository does not make every repository load its lazy data.
3567
4945
  */
3568
4946
  async function collectEventAutomationMatchKeys(db, input) {
3569
- const eventTypes = new Set(input.eventTypes.map((eventType) => eventType.trim()).filter(Boolean));
4947
+ const eventTypes = [...new Set(input.eventTypes.map((eventType) => eventType.trim()).filter(Boolean))];
3570
4948
  const identifiers = [...new Set(input.identifiers.map((value) => value.trim()).filter(Boolean))];
3571
- if (eventTypes.size === 0 || identifiers.length === 0) return [];
3572
- const rows = await db.select().from(juniorAutomations).where(and(eq(juniorAutomations.kind, "event"), eq(juniorAutomations.teamId, input.teamId), eq(juniorAutomations.namespace, input.namespace), inArray(juniorAutomations.identifier, identifiers), eq(juniorAutomations.status, "active"))).orderBy(asc(juniorAutomations.createdAtMs), asc(juniorAutomations.id));
4949
+ if (eventTypes.length === 0) return [];
4950
+ const tasks = await readGatedEventAutomations(db, {
4951
+ catalog: input.catalog,
4952
+ eventTypes,
4953
+ identifiers,
4954
+ namespace: input.namespace,
4955
+ teamId: input.teamId
4956
+ });
3573
4957
  const keys = /* @__PURE__ */ new Set();
3574
- for (const row of rows) {
3575
- const task = parseEventAutomationRow(row);
3576
- if (!task.trigger.events.some((eventType) => eventTypes.has(eventType))) continue;
4958
+ for (const task of tasks) {
4959
+ if (task.trigger.identifier === void 0 && input.data && !eventMatches(knownMatch(task.trigger.match, input.data), input.data)) continue;
3577
4960
  for (const key of Object.keys(task.trigger.match ?? {})) keys.add(key);
3578
4961
  }
3579
4962
  return [...keys].sort();
3580
4963
  }
3581
- /** Find every live task matching one normalized event. */
3582
- async function findMatchingEventAutomations(db, event, teamId) {
3583
- return (await db.select().from(juniorAutomations).where(and(eq(juniorAutomations.kind, "event"), eq(juniorAutomations.teamId, teamId), eq(juniorAutomations.namespace, event.namespace), eq(juniorAutomations.identifier, event.identifier), eq(juniorAutomations.status, "active"))).orderBy(asc(juniorAutomations.createdAtMs), asc(juniorAutomations.id))).map(parseEventAutomationRow).filter((task) => task.trigger.events.includes(event.eventType) && eventMatches(task.trigger.match, event.data));
4964
+ /**
4965
+ * Find every live task matching one normalized event: tasks for its exact
4966
+ * identifier, and tasks with no identifier whose filters match. The gate
4967
+ * skips a row whose trigger and identifier column disagree, and a row with
4968
+ * no identifier whose trigger the enabled `catalog` rejects.
4969
+ */
4970
+ async function findMatchingEventAutomations(db, event, teamId, catalog) {
4971
+ return (await readGatedEventAutomations(db, {
4972
+ catalog,
4973
+ eventTypes: [event.eventType],
4974
+ identifiers: [event.identifier],
4975
+ namespace: event.namespace,
4976
+ teamId
4977
+ })).filter((task) => task.trigger.events.includes(event.eventType) && eventMatches(task.trigger.match, event.data));
3584
4978
  }
3585
4979
  /**
3586
4980
  * Change lifecycle under the same row lock as edits, without changing
@@ -3753,4 +5147,4 @@ async function saveScheduledAutomation(db, task, expectedRevision, editedBy) {
3753
5147
  async function saveScheduledAutomationInLock(db, task, current) {
3754
5148
  await writeScheduledAutomation(db, requireStoredTask(task), current);
3755
5149
  }
3756
- export { persistConversationMessageSummaries as $, isToolResultMessage as $t, commitAcceptedReply as A, abandonTurnRecord as At, recordAttachmentsDelivered as B, buildTurnErrorResponse as Bt, automationVersionOf as C, getConfigDefaults as Cn, canReusePendingAuthLink as Ct, AutomationConflictError as D, continuableMessages as Dt, sameDefinitionValue as E, wasPendingAuthStopped as Et, loadProjection as F, recordTurnSummary as Ft, recordMcpProviderConnected as G, getPiMessageRole as Gt, recordAuthorizationCompleted as H, isTurnExecutionLimitExceededError as Ht, loadTurnProjection as I, TURN_LIMIT_DISABLED_REASON as It, recordTurnRoute as J, hasRuntimeTurnContext as Jt, recordSubscribedReplyRoute as K, getTerminalAssistantMessages as Kt, loadTurnRoute as L, TurnBudgetExceededError as Lt, loadConnectedMcpProviders as M, failTurnRecord as Mt, loadConversationProjection as N, getTurnRecord as Nt, automationRevision as O, loadTurnCheckpoint as Ot, loadLastModelProfile as P, listTurnSummaries as Pt, projectConversationMessages as Q, isToolResultError as Qt, openConversationProjection as R, TurnSliceLimitExceededError as Rt, automationDefinition as S, piMessageFromHistoryItem as Sn, abandonReplacedPendingAuth as St, readAutomationVersion as T, getConversationPendingAuth as Tt, recordAuthorizationRequested as U, persistWithRetry as Ut, recordAuthenticationLinked as V, createTurnLimitGate as Vt, recordGuardianActionReviewed as W, extractAssistantText as Wt, hydrateConversationMessages as X, isAssistantMessage as Xt, appendConversationMessages as Y, instructionTextForProjection as Yt, persistConversationMessages as Z, isContinuablePiBoundary as Zt, parseEventAutomationRow as _, isExperimentalFeatureEnabled as _n, createPluginAuthOrchestration as _t, saveScheduledAutomation as a, SESSION_CONTEXT_SECTION_TAG as an, buildConversationContext as at, admitEventAutomationRun as b, configValueSchema as bn, AuthorizationFlowDisabledError as bt, withScheduledAutomationLock as c, buildDeterministicTurnId as cn, getConversationMessageSlackTs as ct, createEventAutomation as d, getSubscribedReplyPreflightDecision as dn, normalizeConversationText as dt, normalizeToolNameFromResult as en, resolveConversationTitle as et, deleteEventAutomation as f, getThreadStopDecision as fn, recordDeliveredAssistantMessage as ft, listEventAutomationsForTeam as g, getExperimentalFeatures as gn, PluginCredentialFailureError as gt, listDeletedEventAutomationsCreatedBy as h, getAgentContextInputLimitTokens as hn, createPluginEgress as ht, readScheduledAutomation as i, REQUEST_CONTEXT_SECTION_TAG as in, appendThreadContextMessages as it, commitMessages as j, completeTurnRecord as jt, requireAutomationRevision as k, saveTurnCheckpoint as kt, blockEventAutomation as l, SubscribedReplyReason as ln, isHumanConversationMessage as lt, getEventAutomation as m, getAgentContextCompactionTriggerTokens as mn, upsertConversationMessage as mt, listScheduledAutomationsForTeam as n, stripRuntimeTurnContext as nn, fallbackShortTitle as nt, saveScheduledAutomationInLock as o, TURN_CONTEXT_TAG as on, createConversationMemoryService as ot, findMatchingEventAutomations as p, estimateTextTokens as pn, turnHasReply as pt, recordToolExecutionStarted as q, getUserMessageInstructionText as qt, parseScheduledAutomationRow as r, trimTrailingAssistantMessages as rn, resolveTaskTitle as rt, scheduledRunId as s, buildDeterministicAssistantMessageId as sn, estimateConversationContextTokens as st, createScheduledAutomation as t, retainRuntimeTurnContext as tn, settleConversationTitleWork as tt, collectEventAutomationMatchKeys as u, decideSubscribedThreadReply as un, markConversationMessage as ut, saveEventAutomation as v, setExperimentalFeatures as vn, authorizationId as vt, listAutomationVersions as w, setConfigDefaults as wn, clearPendingAuth as wt, eventAutomationRunLimitReason as x, historyItemFromPiMessage as xn, AuthorizationPauseError as xt, setEventAutomationStatus as y, createDurableLocationConfigurationService as yn, AuthPausePersistenceError as yt, recordAgentsInstructionsUpdated as z, TurnToolCallLimitExceededError as zt };
5150
+ export { recordAgentsInstructionsUpdated as $, buildDeterministicTurnId as $n, appendExclusiveInboundMessage as $t, CORE_EVENT_NAMESPACE as A, TurnToolCallLimitExceededError as An, PluginCredentialFailureError as At, registeredEventTypeSchema as B, instructionTextForProjection as Bn, wasPendingAuthStopped as Bt, automationVersionOf as C, requestConversationWork as Cn, isHumanConversationMessage as Ct, AutomationConflictError as D, TURN_LIMIT_DISABLED_REASON as Dn, turnHasReply as Dt, sameDefinitionValue as E, stopConversationWork as En, recordDeliveredAssistantMessage as Et, eventNamespaceSchema as F, extractAssistantText as Fn, AuthorizationPauseError as Ft, commitMessages as G, normalizeToolNameFromResult as Gn, completeTurnRecord as Gt, requireEventIdentifier as H, isContinuablePiBoundary as Hn, loadTurnCheckpoint as Ht, eventWatchUnavailable as I, getPiMessageRole as In, abandonReplacedPendingAuth as It, loadLastModelProfile as J, trimTrailingAssistantMessages as Jn, listTurnSummaries as Jt, loadConnectedMcpProviders as K, retainRuntimeTurnContext as Kn, failTurnRecord as Kt, hasEventCatalogEntries as L, getTerminalAssistantMessages as Ln, canReusePendingAuthLink as Lt, describeRegisteredEventTrigger as M, createTurnLimitGate as Mn, authorizationId as Mt, eventCatalogEntry as N, isTurnExecutionLimitExceededError as Nn, AuthPausePersistenceError as Nt, automationRevision as O, TurnBudgetExceededError as On, upsertConversationMessage as Ot, eventGuidance as P, persistWithRetry as Pn, AuthorizationFlowDisabledError as Pt, openConversationProjection as Q, buildDeterministicAssistantMessageId as Qn, ackMessages as Qt, registeredEventMatchSchema as R, getUserMessageInstructionText as Rn, clearPendingAuth as Rt, automationDefinition as S, requestAnotherSlice as Sn, getConversationMessageSlackTs as St, readAutomationVersion as T, steerWebPendingMessages as Tn, normalizeConversationText as Tt, requireValidEventTrigger as U, isToolResultError as Un, saveTurnCheckpoint as Ut, registeredResourceTypeSchema as V, isAssistantMessage as Vn, continuableMessages as Vt, commitAcceptedReply as W, isToolResultMessage as Wn, abandonTurnRecord as Wt, loadTurnProjection as X, SESSION_CONTEXT_SECTION_TAG as Xn, CONVERSATION_WORK_CHECK_IN_INTERVAL_MS as Xt, loadProjection as Y, REQUEST_CONTEXT_SECTION_TAG as Yn, recordTurnSummary as Yt, loadTurnRoute as Z, TURN_CONTEXT_TAG as Zn, CONVERSATION_WORK_STALE_ENQUEUE_MS as Zt, parseEventAutomationRow as _, markConversationWorkEnqueued as _n, resolveTaskTitle as _t, saveScheduledAutomation as a, clearExpiredConversationLease as an, getAgentContextCompactionTriggerTokens as ar, recordMcpProviderConnected as at, admitEventAutomationRun as b, releaseConversationWork as bn, createConversationMemoryService as bt, withScheduledAutomationLock as c, deadLetterAttempt as cn, isExperimentalFeatureEnabled as cr, recordTurnRoute as ct, createEventAutomation as d, getConversationWorkState as dn, configValueSchema as dr, persistConversationMessages as dt, appendInboundMessage as en, SubscribedReplyReason as er, recordAttachmentsDelivered as et, deleteEventAutomation as f, hasConversationStop as fn, historyItemFromPiMessage as fr, projectConversationMessages as ft, listEventAutomationsForTeam as g, listActiveConversationIds as gn, fallbackShortTitle as gt, listDeletedEventAutomationsCreatedBy as h, isInvalidConversationRecordError as hn, setConfigDefaults as hr, settleConversationTitleWork as ht, readScheduledAutomation as i, clearConsumedConversationWake as in, estimateTextTokens as ir, recordGuardianActionReviewed as it, automationEventCatalog as j, buildTurnErrorResponse as jn, createPluginAuthOrchestration as jt, requireAutomationRevision as k, TurnSliceLimitExceededError as kn, createPluginEgress as kt, blockEventAutomation as l, drainConversationMailbox as ln, setExperimentalFeatures as lr, appendConversationMessages as lt, getEventAutomation as m, isFinalAttempt as mn, getConfigDefaults as mr, resolveConversationTitle as mt, listScheduledAutomationsForTeam as n, cancelHumanFacingPendingMessages as nn, getSubscribedReplyPreflightDecision as nr, recordAuthorizationCompleted as nt, saveScheduledAutomationInLock as o, completeConversationStop as on, getAgentContextInputLimitTokens as or, recordSubscribedReplyRoute as ot, findMatchingEventAutomations as p, hasRunnableConversationWork as pn, piMessageFromHistoryItem as pr, persistConversationMessageSummaries as pt, loadConversationProjection as q, stripRuntimeTurnContext as qn, getTurnRecord as qt, parseScheduledAutomationRow as r, checkInConversationWork as rn, getThreadStopDecision as rr, recordAuthorizationRequested as rt, scheduledRunId as s, completeConversationWork as sn, getExperimentalFeatures as sr, recordToolExecutionStarted as st, createScheduledAutomation as t, beginConversationResume as tn, decideSubscribedThreadReply as tr, recordAuthenticationLinked as tt, collectEventAutomationMatchKeys as u, getConversation as un, createDurableLocationConfigurationService as ur, hydrateConversationMessages as ut, saveEventAutomation as v, recordAttemptFailure as vn, appendThreadContextMessages as vt, listAutomationVersions as w, startConversationWork as wn, markConversationMessage as wt, eventAutomationRunLimitReason as x, removeActiveConversation as xn, estimateConversationContextTokens as xt, setEventAutomationStatus as y, recordConversationRetry as yn, buildConversationContext as yt, registeredEventTriggerProblems as z, hasRuntimeTurnContext as zn, getConversationPendingAuth as zt };