@sentry/junior 0.240.0 → 0.241.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 (56) hide show
  1. package/dist/{admin-D6fRPRgC.js → admin-CJkjl-aN.js} +1 -1
  2. package/dist/{agent-hooks-JFbgedMZ.js → agent-hooks-C5zYOaJk.js} +1952 -1952
  3. package/dist/{agent-runner-0IjJxsL1.js → agent-runner-CMZ6lFQe.js} +336 -1125
  4. package/dist/api/schema.d.ts +9 -0
  5. package/dist/api/schema.js +2 -2
  6. package/dist/api.js +21 -21
  7. package/dist/app.js +104 -31
  8. package/dist/{automation-B5_c8hEb.js → automation-gznOMaQ2.js} +11 -2
  9. package/dist/{briefs-BGWTp-nz.js → briefs-CsQbFjRi.js} +2 -2
  10. package/dist/{catalog-runtime-DdigNl7s.js → catalog-runtime-Tz0O2MZe.js} +1 -1
  11. package/dist/cli/chat.js +12 -12
  12. package/dist/cli/check.js +3 -3
  13. package/dist/cli/main.js +2 -2
  14. package/dist/cli/plugins.js +4 -4
  15. package/dist/cli/snapshot-create.js +3 -3
  16. package/dist/cli/upgrade.js +3 -3
  17. package/dist/{client-Cn73E7br.js → client-CRZ-i1tH.js} +2 -2
  18. package/dist/{config-B0S86yw9.js → config-CCI82OTD.js} +2 -2
  19. package/dist/{conversation-privacy-BKlX7D_R.js → conversation-privacy-D5Byl5eC.js} +1 -1
  20. package/dist/{credential-sync-DeNQm7st.js → credential-sync-DUpSxUum.js} +14 -14
  21. package/dist/{executor-BQ-A5nov.js → executor-CewKi7EG.js} +4 -2
  22. package/dist/{images-C7OKd6uy.js → images-DY6Pxq4w.js} +2 -2
  23. package/dist/instrumentation.js +2 -2
  24. package/dist/{job-queue-DgXLqoeB.js → job-queue-C6o2aJQ6.js} +1 -1
  25. package/dist/{logging-BSNF167N.js → logging-LQnLlPf8.js} +1 -1
  26. package/dist/nitro.js +3 -3
  27. package/dist/{oauth-authorization-BM7g61od.js → oauth-authorization-Clzkm-Ts.js} +1 -1
  28. package/dist/{oauth-callback-server-Dw-BwI6x.js → oauth-callback-server-DcYAJSEz.js} +2 -2
  29. package/dist/{pending-auth-Chn_EA1_.js → pending-auth-CT3ojhKe.js} +2 -2
  30. package/dist/{profile-3t9H_IwL.js → profile-mWuVZ8ht.js} +4 -4
  31. package/dist/{registry-DLZlAhdP.js → registry-DPDH6s5G.js} +1 -1
  32. package/dist/{resolve-9_cIPQsq.js → resolve-BAN8BSGy.js} +2 -2
  33. package/dist/{runner-C_eUbFyp.js → runner-CqXZJfzk.js} +9 -9
  34. package/dist/{sandbox-egress-signals-C1ycxTNA.js → sandbox-egress-signals-DFUUokws.js} +1 -1
  35. package/dist/{schema-C4XwHTY4.js → schema-Cabm4jcj.js} +1 -1
  36. package/dist/{skills-Dqgjr_Tl.js → skills-BtHhSATy.js} +2 -2
  37. package/dist/{spawn-BfEr3t7b.js → spawn-BszkFJKJ.js} +2 -2
  38. package/dist/{task-queue-CuFZDOXn.js → task-queue-1LP6benz.js} +3 -3
  39. package/dist/{task-runner-c7gwV8dj.js → task-runner-kLN3HC-_.js} +54 -12
  40. package/dist/{checkpoint-C1ZnSz7r.js → tasks-CO52vLkI.js} +1056 -14
  41. package/dist/{thread-state-exTu-G1w.js → thread-state-CaFRG5Mh.js} +6 -6
  42. package/dist/{turn-failure-response-DFp99ziF.js → turn-failure-response-Dmovuiq1.js} +9 -8
  43. package/dist/{turn-lifecycle-C4TjuOzG.js → turn-lifecycle-DGqZgU6J.js} +1 -1
  44. package/dist/{turn-session-routing-Vpg8jUD1.js → turn-session-routing-B_v8Wbn4.js} +1 -1
  45. package/dist/{validation-D2lULEG9.js → validation-B4L2Rmiw.js} +1 -1
  46. package/dist/{version-BjBMCc2o.js → version-DiR36OaN.js} +1 -1
  47. package/dist/version.js +1 -1
  48. package/dist/{viewer-XE3nFqKM.js → viewer-D6stAU7h.js} +3 -3
  49. package/dist/{web-authorization-8E8IrWOj.js → web-authorization-DLMBx2nL.js} +6 -6
  50. package/dist/{web-input-DF4GGJrf.js → web-input---xuox1t.js} +135 -129
  51. package/dist/{work-Dt_JjfbX.js → work-G-HdoOSF.js} +7 -7
  52. package/dist/{worker-BrbnLmD3.js → worker-CDUDfL9P.js} +5 -5
  53. package/migrations/0049_event_automation_status_reason.sql +1 -0
  54. package/migrations/meta/0049_snapshot.json +3581 -0
  55. package/migrations/meta/_journal.json +8 -1
  56. package/package.json +4 -4
@@ -1,17 +1,21 @@
1
- import { d as unwrapCurrentInstruction, f as escapeXml, i as completeText, o as resolveGatewayModel } from "./client-Cn73E7br.js";
2
- import { d as logException, o as getActiveTraceId, p as logWarn, t as buildTurnFailureResponse, v as setSpanAttributes } from "./logging-BSNF167N.js";
3
- import { n as botConfig } from "./config-B0S86yw9.js";
4
- import { c as toStoredSlackActor, g as parseSlackMessageTs } from "./actor-BTtLaeWy.js";
5
- import { c as getConversationEventStore, f as getSqlExecutor, m as withConversationEventLock, p as createSqlConversationEventStore, u as getConversationStore, v as startLocationThread, x as addAgentTurnUsage, y as bindProviderConversation } from "./viewer-XE3nFqKM.js";
1
+ import { d as unwrapCurrentInstruction, f as escapeXml, i as completeText, o as resolveGatewayModel } from "./client-CRZ-i1tH.js";
2
+ import { d as logException, o as getActiveTraceId, p as logWarn, s as getLogContextAttributes, t as buildTurnFailureResponse, v as setSpanAttributes } from "./logging-LQnLlPf8.js";
3
+ import { n as captureMessage } from "./sentry-CgWZM_kV.js";
4
+ import { n as botConfig, v as normalizeSlackEmojiName } from "./config-CCI82OTD.js";
5
+ import { c as toStoredSlackActor, f as parseSlackUserId, g as parseSlackMessageTs } from "./actor-BTtLaeWy.js";
6
+ import { c as getConversationEventStore, f as getSqlExecutor, m as withConversationEventLock, p as createSqlConversationEventStore, u as getConversationStore, v as startLocationThread, x as addAgentTurnUsage, y as bindProviderConversation } from "./viewer-D6stAU7h.js";
7
+ import { a as juniorSchedulerRuns, o as juniorSchedulerTasks, s as juniorAutomationVersions, u as juniorEventAutomations } from "./executor-CewKi7EG.js";
6
8
  import { d as instructionActors, f as instructionProvenanceFor, n as agentHistoryItemSchema, u as contextProvenance } from "./history-BLTUgFYO.js";
7
- import { p as credentialSubjectSchema } from "./registry-DLZlAhdP.js";
8
- import { Jt as agentsInstructionsUpdatedEvent, Xt as authenticationUnlinkedEvent, Yt as authenticationLinkedEvent, qt as JUNIOR_NATIVE_EVENT_NAMESPACE } from "./agent-hooks-JFbgedMZ.js";
9
+ import { p as credentialSubjectSchema } from "./registry-DPDH6s5G.js";
10
+ import { Ft as SlackActionError, Ht as normalizeSlackConversationId, Jt as agentsInstructionsUpdatedEvent, Wt as withSlackRetries, Xt as authenticationUnlinkedEvent, Yt as authenticationLinkedEvent, n as applyPluginFormatMarkdown, qt as JUNIOR_NATIVE_EVENT_NAMESPACE, zt as getSlackClient } from "./agent-hooks-C5zYOaJk.js";
9
11
  import { H as messageCardRef } from "./conversation-DwD-vquD.js";
10
- import { D as fenceLock, O as withLock, T as MUTATION_LOCK_TTL_MS, k as sleep, w as getStateAdapter } from "./profile-3t9H_IwL.js";
12
+ import { D as fenceLock, O as withLock, T as MUTATION_LOCK_TTL_MS, k as sleep, w as getStateAdapter } from "./profile-mWuVZ8ht.js";
13
+ import { A as eventAutomationSchema, D as scheduledRunSchema, E as scheduledAutomationSchema, S as automationVersionSchema } from "./automation-gznOMaQ2.js";
11
14
  import { isDeepStrictEqual } from "node:util";
12
15
  import { z } from "zod";
13
16
  import { createHash } from "node:crypto";
14
- import { actorSchema, sourceSchema } from "@sentry/junior-plugin-api";
17
+ import { and, asc, desc, eq, inArray, ne, notInArray, or, sql } from "drizzle-orm";
18
+ import { actorSchema, eventMatches, slackDestinationSchema, sourceSchema, taskOutcomeSchema } from "@sentry/junior-plugin-api";
15
19
  //#region src/chat/pi/messages.ts
16
20
  /** Permissive schema for durable Pi SDK messages whose content shape may evolve. */
17
21
  const piMessageSchema = z.object({ role: z.string() }).passthrough().transform((value) => value);
@@ -711,13 +715,13 @@ function toStoredConversationMessage(message) {
711
715
  createdAtMs: message.createdAtMs
712
716
  };
713
717
  }
714
- function canonicalJson(value) {
715
- if (Array.isArray(value)) return value.map(canonicalJson);
718
+ function canonicalJson$1(value) {
719
+ if (Array.isArray(value)) return value.map(canonicalJson$1);
716
720
  if (!value || typeof value !== "object") return value;
717
- return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalJson(item)]));
721
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalJson$1(item)]));
718
722
  }
719
723
  function messageStateKey(message) {
720
- return createHash("sha256").update(JSON.stringify(canonicalJson(message))).digest("hex").slice(0, 32);
724
+ return createHash("sha256").update(JSON.stringify(canonicalJson$1(message))).digest("hex").slice(0, 32);
721
725
  }
722
726
  /** Hydrate the current message working set from canonical conversation events. */
723
727
  async function hydrateConversationMessages(args) {
@@ -1241,6 +1245,15 @@ async function recordGuardianActionReviewed(args) {
1241
1245
  }]);
1242
1246
  }
1243
1247
  //#endregion
1248
+ //#region src/chat/tools/execution/tool-input-error.ts
1249
+ /** Thrown when a tool fails due to invalid model/user input, not a system error. */
1250
+ var ToolInputError = class extends Error {
1251
+ constructor(message, options) {
1252
+ super(message, options);
1253
+ this.name = "ToolInputError";
1254
+ }
1255
+ };
1256
+ //#endregion
1244
1257
  //#region src/chat/turn-context-tag.ts
1245
1258
  /**
1246
1259
  * Canonical tag name for the runtime turn context block injected into Pi
@@ -2159,4 +2172,1033 @@ async function saveDone(args) {
2159
2172
  });
2160
2173
  }
2161
2174
  //#endregion
2162
- export { resolveConversationTitle as $, commitAcceptedReply as A, recordAuthenticationLinked as B, isToolResultError as C, stripRuntimeTurnContext as D, retainRuntimeTurnContext as E, loadTurnProjection as F, recordSubscribedReplyRoute as G, recordAuthorizationRequested as H, loadTurnRoute as I, appendConversationMessages as J, recordToolExecutionStarted as K, openConversationProjection as L, loadConnectedMcpProviders as M, loadConversationProjection as N, trimTrailingAssistantMessages as O, loadProjection as P, persistConversationMessageSummaries as Q, recordAgentsInstructionsUpdated as R, isContinuablePiBoundary as S, normalizeToolNameFromResult as T, recordGuardianActionReviewed as U, recordAuthorizationCompleted as V, recordMcpProviderConnected as W, persistConversationMessages as X, hydrateConversationMessages as Y, projectConversationMessages as Z, getTerminalAssistantMessages as _, historyItemFromPiMessage as _t, completeTurnRecord as a, createConversationMemoryService as at, instructionTextForProjection as b, listTurnSummaries as c, isHumanConversationMessage as ct, TurnSliceLimitExceededError as d, recordDeliveredAssistantMessage as dt, settleConversationTitleWork as et, assertTurnToolCallLimit as f, turnHasReply as ft, getPiMessageRole as g, getAgentContextInputLimitTokens as gt, extractAssistantText as h, getAgentContextCompactionTriggerTokens as ht, abandonTurnRecord as i, buildConversationContext as it, commitMessages as j, TURN_CONTEXT_TAG as k, recordTurnSummary as l, markConversationMessage as lt, isTurnExecutionLimitExceededError as m, estimateTextTokens as mt, loadTurnCheckpoint as n, resolveTaskTitle as nt, failTurnRecord as o, estimateConversationContextTokens as ot, buildTurnErrorResponse as p, upsertConversationMessage as pt, recordTurnRoute as q, saveTurnCheckpoint as r, appendThreadContextMessages as rt, getTurnRecord as s, getConversationMessageSlackTs as st, continuableMessages as t, fallbackShortTitle as tt, persistWithRetry as u, normalizeConversationText as ut, getUserMessageInstructionText as v, piMessageFromHistoryItem as vt, isToolResultMessage as w, isAssistantMessage as x, hasRuntimeTurnContext as y, recordAttachmentsDelivered as z };
2175
+ //#region src/chat/slack/post-warning.ts
2176
+ const codes = /* @__PURE__ */ new Set([
2177
+ "invalid_metadata_format",
2178
+ "invalid_metadata_schema",
2179
+ "metadata_too_large",
2180
+ "invalid_arguments",
2181
+ "invalid_blocks",
2182
+ "missing_scope",
2183
+ "missing_charset",
2184
+ "superfluous_charset",
2185
+ "message_truncated"
2186
+ ]);
2187
+ const pathFields = /* @__PURE__ */ new Set([
2188
+ "metadata",
2189
+ "event_type",
2190
+ "event_payload",
2191
+ "entities",
2192
+ "entity_type",
2193
+ "entity_payload",
2194
+ "external_ref",
2195
+ "id",
2196
+ "type",
2197
+ "url",
2198
+ "attributes",
2199
+ "title",
2200
+ "text",
2201
+ "display_id",
2202
+ "display_type",
2203
+ "product_name",
2204
+ "fields",
2205
+ "status",
2206
+ "custom_fields",
2207
+ "key",
2208
+ "label",
2209
+ "value",
2210
+ "long",
2211
+ "format",
2212
+ "link",
2213
+ "display_order",
2214
+ "app_unfurl_url",
2215
+ "app_id",
2216
+ "created_by",
2217
+ "created_at",
2218
+ "updated_by",
2219
+ "updated_at",
2220
+ "full_size_preview",
2221
+ "preview_url",
2222
+ "mime_type"
2223
+ ]);
2224
+ const diagnosticWords = new Set(`WARN ERROR Message message metadata was incorrectly formatted The the will be
2225
+ ignored as a result For for event entity refer to following errors error
2226
+ missing required field fields property properties additional unexpected unknown
2227
+ unsupported invalid expected received actual type types must should match matches
2228
+ matching does not is are has have of at in on and or an only allowed allow
2229
+ value values string number integer boolean object array null minimum maximum
2230
+ length min max items item empty nonempty too long short large small size limit
2231
+ exceeded exceeds less greater than equal valid format schema validation failed
2232
+ failure cannot contain contains found one any all none enum pattern definition
2233
+ defined constraint constraints satisfy satisfies with without provided specified
2234
+ unrecognized unrecognised recognized recognised extraneous disallowed duplicate
2235
+ unique malformed nested root exactly needs need requires requirement supported
2236
+ json-pointer input different schema_uri schema_id schemas additionalProperties
2237
+ minLength maxLength minItems maxItems anyOf oneOf allOf`.split(/\s+/));
2238
+ function summarizeDiagnostic(value) {
2239
+ return value.slice(0, 2e3).replace(/\b(?:https?:\/\/|www\.)[^\s"'`<>]+/gi, "[value]").replace(/\/[^\s"'`[\],;()]*|[\p{L}\p{N}\p{M}_]+(?:[-./:@=+%~][\p{L}\p{N}\p{M}_]+)*|[^\s\w[\]{}():,;."'`/*=<>!?+-]/gu, (token) => {
2240
+ if (token.startsWith("/")) return token.split("/").slice(0, 17).map((part, index) => index === 0 ? "" : pathFields.has(part) ? part : "*").join("/");
2241
+ return codes.has(token) || pathFields.has(token) || diagnosticWords.has(token) ? token : "[value]";
2242
+ }).replace(/\s+/g, " ").slice(0, 2e3);
2243
+ }
2244
+ /** Report Slack warnings once, on the request span, without retrying the post. */
2245
+ function captureSlackPostWarning(response, attributes) {
2246
+ const warnings = response.response_metadata?.warnings ?? [];
2247
+ const messages = response.response_metadata?.messages ?? [];
2248
+ const knownCodes = [...new Set(warnings.slice(0, 20).filter((code) => codes.has(code)))].sort();
2249
+ const diagnostics = {
2250
+ "app.slack.warning_count": warnings.length,
2251
+ "app.slack.response_message_count": messages.length,
2252
+ "app.slack.diagnostic_codes": knownCodes
2253
+ };
2254
+ setSpanAttributes(diagnostics);
2255
+ if (!warnings.length && !messages.length) return;
2256
+ captureMessage("Slack chat.postMessage returned warnings", {
2257
+ level: "warning",
2258
+ fingerprint: ["slack.chat.postMessage.warning", ...knownCodes.length ? knownCodes : ["unknown"]],
2259
+ tags: { "app.slack.method": "chat.postMessage" },
2260
+ extra: {
2261
+ ...getLogContextAttributes(),
2262
+ ...attributes,
2263
+ ...diagnostics,
2264
+ "app.slack.response_diagnostics": {
2265
+ warnings: warnings.slice(0, 20).map(summarizeDiagnostic),
2266
+ messages: messages.slice(0, 20).map(summarizeDiagnostic)
2267
+ }
2268
+ }
2269
+ });
2270
+ }
2271
+ //#endregion
2272
+ //#region src/chat/slack/work-object.ts
2273
+ const url = z.url({ protocol: /^https?$/ });
2274
+ const textField = z.strictObject({
2275
+ type: z.literal("string"),
2276
+ key: z.string().min(1),
2277
+ label: z.string().min(1),
2278
+ value: z.string(),
2279
+ long: z.boolean().optional(),
2280
+ format: z.literal("markdown").optional(),
2281
+ link: url.optional()
2282
+ }).refine((field) => !field.format || !field.link, { message: "Slack string fields cannot combine format and link" });
2283
+ const customField = z.discriminatedUnion("type", [textField, z.strictObject({
2284
+ type: z.literal("slack#/types/timestamp"),
2285
+ key: z.string().min(1),
2286
+ label: z.string().min(1),
2287
+ value: z.number().int(),
2288
+ link: url.optional()
2289
+ })]);
2290
+ const payload = z.strictObject({
2291
+ attributes: z.strictObject({
2292
+ title: z.strictObject({ text: z.string().min(1) }),
2293
+ display_id: z.string().optional(),
2294
+ display_type: z.string().optional(),
2295
+ product_name: z.string().optional(),
2296
+ product_icon: z.strictObject({
2297
+ url,
2298
+ alt_text: z.string().min(1)
2299
+ }).optional()
2300
+ }),
2301
+ custom_fields: z.array(customField).optional(),
2302
+ display_order: z.array(z.string()).optional()
2303
+ });
2304
+ /** Limit IDs to Junior's safe subset; Slack silently rejects JSON punctuation. */
2305
+ const slackExternalRefIdSchema = z.string().min(1).regex(/^[A-Za-z0-9_-]+$/, "Unsupported Slack external reference ID");
2306
+ const entity = z.strictObject({
2307
+ external_ref: z.strictObject({
2308
+ id: slackExternalRefIdSchema,
2309
+ type: z.string().min(1).optional()
2310
+ }),
2311
+ url
2312
+ });
2313
+ /** Validate notification and detail metadata before Slack can ignore bad fields. */
2314
+ const slackEntitySchema = z.discriminatedUnion("entity_type", [entity.extend({
2315
+ entity_type: z.literal("slack#/entities/item"),
2316
+ entity_payload: payload.extend({ fields: z.never().optional() })
2317
+ }), entity.extend({
2318
+ entity_type: z.literal("slack#/entities/task"),
2319
+ entity_payload: payload.extend({ fields: z.strictObject({ status: z.strictObject({ value: z.string() }).optional() }).optional() })
2320
+ })]);
2321
+ //#endregion
2322
+ //#region src/chat/slack/outbound.ts
2323
+ const MAX_SLACK_MESSAGE_TEXT_CHARS = 4e4;
2324
+ function requireSlackConversationId(channelId, action) {
2325
+ const normalized = normalizeSlackConversationId(channelId);
2326
+ if (!normalized) throw new Error(`${action} requires a valid channel ID`);
2327
+ return normalized;
2328
+ }
2329
+ function requireSlackThreadTimestamp(threadTs, action) {
2330
+ const normalized = threadTs.trim();
2331
+ if (!normalized) throw new Error(`${action} requires a thread timestamp`);
2332
+ return normalized;
2333
+ }
2334
+ function requireSlackMessageTimestamp(timestamp, action) {
2335
+ const normalized = parseSlackMessageTs(timestamp);
2336
+ if (!normalized) throw new Error(`${action} requires a target message timestamp`);
2337
+ return normalized;
2338
+ }
2339
+ function requireSlackMessageText(text, action) {
2340
+ if (text.trim().length === 0) throw new Error(`${action} requires non-empty text`);
2341
+ if (text.length > MAX_SLACK_MESSAGE_TEXT_CHARS) throw new Error(`${action} text exceeds Slack's 40000 character truncation limit`);
2342
+ return text;
2343
+ }
2344
+ /** Resolve a Slack message permalink without making read-only workflows fail on lookup errors. */
2345
+ async function getSlackMessagePermalink(args) {
2346
+ try {
2347
+ return (await withSlackRetries(() => getSlackClient().chat.getPermalink({
2348
+ channel: args.channelId,
2349
+ message_ts: args.messageTs
2350
+ }), 3, {
2351
+ action: "chat.getPermalink",
2352
+ idempotent: true,
2353
+ spanAttributes: {
2354
+ "app.slack.channel_id": args.channelId,
2355
+ "app.slack.message_ts": args.messageTs
2356
+ }
2357
+ })).permalink;
2358
+ } catch {
2359
+ return;
2360
+ }
2361
+ }
2362
+ /** Open the bot's direct message with one Slack user and return its channel ID. */
2363
+ async function openSlackDirectMessage(userId) {
2364
+ const channelId = (await withSlackRetries(() => getSlackClient().conversations.open({ users: userId }), 3, { action: "conversations.open" })).channel?.id;
2365
+ if (!channelId) throw new Error("Slack did not return a direct message channel.");
2366
+ return channelId;
2367
+ }
2368
+ /** Post Slack `mrkdwn` text to a conversation or thread via the shared outbound boundary. */
2369
+ async function postSlackMessage(input) {
2370
+ const channelId = requireSlackConversationId(input.channelId, "Slack message posting");
2371
+ const text = requireSlackMessageText(input.text, "Slack message posting");
2372
+ const entities = input.entities?.length ? slackEntitySchema.array().parse(input.entities) : void 0;
2373
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack thread message posting") : void 0;
2374
+ const attributes = {
2375
+ "app.slack.channel_id": channelId,
2376
+ ...threadTs ? { "app.slack.thread_ts": threadTs } : void 0,
2377
+ "app.slack.work_object.count": entities?.length ?? 0
2378
+ };
2379
+ const response = await withSlackRetries(async () => {
2380
+ const response = await getSlackClient().chat.postMessage({
2381
+ channel: channelId,
2382
+ text,
2383
+ unfurl_links: false,
2384
+ unfurl_media: false,
2385
+ ...input.blocks?.length ? { blocks: input.blocks } : void 0,
2386
+ ...entities ? { metadata: { entities } } : void 0,
2387
+ ...threadTs ? { thread_ts: threadTs } : void 0
2388
+ });
2389
+ const messageId = parseSlackMessageTs(response.ts);
2390
+ setSpanAttributes({ "messaging.message.id": messageId });
2391
+ captureSlackPostWarning(response, {
2392
+ ...attributes,
2393
+ "messaging.message.id": messageId
2394
+ });
2395
+ return response;
2396
+ }, 3, {
2397
+ action: "chat.postMessage",
2398
+ attributes
2399
+ });
2400
+ const messageTs = parseSlackMessageTs(response.ts);
2401
+ if (!messageTs) throw new Error("Slack message posted without ts");
2402
+ return {
2403
+ ts: messageTs,
2404
+ ...input.includePermalink ? { permalink: await getSlackMessagePermalink({
2405
+ channelId,
2406
+ messageTs
2407
+ }) } : void 0
2408
+ };
2409
+ }
2410
+ /**
2411
+ * Post an ephemeral Slack message. Delivery is best-effort on Slack's side, but
2412
+ * request validation and Web API behavior are centralized here.
2413
+ */
2414
+ async function postSlackEphemeralMessage(input) {
2415
+ const channelId = requireSlackConversationId(input.channelId, "Slack ephemeral message posting");
2416
+ const userId = parseSlackUserId(input.userId);
2417
+ if (!userId) throw new Error("Slack ephemeral message posting requires a user ID");
2418
+ const text = requireSlackMessageText(input.text, "Slack ephemeral message posting");
2419
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack ephemeral thread message posting") : void 0;
2420
+ return { messageTs: (await withSlackRetries(() => getSlackClient().chat.postEphemeral({
2421
+ channel: channelId,
2422
+ user: userId,
2423
+ text,
2424
+ ...input.blocks?.length ? { blocks: input.blocks } : void 0,
2425
+ ...threadTs ? { thread_ts: threadTs } : void 0
2426
+ }), 3, {
2427
+ action: "chat.postEphemeral",
2428
+ spanAttributes: {
2429
+ "app.slack.channel_id": channelId,
2430
+ "app.slack.user_id": userId,
2431
+ ...threadTs ? { "app.slack.thread_ts": threadTs } : void 0
2432
+ }
2433
+ })).message_ts };
2434
+ }
2435
+ /** Upload files into a Slack conversation or thread via the shared outbound file boundary. */
2436
+ async function uploadFilesToConversation(input) {
2437
+ const channelId = requireSlackConversationId(input.channelId, "Slack file upload");
2438
+ const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack file upload") : void 0;
2439
+ if (input.files.length === 0) throw new Error("Slack file upload requires at least one file");
2440
+ const fileUploads = input.files.map((file) => {
2441
+ const filename = file.filename.trim();
2442
+ if (!filename) throw new Error("Slack file upload requires every file to have a filename");
2443
+ return {
2444
+ file: file.data,
2445
+ filename
2446
+ };
2447
+ });
2448
+ return { files: ((await withSlackRetries(() => getSlackClient().filesUploadV2({
2449
+ channel_id: channelId,
2450
+ ...threadTs ? { thread_ts: threadTs } : void 0,
2451
+ file_uploads: fileUploads
2452
+ }), 3, {
2453
+ action: "filesUploadV2",
2454
+ spanAttributes: {
2455
+ "app.slack.channel_id": channelId,
2456
+ ...threadTs ? { "app.slack.thread_ts": threadTs } : void 0
2457
+ }
2458
+ })).files ?? []).flatMap((completion) => (completion.files ?? []).map((file) => file.id ? { id: file.id } : {})) };
2459
+ }
2460
+ /** Add a reaction to a Slack message, treating `already_reacted` as idempotent success. */
2461
+ async function addReactionToMessage(input) {
2462
+ const channelId = requireSlackConversationId(input.channelId, "Slack reaction");
2463
+ const timestamp = requireSlackMessageTimestamp(input.timestamp, "Slack reaction");
2464
+ const emoji = normalizeSlackEmojiName(input.emoji);
2465
+ if (!emoji) throw new Error("Slack reaction requires a valid emoji alias name");
2466
+ try {
2467
+ await withSlackRetries(() => getSlackClient(input.timeoutMs === void 0 ? void 0 : { timeoutMs: input.timeoutMs }).reactions.add({
2468
+ channel: channelId,
2469
+ timestamp,
2470
+ name: emoji
2471
+ }), input.timeoutMs === void 0 ? 3 : 1, {
2472
+ action: "reactions.add",
2473
+ idempotent: true,
2474
+ spanAttributes: {
2475
+ "app.slack.channel_id": channelId,
2476
+ "app.slack.message_ts": timestamp,
2477
+ "app.slack.reaction": emoji
2478
+ }
2479
+ });
2480
+ } catch (error) {
2481
+ if (error instanceof SlackActionError && error.code === "already_reacted") return { ok: true };
2482
+ throw error;
2483
+ }
2484
+ return { ok: true };
2485
+ }
2486
+ /** Remove a reaction from a Slack message, treating `no_reaction` as idempotent success. */
2487
+ async function removeReactionFromMessage(input) {
2488
+ const channelId = requireSlackConversationId(input.channelId, "Slack reaction removal");
2489
+ const timestamp = requireSlackMessageTimestamp(input.timestamp, "Slack reaction removal");
2490
+ const emoji = normalizeSlackEmojiName(input.emoji);
2491
+ if (!emoji) throw new Error("Slack reaction removal requires a valid emoji alias name");
2492
+ try {
2493
+ await withSlackRetries(() => getSlackClient().reactions.remove({
2494
+ channel: channelId,
2495
+ timestamp,
2496
+ name: emoji
2497
+ }), 3, {
2498
+ action: "reactions.remove",
2499
+ idempotent: true,
2500
+ spanAttributes: {
2501
+ "app.slack.channel_id": channelId,
2502
+ "app.slack.message_ts": timestamp,
2503
+ "app.slack.reaction": emoji
2504
+ }
2505
+ });
2506
+ } catch (error) {
2507
+ if (error instanceof SlackActionError && error.code === "no_reaction") return { ok: true };
2508
+ throw error;
2509
+ }
2510
+ return { ok: true };
2511
+ }
2512
+ //#endregion
2513
+ //#region src/chat/slack/status-format.ts
2514
+ const SLACK_STATUS_MAX_LENGTH = 50;
2515
+ /** Truncate a status string to fit Slack's 50-char assistant status limit. */
2516
+ function truncateStatusText(text) {
2517
+ const trimmed = text.trim();
2518
+ if (!trimmed) return "";
2519
+ if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) return trimmed;
2520
+ return `${trimmed.slice(0, 47).trimEnd()}...`;
2521
+ }
2522
+ //#endregion
2523
+ //#region src/chat/slack/mrkdwn.ts
2524
+ /** Escape dynamic text for Slack mrkdwn without changing intended formatting. */
2525
+ function escapeSlackMrkdwnText(text) {
2526
+ return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
2527
+ }
2528
+ /** Escape a URL for Slack explicit link syntax while preserving query semantics. */
2529
+ function escapeSlackLinkUrl(url) {
2530
+ return url.replaceAll("&", "&amp;").replaceAll("<", "%3C").replaceAll(">", "%3E");
2531
+ }
2532
+ /** Build a Slack explicit link from dynamic URL and label values. */
2533
+ function formatSlackLink(url, label) {
2534
+ return `<${escapeSlackLinkUrl(url)}|${escapeSlackMrkdwnText(label)}>`;
2535
+ }
2536
+ function readInlineCodeSpan(line, start) {
2537
+ if (line[start] !== "`") return;
2538
+ let n = 1;
2539
+ while (line[start + n] === "`") n++;
2540
+ const marker = "`".repeat(n);
2541
+ let search = start + n;
2542
+ while (search < line.length) {
2543
+ const close = line.indexOf(marker, search);
2544
+ if (close === -1) return;
2545
+ const after = close + n;
2546
+ if (line[after] !== "`") return {
2547
+ text: line.slice(start, after),
2548
+ end: after
2549
+ };
2550
+ search = after + 1;
2551
+ }
2552
+ }
2553
+ function readExistingSlackAngleToken(line, start) {
2554
+ if (line[start] !== "<") return;
2555
+ const close = line.indexOf(">", start + 1);
2556
+ if (close === -1) return;
2557
+ const body = line.slice(start + 1, close);
2558
+ if (/^(?:https?:\/\/|@|#|!)/.test(body)) return {
2559
+ text: line.slice(start, close + 1),
2560
+ end: close + 1
2561
+ };
2562
+ }
2563
+ function readMarkdownLink(line, start) {
2564
+ if (line[start] !== "[") return;
2565
+ const labelEnd = line.indexOf("](", start + 1);
2566
+ if (labelEnd === -1) return;
2567
+ const destStart = labelEnd + 2;
2568
+ if (!line.startsWith("http://", destStart) && !line.startsWith("https://", destStart)) return;
2569
+ const closeParens = line.indexOf(")", destStart);
2570
+ if (closeParens === -1) return;
2571
+ return {
2572
+ text: line.slice(start, closeParens + 1),
2573
+ end: closeParens + 1
2574
+ };
2575
+ }
2576
+ function hasUnmatchedClosingParen(text) {
2577
+ let balance = 0;
2578
+ for (const ch of text) if (ch === "(") balance++;
2579
+ else if (ch === ")") balance--;
2580
+ return balance < 0;
2581
+ }
2582
+ function readBareUrl(line, start) {
2583
+ let end = start;
2584
+ while (end < line.length) {
2585
+ const ch = line[end];
2586
+ if (/\s/.test(ch) || ch === "<" || ch === ">" || ch === "\"" || ch === "`" || ch === "|" || ch === "*") break;
2587
+ end++;
2588
+ }
2589
+ if (end === start) return;
2590
+ let raw = line.slice(start, end);
2591
+ let suffix = "";
2592
+ const peel = () => {
2593
+ suffix = raw.slice(-1) + suffix;
2594
+ raw = raw.slice(0, -1);
2595
+ };
2596
+ const shouldPeel = () => raw.endsWith("_") || /[.,!?;:]$/.test(raw) || raw.endsWith(")") && hasUnmatchedClosingParen(raw);
2597
+ while (raw.length > 0 && shouldPeel()) peel();
2598
+ if (!/^https?:\/\/.+/.test(raw)) return;
2599
+ return {
2600
+ url: raw,
2601
+ suffix,
2602
+ end
2603
+ };
2604
+ }
2605
+ /** Wrap bare http(s) URLs on a single line as Slack explicit `<url>` links. */
2606
+ function wrapBareUrlsOnLine(line) {
2607
+ let result = "";
2608
+ let i = 0;
2609
+ while (i < line.length) {
2610
+ const codeSpan = readInlineCodeSpan(line, i);
2611
+ if (codeSpan) {
2612
+ result += codeSpan.text;
2613
+ i = codeSpan.end;
2614
+ continue;
2615
+ }
2616
+ const angleToken = readExistingSlackAngleToken(line, i);
2617
+ if (angleToken) {
2618
+ result += angleToken.text;
2619
+ i = angleToken.end;
2620
+ continue;
2621
+ }
2622
+ const mdLink = readMarkdownLink(line, i);
2623
+ if (mdLink) {
2624
+ result += mdLink.text;
2625
+ i = mdLink.end;
2626
+ continue;
2627
+ }
2628
+ if (line.startsWith("https://", i) || line.startsWith("http://", i)) {
2629
+ const parsed = readBareUrl(line, i);
2630
+ if (parsed) {
2631
+ result += `<${parsed.url}>${parsed.suffix}`;
2632
+ i = parsed.end;
2633
+ continue;
2634
+ }
2635
+ }
2636
+ result += line[i];
2637
+ i++;
2638
+ }
2639
+ return result;
2640
+ }
2641
+ /**
2642
+ * Pre-wrap bare http(s) URLs outside fenced code blocks as Slack explicit
2643
+ * links, preventing Slack's auto-linker from consuming adjacent formatting
2644
+ * markers into the URL.
2645
+ *
2646
+ * Uses the same fence-toggle rule as `ensureBlockSpacing` so both passes
2647
+ * agree on which lines are code.
2648
+ */
2649
+ function wrapBareUrls(text) {
2650
+ const lines = text.split("\n");
2651
+ const out = [];
2652
+ let inCodeBlock = false;
2653
+ for (const line of lines) {
2654
+ if (line.trimStart().startsWith("```")) {
2655
+ inCodeBlock = !inCodeBlock;
2656
+ out.push(line);
2657
+ continue;
2658
+ }
2659
+ out.push(inCodeBlock ? line : wrapBareUrlsOnLine(line));
2660
+ }
2661
+ return out.join("\n");
2662
+ }
2663
+ /** Insert blank lines between content blocks so Slack renders them with visual separation. */
2664
+ function ensureBlockSpacing(text) {
2665
+ const codeBlockPattern = /^```/;
2666
+ const listItemPattern = /^[-*•]\s|^\d+\.\s/;
2667
+ const lines = text.split("\n");
2668
+ const result = [];
2669
+ let inCodeBlock = false;
2670
+ for (let i = 0; i < lines.length; i++) {
2671
+ const line = lines[i];
2672
+ if (codeBlockPattern.test(line.trimStart())) {
2673
+ if (!inCodeBlock) {
2674
+ const prev = result.length > 0 ? result[result.length - 1] : void 0;
2675
+ if (prev !== void 0 && prev.trim() !== "") result.push("");
2676
+ }
2677
+ inCodeBlock = !inCodeBlock;
2678
+ result.push(line);
2679
+ continue;
2680
+ }
2681
+ if (inCodeBlock) {
2682
+ result.push(line);
2683
+ continue;
2684
+ }
2685
+ const prev = result.length > 0 ? result[result.length - 1] : void 0;
2686
+ if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) result.push("");
2687
+ result.push(line);
2688
+ }
2689
+ return result.join("\n");
2690
+ }
2691
+ /**
2692
+ * Normalize model-authored Slack markdown for delivery via `markdown_text`
2693
+ * or `{ type: "markdown" }` blocks.
2694
+ *
2695
+ * Plugins may first rewrite ordinary Markdown. Core then applies Slack-only
2696
+ * delivery shaping: bare URL wrapping and block spacing. Slack reply delivery
2697
+ * owns chunking and continuation markers separately.
2698
+ */
2699
+ function normalizeSlackReplyMarkdown(text) {
2700
+ let normalized = applyPluginFormatMarkdown(text).replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
2701
+ normalized = wrapBareUrls(normalized);
2702
+ normalized = ensureBlockSpacing(normalized);
2703
+ return normalized.replace(/\n{3,}/g, "\n\n").trim();
2704
+ }
2705
+ /** Normalize assistant status text before handing it to Slack. */
2706
+ function normalizeSlackStatusText(text) {
2707
+ const trimmed = text.trim();
2708
+ if (!trimmed) return "";
2709
+ return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
2710
+ }
2711
+ /** Render a Slack user mention, which Slack shows as the person's name. */
2712
+ function slackMention(userId) {
2713
+ return `<@${userId}>`;
2714
+ }
2715
+ //#endregion
2716
+ //#region src/chat/automations/revision.ts
2717
+ /** Storage revisions prevent stale edits from replacing newer values. */
2718
+ var AutomationConflictError = class extends ToolInputError {
2719
+ constructor() {
2720
+ super("This Automation changed after it was loaded. Reload it before saving.");
2721
+ this.name = "AutomationConflictError";
2722
+ }
2723
+ };
2724
+ /** Sort object keys so equal values produce equal JSON. */
2725
+ function canonicalJson(value) {
2726
+ if (Array.isArray(value)) return value.map(canonicalJson);
2727
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => [key, canonicalJson(child)]));
2728
+ return value;
2729
+ }
2730
+ /** Identify the stored values being edited, including lifecycle and creator authority. */
2731
+ function automationRevision(task) {
2732
+ return createHash("sha256").update(JSON.stringify(canonicalJson(task))).digest("hex");
2733
+ }
2734
+ /** Reject a stale edit while the storage owner holds the write lock. */
2735
+ function requireAutomationRevision(task, revision) {
2736
+ if (!task || automationRevision(task) !== revision) throw new AutomationConflictError();
2737
+ }
2738
+ //#endregion
2739
+ //#region src/chat/automations/versions.ts
2740
+ /** Automation versions keep each saved definition so people can see how it changed. */
2741
+ /** Return the versioned part of an Automation. Lifecycle and run state are excluded. */
2742
+ function automationDefinition(task) {
2743
+ const common = {
2744
+ title: task.title?.trim() || null,
2745
+ instruction: task.task.text,
2746
+ credentialMode: task.credentialMode,
2747
+ destination: task.destination,
2748
+ outcomes: task.outcomes
2749
+ };
2750
+ return "schedule" in task ? {
2751
+ ...common,
2752
+ schedule: task.schedule
2753
+ } : {
2754
+ ...common,
2755
+ trigger: task.trigger
2756
+ };
2757
+ }
2758
+ /** Compare definition values after key sorting. */
2759
+ function sameDefinitionValue(left, right) {
2760
+ return JSON.stringify(canonicalJson(left)) === JSON.stringify(canonicalJson(right));
2761
+ }
2762
+ /**
2763
+ * Save a version when the definition changed. Call this in the same
2764
+ * transaction and lock as the Automation write. Without `current`, the
2765
+ * Automation is new, so the creator saves version 1.
2766
+ */
2767
+ async function recordAutomationVersion(db, kind, task, current, editedBy) {
2768
+ const definition = automationDefinition(task);
2769
+ if (current && sameDefinitionValue(automationDefinition(current), definition)) return;
2770
+ await db.insert(juniorAutomationVersions).values({
2771
+ kind,
2772
+ automationId: task.id,
2773
+ version: sql`(
2774
+ SELECT coalesce(max(${juniorAutomationVersions.version}), 0) + 1
2775
+ FROM ${juniorAutomationVersions}
2776
+ WHERE ${juniorAutomationVersions.kind} = ${kind}
2777
+ AND ${juniorAutomationVersions.automationId} = ${task.id}
2778
+ )`,
2779
+ createdAtMs: Date.now(),
2780
+ editedBy: editedBy ?? (current ? null : task.createdBy),
2781
+ definition
2782
+ });
2783
+ }
2784
+ function parseVersionRow(kind, row) {
2785
+ return automationVersionSchema.parse({
2786
+ kind,
2787
+ version: row.version,
2788
+ createdAt: new Date(row.createdAtMs).toISOString(),
2789
+ editedBy: row.editedBy,
2790
+ definition: row.definition
2791
+ });
2792
+ }
2793
+ /** Read one saved definition. */
2794
+ async function readAutomationVersion(db, kind, automationId, version) {
2795
+ const [row] = await db.select().from(juniorAutomationVersions).where(and(eq(juniorAutomationVersions.kind, kind), eq(juniorAutomationVersions.automationId, automationId), eq(juniorAutomationVersions.version, version))).limit(1);
2796
+ return row ? parseVersionRow(kind, row) : void 0;
2797
+ }
2798
+ /** Read saved definitions, newest first. */
2799
+ async function listAutomationVersions(db, kind, automationId, limit) {
2800
+ return (await db.select().from(juniorAutomationVersions).where(and(eq(juniorAutomationVersions.kind, kind), eq(juniorAutomationVersions.automationId, automationId))).orderBy(desc(juniorAutomationVersions.version)).limit(limit)).map((row) => parseVersionRow(kind, row));
2801
+ }
2802
+ //#endregion
2803
+ //#region src/chat/event-automations/store.ts
2804
+ const retainedEventAutomationSchema = eventAutomationSchema.extend({
2805
+ destination: slackDestinationSchema,
2806
+ outcomes: z.array(taskOutcomeSchema).max(5)
2807
+ });
2808
+ const eventAutomationRowColumns = {
2809
+ status: juniorEventAutomations.status,
2810
+ statusReason: juniorEventAutomations.statusReason,
2811
+ task: juniorEventAutomations.task,
2812
+ title: juniorEventAutomations.title
2813
+ };
2814
+ /** JSON task payload must not carry SQL-backed columns. */
2815
+ function eventAutomationJsonPayload(task) {
2816
+ const { status: _status, statusReason: _statusReason, title: _title, ...payload } = task;
2817
+ return payload;
2818
+ }
2819
+ /** Decode a retained event Automation with its SQL-backed fields. */
2820
+ function parseEventAutomationRow(row) {
2821
+ const raw = row.task && typeof row.task === "object" ? { ...row.task } : row.task;
2822
+ if (raw && typeof raw === "object" && "title" in raw) delete raw.title;
2823
+ if (raw && typeof raw === "object" && !("outcomes" in raw) && "destination" in raw) raw.outcomes = [{
2824
+ action: "send_message",
2825
+ destination: raw.destination
2826
+ }];
2827
+ const retained = retainedEventAutomationSchema.parse(raw);
2828
+ const { threadTs: _threadTs, ...destination } = retained.destination;
2829
+ const title = row.title?.trim();
2830
+ return {
2831
+ ...retained,
2832
+ destination,
2833
+ outcomes: retained.outcomes.map((outcome) => {
2834
+ const { threadTs: _threadTs, ...destination } = outcome.destination;
2835
+ return {
2836
+ ...outcome,
2837
+ destination
2838
+ };
2839
+ }),
2840
+ status: row.status ?? "active",
2841
+ ...row.statusReason ? { statusReason: row.statusReason } : void 0,
2842
+ ...title ? { title } : void 0
2843
+ };
2844
+ }
2845
+ function activeEventAutomationWhere() {
2846
+ return ne(juniorEventAutomations.status, "deleted");
2847
+ }
2848
+ /** Read one event automation by id, including deleted rows when present. */
2849
+ async function getEventAutomation(db, id) {
2850
+ const rows = await db.select({ ...eventAutomationRowColumns }).from(juniorEventAutomations).where(eq(juniorEventAutomations.id, id)).limit(1);
2851
+ return rows[0] ? parseEventAutomationRow(rows[0]) : void 0;
2852
+ }
2853
+ /** Create one retry-stable event automation, or revive a deleted row with the new payload. */
2854
+ async function createEventAutomation(db, task) {
2855
+ const parsed = eventAutomationSchema.parse(eventAutomationJsonPayload(task));
2856
+ const title = task.title?.trim() || null;
2857
+ return db.transaction(async (tx) => {
2858
+ const inserted = await tx.insert(juniorEventAutomations).values({
2859
+ id: parsed.id,
2860
+ teamId: parsed.destination.teamId,
2861
+ namespace: parsed.trigger.namespace,
2862
+ identifier: parsed.trigger.identifier,
2863
+ createdAtMs: parsed.createdAtMs,
2864
+ status: "active",
2865
+ title,
2866
+ task: parsed
2867
+ }).onConflictDoNothing().returning({ ...eventAutomationRowColumns });
2868
+ if (inserted[0]) {
2869
+ const created = parseEventAutomationRow(inserted[0]);
2870
+ await recordAutomationVersion(tx, "event", created, void 0);
2871
+ return created;
2872
+ }
2873
+ const existing = await getEventAutomation(tx, parsed.id);
2874
+ if (!existing) return {
2875
+ ...parsed,
2876
+ status: "active",
2877
+ ...title ? { title } : void 0
2878
+ };
2879
+ if (existing.status !== "deleted") return existing;
2880
+ const rows = await tx.update(juniorEventAutomations).set({
2881
+ teamId: parsed.destination.teamId,
2882
+ namespace: parsed.trigger.namespace,
2883
+ identifier: parsed.trigger.identifier,
2884
+ status: "active",
2885
+ statusReason: null,
2886
+ title,
2887
+ task: parsed
2888
+ }).where(and(eq(juniorEventAutomations.id, parsed.id), eq(juniorEventAutomations.status, "deleted"))).returning({ ...eventAutomationRowColumns });
2889
+ if (!rows[0]) return await getEventAutomation(tx, parsed.id) ?? {
2890
+ ...parsed,
2891
+ status: "active",
2892
+ ...title ? { title } : void 0
2893
+ };
2894
+ const revived = parseEventAutomationRow(rows[0]);
2895
+ await recordAutomationVersion(tx, "event", revived, void 0);
2896
+ return revived;
2897
+ });
2898
+ }
2899
+ /**
2900
+ * Replace an existing non-deleted event automation under a row lock.
2901
+ * Pass `editedBy` when a person changes the definition so its version names them.
2902
+ */
2903
+ async function saveEventAutomation(db, task, expectedRevision, editedBy) {
2904
+ const parsed = eventAutomationSchema.parse(eventAutomationJsonPayload(task));
2905
+ const title = task.title?.trim() || null;
2906
+ return db.transaction(async (tx) => {
2907
+ const rows = await tx.select({ ...eventAutomationRowColumns }).from(juniorEventAutomations).where(eq(juniorEventAutomations.id, parsed.id)).for("update");
2908
+ const current = rows[0] ? parseEventAutomationRow(rows[0]) : void 0;
2909
+ if (expectedRevision !== void 0) requireAutomationRevision(current, expectedRevision);
2910
+ if (!current || current.status === "deleted") return void 0;
2911
+ const saved = parseEventAutomationRow((await tx.update(juniorEventAutomations).set({
2912
+ namespace: parsed.trigger.namespace,
2913
+ identifier: parsed.trigger.identifier,
2914
+ title,
2915
+ task: parsed
2916
+ }).where(eq(juniorEventAutomations.id, parsed.id)).returning({ ...eventAutomationRowColumns }))[0]);
2917
+ await recordAutomationVersion(tx, "event", saved, current, editedBy);
2918
+ return saved;
2919
+ });
2920
+ }
2921
+ /** Mark one existing event automation deleted while retaining the row for history. */
2922
+ async function deleteEventAutomation(db, id) {
2923
+ const rows = await db.update(juniorEventAutomations).set({ status: "deleted" }).where(and(eq(juniorEventAutomations.id, id), activeEventAutomationWhere())).returning({ ...eventAutomationRowColumns });
2924
+ return rows[0] ? parseEventAutomationRow(rows[0]) : void 0;
2925
+ }
2926
+ /** List live event automations in one Slack workspace. */
2927
+ async function listEventAutomationsForTeam(db, teamId) {
2928
+ return (await db.select({ ...eventAutomationRowColumns }).from(juniorEventAutomations).where(and(eq(juniorEventAutomations.teamId, teamId), activeEventAutomationWhere())).orderBy(asc(juniorEventAutomations.createdAtMs), asc(juniorEventAutomations.id))).map(parseEventAutomationRow);
2929
+ }
2930
+ function viewerSlackIdentities(user) {
2931
+ return user.identities.filter((identity) => identity.provider === "slack" && Boolean(identity.providerTenantId) && Boolean(identity.providerSubjectId));
2932
+ }
2933
+ /** Return whether one event automation was created by a viewer-linked Slack identity. */
2934
+ function eventAutomationBelongsToUser(task, user) {
2935
+ return viewerSlackIdentities(user).some((identity) => identity.providerTenantId === task.destination.teamId && identity.providerSubjectId === task.createdBy.slackUserId);
2936
+ }
2937
+ /**
2938
+ * List deleted event automations created by one user, newest first.
2939
+ * Used only to keep historical runs after the live automation list drops the row.
2940
+ */
2941
+ async function listDeletedEventAutomationsCreatedBy(db, user, limit) {
2942
+ const identities = viewerSlackIdentities(user);
2943
+ const ownership = or(...identities.map((identity) => and(eq(juniorEventAutomations.teamId, identity.providerTenantId), sql`${juniorEventAutomations.task}->'createdBy'->>'slackUserId' = ${identity.providerSubjectId}`)));
2944
+ if (!ownership) return [];
2945
+ return (await db.select({ ...eventAutomationRowColumns }).from(juniorEventAutomations).where(and(ownership, eq(juniorEventAutomations.status, "deleted"))).orderBy(desc(juniorEventAutomations.createdAtMs), desc(juniorEventAutomations.id)).limit(limit)).map(parseEventAutomationRow);
2946
+ }
2947
+ /**
2948
+ * Collect match keys from live event automations for these identifiers and event types.
2949
+ * Does not evaluate match values — only reports keys that filters use.
2950
+ */
2951
+ async function collectEventAutomationMatchKeys(db, input) {
2952
+ const eventTypes = new Set(input.eventTypes.map((eventType) => eventType.trim()).filter(Boolean));
2953
+ const identifiers = [...new Set(input.identifiers.map((value) => value.trim()).filter(Boolean))];
2954
+ if (eventTypes.size === 0 || identifiers.length === 0) return [];
2955
+ const rows = await db.select({ ...eventAutomationRowColumns }).from(juniorEventAutomations).where(and(eq(juniorEventAutomations.teamId, input.teamId), eq(juniorEventAutomations.namespace, input.namespace), inArray(juniorEventAutomations.identifier, identifiers), eq(juniorEventAutomations.status, "active"))).orderBy(asc(juniorEventAutomations.createdAtMs), asc(juniorEventAutomations.id));
2956
+ const keys = /* @__PURE__ */ new Set();
2957
+ for (const row of rows) {
2958
+ const task = parseEventAutomationRow(row);
2959
+ if (!task.trigger.events.some((eventType) => eventTypes.has(eventType))) continue;
2960
+ for (const key of Object.keys(task.trigger.match ?? {})) keys.add(key);
2961
+ }
2962
+ return [...keys].sort();
2963
+ }
2964
+ /** Find every live task matching one normalized event. */
2965
+ async function findMatchingEventAutomations(db, event, teamId) {
2966
+ return (await db.select({ ...eventAutomationRowColumns }).from(juniorEventAutomations).where(and(eq(juniorEventAutomations.teamId, teamId), eq(juniorEventAutomations.namespace, event.namespace), eq(juniorEventAutomations.identifier, event.identifier), eq(juniorEventAutomations.status, "active"))).orderBy(asc(juniorEventAutomations.createdAtMs), asc(juniorEventAutomations.id))).map(parseEventAutomationRow).filter((task) => task.trigger.events.includes(event.eventType) && eventMatches(task.trigger.match, event.data));
2967
+ }
2968
+ /**
2969
+ * Change lifecycle under the same row lock as edits, without changing
2970
+ * credentials. A pause keeps an unresolved block reason, so resuming a paused
2971
+ * blocked automation returns it to blocked. Resume a blocked automation to
2972
+ * clear the reason.
2973
+ */
2974
+ async function setEventAutomationStatus(db, id, status, revision) {
2975
+ return db.transaction(async (tx) => {
2976
+ const rows = await tx.select().from(juniorEventAutomations).where(eq(juniorEventAutomations.id, id)).for("update");
2977
+ const current = rows[0] ? parseEventAutomationRow(rows[0]) : void 0;
2978
+ requireAutomationRevision(current, revision);
2979
+ if (!current || current.status === "deleted") throw new Error("Automation no longer exists.");
2980
+ const next = status === "paused" ? {
2981
+ status,
2982
+ statusReason: current.statusReason
2983
+ } : current.status === "paused" && current.statusReason ? {
2984
+ status: "blocked",
2985
+ statusReason: current.statusReason
2986
+ } : {
2987
+ status,
2988
+ statusReason: void 0
2989
+ };
2990
+ await tx.update(juniorEventAutomations).set({
2991
+ status: next.status,
2992
+ statusReason: next.statusReason ?? null
2993
+ }).where(eq(juniorEventAutomations.id, id));
2994
+ const { statusReason: _statusReason, ...rest } = current;
2995
+ return {
2996
+ ...rest,
2997
+ status: next.status,
2998
+ ...next.statusReason ? { statusReason: next.statusReason } : void 0
2999
+ };
3000
+ });
3001
+ }
3002
+ /**
3003
+ * Stop an event automation after a run reports a problem that only its
3004
+ * creator can fix. An active automation becomes blocked. A paused automation
3005
+ * stays paused and keeps the reason, so resume returns it to blocked.
3006
+ * Deleted automations do not change. Returns true only when this call set
3007
+ * the automation to blocked, so the creator notice goes once and never for
3008
+ * an automation that the dashboard shows as paused.
3009
+ */
3010
+ async function blockEventAutomation(db, id, reason) {
3011
+ return (await db.update(juniorEventAutomations).set({
3012
+ status: sql`case when ${juniorEventAutomations.status} = 'active' then 'blocked' else ${juniorEventAutomations.status} end`,
3013
+ statusReason: reason
3014
+ }).where(and(eq(juniorEventAutomations.id, id), or(eq(juniorEventAutomations.status, "active"), and(eq(juniorEventAutomations.status, "paused"), sql`${juniorEventAutomations.statusReason} is distinct from ${reason}`)))).returning({ status: juniorEventAutomations.status }))[0]?.status === "blocked";
3015
+ }
3016
+ //#endregion
3017
+ //#region src/chat/scheduled-automations/tasks.ts
3018
+ const SCHEDULER_KEY_PREFIX = "junior:scheduler";
3019
+ const retainedScheduledAutomationSchema = scheduledAutomationSchema.omit({
3020
+ creatorIdentityId: true,
3021
+ title: true
3022
+ }).extend({
3023
+ destination: slackDestinationSchema,
3024
+ outcomes: z.array(taskOutcomeSchema).max(5),
3025
+ version: z.number().optional()
3026
+ }).strict();
3027
+ /** Decode a retained scheduler task row and reject invalid routing context. */
3028
+ function parseScheduledAutomationRow(row) {
3029
+ const record = z.record(z.string(), z.unknown()).safeParse(row.record);
3030
+ if (!record.success) return void 0;
3031
+ const { creatorIdentityId: legacyCreatorIdentityId, title: legacyTitle, ...retained } = record.data;
3032
+ const current = retained.outcomes === void 0 && retained.destination !== void 0 ? {
3033
+ ...retained,
3034
+ outcomes: [{
3035
+ action: "send_message",
3036
+ destination: retained.destination
3037
+ }]
3038
+ } : retained;
3039
+ const parsed = retainedScheduledAutomationSchema.safeParse(current);
3040
+ if (!parsed.success) return void 0;
3041
+ const { status, version: _version, ...retainedTask } = parsed.data;
3042
+ const destination = {
3043
+ platform: "slack",
3044
+ teamId: retainedTask.destination.teamId,
3045
+ channelId: retainedTask.destination.channelId
3046
+ };
3047
+ const task = {
3048
+ ...retainedTask,
3049
+ destination,
3050
+ outcomes: retainedTask.outcomes.map((outcome) => ({
3051
+ ...outcome,
3052
+ destination: {
3053
+ platform: "slack",
3054
+ teamId: outcome.destination.teamId,
3055
+ channelId: outcome.destination.channelId
3056
+ }
3057
+ }))
3058
+ };
3059
+ const fallbackIdentity = row.creatorIdentityId === null ? z.string().safeParse(legacyCreatorIdentityId) : void 0;
3060
+ const creatorIdentityId = row.creatorIdentityId ?? (fallbackIdentity?.success ? fallbackIdentity.data : void 0);
3061
+ if (creatorIdentityId === void 0) return void 0;
3062
+ const fallbackTitle = row.title === null ? z.string().safeParse(legacyTitle) : void 0;
3063
+ const title = (row.title !== null ? row.title : fallbackTitle?.success ? fallbackTitle.data : void 0)?.trim() || void 0;
3064
+ return {
3065
+ ...task,
3066
+ creatorIdentityId,
3067
+ status,
3068
+ ...title ? { title } : void 0
3069
+ };
3070
+ }
3071
+ /** Keep scan paths from returning current or legacy tombstones. */
3072
+ function isListedScheduledAutomation(task) {
3073
+ return task !== void 0 && task.status !== "deleted";
3074
+ }
3075
+ /** Build the stable run id for one task occurrence. */
3076
+ function scheduledRunId(taskId, scheduledForMs) {
3077
+ return `${taskId}:${scheduledForMs}`;
3078
+ }
3079
+ /** Acquire the deployed advisory lock for one scheduled automation. */
3080
+ function withScheduledAutomationLock(db, taskId, callback) {
3081
+ return db.transaction(async (tx) => {
3082
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext(${`${SCHEDULER_KEY_PREFIX}:task:${taskId}:lock`}))`);
3083
+ return await callback(tx);
3084
+ });
3085
+ }
3086
+ function requireStoredTask(task) {
3087
+ const parsed = scheduledAutomationSchema.safeParse(task);
3088
+ if (!parsed.success) throw new Error("Scheduled automation routing context is invalid.");
3089
+ const { title, ...current } = parsed.data;
3090
+ const normalizedTitle = title?.trim() || void 0;
3091
+ return {
3092
+ ...current,
3093
+ ...normalizedTitle ? { title: normalizedTitle } : void 0
3094
+ };
3095
+ }
3096
+ function scheduledAutomationJsonRecord(task) {
3097
+ const { title: _title, ...record } = task;
3098
+ return record;
3099
+ }
3100
+ async function upsertScheduledAutomation(db, task) {
3101
+ const title = task.title?.trim() || null;
3102
+ await db.insert(juniorSchedulerTasks).values({
3103
+ createdAtMs: task.createdAtMs,
3104
+ creatorIdentityId: task.creatorIdentityId,
3105
+ creatorSlackUserId: task.createdBy.slackUserId,
3106
+ id: task.id,
3107
+ nextRunAtMs: task.nextRunAtMs,
3108
+ record: scheduledAutomationJsonRecord(task),
3109
+ runNowAtMs: task.runNowAtMs,
3110
+ status: task.status,
3111
+ teamId: task.destination.teamId,
3112
+ title
3113
+ }).onConflictDoUpdate({
3114
+ target: juniorSchedulerTasks.id,
3115
+ set: {
3116
+ createdAtMs: sql`excluded.created_at_ms`,
3117
+ creatorIdentityId: sql`excluded.creator_identity_id`,
3118
+ creatorSlackUserId: sql`excluded.creator_slack_user_id`,
3119
+ nextRunAtMs: sql`excluded.next_run_at_ms`,
3120
+ record: sql`excluded.record`,
3121
+ runNowAtMs: sql`excluded.run_now_at_ms`,
3122
+ status: sql`excluded.status`,
3123
+ teamId: sql`excluded.team_id`,
3124
+ title: sql`excluded.title`
3125
+ }
3126
+ });
3127
+ }
3128
+ /** Read and decode one scheduled automation. */
3129
+ async function readScheduledAutomation(db, taskId) {
3130
+ const rows = await db.select({
3131
+ creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
3132
+ record: juniorSchedulerTasks.record,
3133
+ title: juniorSchedulerTasks.title
3134
+ }).from(juniorSchedulerTasks).where(eq(juniorSchedulerTasks.id, taskId)).limit(1);
3135
+ return rows[0] ? parseScheduledAutomationRow(rows[0]) : void 0;
3136
+ }
3137
+ async function readListedScheduledAutomations(db, teamId) {
3138
+ return (await db.select({
3139
+ creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
3140
+ record: juniorSchedulerTasks.record,
3141
+ title: juniorSchedulerTasks.title
3142
+ }).from(juniorSchedulerTasks).where(and(notInArray(juniorSchedulerTasks.status, ["deleted"]), teamId === void 0 ? void 0 : eq(juniorSchedulerTasks.teamId, teamId))).orderBy(asc(juniorSchedulerTasks.createdAtMs), asc(juniorSchedulerTasks.id))).map(parseScheduledAutomationRow).filter(isListedScheduledAutomation);
3143
+ }
3144
+ /** List decoded scheduled automations in stable creation order. */
3145
+ async function listScheduledAutomations(db) {
3146
+ return await readListedScheduledAutomations(db);
3147
+ }
3148
+ /** List decoded scheduled automations for one team in stable creation order. */
3149
+ async function listScheduledAutomationsForTeam(db, teamId) {
3150
+ return await readListedScheduledAutomations(db, teamId);
3151
+ }
3152
+ async function writeScheduledAutomation(db, task, current, editedBy) {
3153
+ if (current?.status === "blocked" && task.status === "active" && typeof task.nextRunAtMs === "number" && Number.isFinite(task.nextRunAtMs)) await db.delete(juniorSchedulerRuns).where(and(eq(juniorSchedulerRuns.id, scheduledRunId(task.id, task.nextRunAtMs)), eq(juniorSchedulerRuns.status, "blocked")));
3154
+ if (task.status === "deleted" && current?.status !== "deleted") await skipPendingRunsForDeletedTask(db, task);
3155
+ await upsertScheduledAutomation(db, task);
3156
+ await recordAutomationVersion(db, "scheduled", task, current, editedBy);
3157
+ }
3158
+ async function skipPendingRunsForDeletedTask(db, task) {
3159
+ const errorMessage = `Scheduled automation ${task.id} was deleted before the run started.`;
3160
+ const rows = await db.select({ record: juniorSchedulerRuns.record }).from(juniorSchedulerRuns).where(and(eq(juniorSchedulerRuns.taskId, task.id), eq(juniorSchedulerRuns.status, "pending")));
3161
+ for (const row of rows) {
3162
+ const parsed = scheduledRunSchema.safeParse(row.record);
3163
+ if (!parsed.success || parsed.data.status !== "pending") continue;
3164
+ const next = scheduledRunSchema.parse({
3165
+ ...parsed.data,
3166
+ completedAtMs: task.updatedAtMs,
3167
+ errorMessage,
3168
+ status: "skipped"
3169
+ });
3170
+ await db.update(juniorSchedulerRuns).set({
3171
+ record: next,
3172
+ status: next.status
3173
+ }).where(and(eq(juniorSchedulerRuns.id, next.id), eq(juniorSchedulerRuns.status, "pending")));
3174
+ }
3175
+ }
3176
+ /** Create a scheduled automation once under its retry-stable task lock. */
3177
+ async function createScheduledAutomation(db, task) {
3178
+ const next = requireStoredTask(task);
3179
+ return await withScheduledAutomationLock(db, task.id, async (tx) => {
3180
+ const current = await readScheduledAutomation(tx, task.id);
3181
+ if (current) return current;
3182
+ await writeScheduledAutomation(tx, next, void 0);
3183
+ return next;
3184
+ });
3185
+ }
3186
+ /**
3187
+ * Save a scheduled automation and clear its blocked occurrence on reactivation.
3188
+ * Pass `editedBy` when a person changes the definition so its version names them.
3189
+ */
3190
+ async function saveScheduledAutomation(db, task, expectedRevision, editedBy) {
3191
+ const next = requireStoredTask(task);
3192
+ await withScheduledAutomationLock(db, task.id, async (tx) => {
3193
+ const current = await readScheduledAutomation(tx, task.id);
3194
+ if (expectedRevision !== void 0) requireAutomationRevision(current, expectedRevision);
3195
+ await writeScheduledAutomation(tx, next, current, editedBy);
3196
+ });
3197
+ return next;
3198
+ }
3199
+ /** Save a scheduled automation inside a task lock already held by the caller. */
3200
+ async function saveScheduledAutomationInLock(db, task, current) {
3201
+ await writeScheduledAutomation(db, requireStoredTask(task), current);
3202
+ }
3203
+ //#endregion
3204
+ export { persistWithRetry as $, markConversationMessage as $t, formatSlackLink as A, recordAuthenticationLinked as At, removeReactionFromMessage as B, persistConversationMessages as Bt, listAutomationVersions as C, loadConversationProjection as Ct, automationRevision as D, openConversationProjection as Dt, AutomationConflictError as E, loadTurnRoute as Et, addReactionToMessage as F, recordSubscribedReplyRoute as Ft, loadTurnCheckpoint as G, fallbackShortTitle as Gt, slackEntitySchema as H, persistConversationMessageSummaries as Ht, getSlackMessagePermalink as I, recordToolExecutionStarted as It, completeTurnRecord as J, buildConversationContext as Jt, saveTurnCheckpoint as K, resolveTaskTitle as Kt, openSlackDirectMessage as L, recordTurnRoute as Lt, normalizeSlackStatusText as M, recordAuthorizationRequested as Mt, slackMention as N, recordGuardianActionReviewed as Nt, requireAutomationRevision as O, recordAgentsInstructionsUpdated as Ot, truncateStatusText as P, recordMcpProviderConnected as Pt, recordTurnSummary as Q, isHumanConversationMessage as Qt, postSlackEphemeralMessage as R, appendConversationMessages as Rt, automationDefinition as S, loadConnectedMcpProviders as St, sameDefinitionValue as T, loadTurnProjection as Tt, slackExternalRefIdSchema as U, resolveConversationTitle as Ut, uploadFilesToConversation as V, projectConversationMessages as Vt, continuableMessages as W, settleConversationTitleWork as Wt, getTurnRecord as X, estimateConversationContextTokens as Xt, failTurnRecord as Y, createConversationMemoryService as Yt, listTurnSummaries as Z, getConversationMessageSlackTs as Zt, listDeletedEventAutomationsCreatedBy as _, trimTrailingAssistantMessages as _t, readScheduledAutomation as a, getAgentContextCompactionTriggerTokens as an, getPiMessageRole as at, saveEventAutomation as b, commitAcceptedReply as bt, scheduledRunId as c, piMessageFromHistoryItem as cn, hasRuntimeTurnContext as ct, collectEventAutomationMatchKeys as d, isContinuablePiBoundary as dt, normalizeConversationText as en, TurnSliceLimitExceededError as et, createEventAutomation as f, isToolResultError as ft, getEventAutomation as g, stripRuntimeTurnContext as gt, findMatchingEventAutomations as h, retainRuntimeTurnContext as ht, parseScheduledAutomationRow as i, estimateTextTokens as in, extractAssistantText as it, normalizeSlackReplyMarkdown as j, recordAuthorizationCompleted as jt, escapeSlackMrkdwnText as k, recordAttachmentsDelivered as kt, withScheduledAutomationLock as l, instructionTextForProjection as lt, eventAutomationBelongsToUser as m, normalizeToolNameFromResult as mt, listScheduledAutomations as n, turnHasReply as nn, buildTurnErrorResponse as nt, saveScheduledAutomation as o, getAgentContextInputLimitTokens as on, getTerminalAssistantMessages as ot, deleteEventAutomation as p, isToolResultMessage as pt, abandonTurnRecord as q, appendThreadContextMessages as qt, listScheduledAutomationsForTeam as r, upsertConversationMessage as rn, isTurnExecutionLimitExceededError as rt, saveScheduledAutomationInLock as s, historyItemFromPiMessage as sn, getUserMessageInstructionText as st, createScheduledAutomation as t, recordDeliveredAssistantMessage as tn, assertTurnToolCallLimit as tt, blockEventAutomation as u, isAssistantMessage as ut, listEventAutomationsForTeam as v, TURN_CONTEXT_TAG as vt, readAutomationVersion as w, loadProjection as wt, setEventAutomationStatus as x, commitMessages as xt, parseEventAutomationRow as y, ToolInputError as yt, postSlackMessage as z, hydrateConversationMessages as zt };