@sentry/junior 0.239.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 (64) hide show
  1. package/dist/{admin-Cc-iA6z9.js → admin-CJkjl-aN.js} +1 -1
  2. package/dist/{agent-hooks-DzfA5m3R.js → agent-hooks-C5zYOaJk.js} +1972 -1866
  3. package/dist/{agent-runner--1wC26S6.js → agent-runner-CMZ6lFQe.js} +377 -1227
  4. package/dist/api/schema.d.ts +9 -0
  5. package/dist/api/schema.js +2 -2
  6. package/dist/api.d.ts +1 -1
  7. package/dist/api.js +22 -21
  8. package/dist/{app-FB2hmHpZ.d.ts → app-MEd7wbF0.d.ts} +1 -1
  9. package/dist/app.d.ts +1 -1
  10. package/dist/app.js +105 -31
  11. package/dist/{automation-CrQGQ2MT.js → automation-gznOMaQ2.js} +13 -255
  12. package/dist/{briefs-DGc2Thua.js → briefs-CsQbFjRi.js} +2 -2
  13. package/dist/cadence-D00SCzT1.js +256 -0
  14. package/dist/{catalog-runtime-DpB_hG_I.js → catalog-runtime-Tz0O2MZe.js} +1 -1
  15. package/dist/cli/chat.js +12 -12
  16. package/dist/cli/check.js +3 -3
  17. package/dist/cli/main.js +2 -2
  18. package/dist/cli/plugins.js +4 -4
  19. package/dist/cli/snapshot-create.js +3 -3
  20. package/dist/cli/upgrade.d.ts +1 -1
  21. package/dist/cli/upgrade.js +3 -3
  22. package/dist/{client-DNf-sJmM.js → client-CRZ-i1tH.js} +2 -2
  23. package/dist/{config-B8QN-04U.js → config-CCI82OTD.js} +2 -2
  24. package/dist/{conversation-privacy-CrYizZM3.js → conversation-privacy-D5Byl5eC.js} +1 -1
  25. package/dist/{credential-sync-BG_Yvja8.js → credential-sync-DUpSxUum.js} +14 -14
  26. package/dist/{db-CWZP77Ex.d.ts → db-Ca9Nt5ci.d.ts} +1 -1
  27. package/dist/{executor-BQV1Zc1z.js → executor-CewKi7EG.js} +4 -2
  28. package/dist/{images-C_0iwPsW.js → images-DY6Pxq4w.js} +2 -2
  29. package/dist/instrumentation.js +2 -2
  30. package/dist/{job-queue-D82gksYM.js → job-queue-C6o2aJQ6.js} +1 -1
  31. package/dist/{logging-Crg0e1jb.js → logging-LQnLlPf8.js} +1 -1
  32. package/dist/nitro.d.ts +1 -1
  33. package/dist/nitro.js +3 -3
  34. package/dist/{oauth-authorization-Bdu39G-H.js → oauth-authorization-Clzkm-Ts.js} +1 -1
  35. package/dist/{oauth-callback-server-eJfI6skP.js → oauth-callback-server-DcYAJSEz.js} +2 -2
  36. package/dist/{pending-auth-DauOaies.js → pending-auth-CT3ojhKe.js} +2 -2
  37. package/dist/{profile-XzHSfIah.js → profile-mWuVZ8ht.js} +4 -4
  38. package/dist/{registry-BJME30oR.js → registry-DPDH6s5G.js} +1 -1
  39. package/dist/{resolve-ZKXfsZuZ.js → resolve-BAN8BSGy.js} +2 -2
  40. package/dist/{runner-DkHL8vRA.js → runner-CqXZJfzk.js} +9 -9
  41. package/dist/{sandbox-egress-signals-BEQsK7ym.js → sandbox-egress-signals-DFUUokws.js} +1 -1
  42. package/dist/{schema-CxWAN2zq.js → schema-Cabm4jcj.js} +1 -1
  43. package/dist/{skills-CMl248J3.js → skills-BtHhSATy.js} +2 -2
  44. package/dist/{spawn-DrvZYrTQ.js → spawn-BszkFJKJ.js} +2 -2
  45. package/dist/{store-BxxbxBlm.d.ts → store-DoewL5mG.d.ts} +0 -7
  46. package/dist/{task-queue-BMzTARMo.js → task-queue-1LP6benz.js} +3 -3
  47. package/dist/{task-runner-CXTpWSmw.js → task-runner-kLN3HC-_.js} +54 -12
  48. package/dist/{checkpoint-CN7yMfSo.js → tasks-CO52vLkI.js} +1064 -18
  49. package/dist/{thread-state-BM1sLptE.js → thread-state-CaFRG5Mh.js} +6 -6
  50. package/dist/{turn-failure-response-ISrbka5I.js → turn-failure-response-Dmovuiq1.js} +9 -8
  51. package/dist/{turn-lifecycle-f7TPxQsc.js → turn-lifecycle-DGqZgU6J.js} +1 -1
  52. package/dist/{turn-session-routing-BcAD7onh.js → turn-session-routing-B_v8Wbn4.js} +1 -1
  53. package/dist/{validation-Bo6BHmig.js → validation-B4L2Rmiw.js} +1 -1
  54. package/dist/{version-Cv__n5ue.js → version-DiR36OaN.js} +1 -1
  55. package/dist/version.js +1 -1
  56. package/dist/{viewer-EftpWXcF.js → viewer-D6stAU7h.js} +9 -11
  57. package/dist/{web-authorization-BLDXKk2u.js → web-authorization-DLMBx2nL.js} +6 -6
  58. package/dist/{web-input-DSa6geKr.js → web-input---xuox1t.js} +136 -147
  59. package/dist/{work-WOk5DovU.js → work-G-HdoOSF.js} +7 -7
  60. package/dist/{worker-B7rtMFzw.js → worker-CDUDfL9P.js} +5 -5
  61. package/migrations/0049_event_automation_status_reason.sql +1 -0
  62. package/migrations/meta/0049_snapshot.json +3581 -0
  63. package/migrations/meta/_journal.json +8 -1
  64. package/package.json +5 -5
@@ -1,28 +1,29 @@
1
- import { b as getGatewayApiKey, c as hasCompactedConversationContext, d as unwrapCurrentInstruction, f as escapeXml, g as getProviderErrorAttributes, h as findProviderError, i as completeText, l as isCompactionSummaryText, m as createProviderError, n as GEN_AI_SERVER_ADDRESS, o as resolveGatewayModel, p as ProviderError, r as completeObject, s as COMPACTION_SUMMARY_PREFIX, t as GEN_AI_PROVIDER_NAME, u as renderCurrentInstruction, v as isProviderRetryError, x as resolveGatewayCredential, y as MISSING_GATEWAY_CREDENTIALS_ERROR } from "./client-DNf-sJmM.js";
2
- import { C as withLogContext, F as normalizeIdentityEmail, I as isRecord$3, L as toOptionalNumber, R as toOptionalString, a as extractGenAiUsageSummary, b as setTags, c as getLogContextAttributesForSpan, d as logException, f as logInfo, g as serializeGenAiAttribute, i as extractGenAiUsageAttributes, l as getTracePropagationHeaders, m as normalizeGenAiFinishReason, p as logWarn, s as getLogContextAttributes, u as logError, v as setSpanAttributes, w as withSpan, x as summarizeMessageText, y as setSpanStatus } from "./logging-Crg0e1jb.js";
3
- import { m as startInactiveSpan, n as captureMessage, v as withActiveSpan } from "./sentry-CgWZM_kV.js";
4
- import { a as toCanonicalInputMessage, c as toGenAiPayloadMetadata, i as runWithConversationPrivacy, l as toGenAiPayloadTraceAttributes, o as toCanonicalOutputMessage, r as resolveConversationPrivacy, s as toGenAiMessagesTraceAttributes, t as canExposeConversationPayload } from "./conversation-privacy-CrYizZM3.js";
5
- import { i as getChatConfig, n as botConfig, o as getSlackBotToken, v as normalizeSlackEmojiName } from "./config-B8QN-04U.js";
1
+ import { b as getGatewayApiKey, c as hasCompactedConversationContext, d as unwrapCurrentInstruction, f as escapeXml, g as getProviderErrorAttributes, h as findProviderError, i as completeText, l as isCompactionSummaryText, m as createProviderError, n as GEN_AI_SERVER_ADDRESS, o as resolveGatewayModel, p as ProviderError, r as completeObject, s as COMPACTION_SUMMARY_PREFIX, t as GEN_AI_PROVIDER_NAME, u as renderCurrentInstruction, v as isProviderRetryError, x as resolveGatewayCredential, y as MISSING_GATEWAY_CREDENTIALS_ERROR } from "./client-CRZ-i1tH.js";
2
+ import { C as withLogContext, F as normalizeIdentityEmail, I as isRecord$3, L as toOptionalNumber, R as toOptionalString, a as extractGenAiUsageSummary, b as setTags, c as getLogContextAttributesForSpan, d as logException, f as logInfo, g as serializeGenAiAttribute, i as extractGenAiUsageAttributes, l as getTracePropagationHeaders, m as normalizeGenAiFinishReason, p as logWarn, u as logError, v as setSpanAttributes, w as withSpan, x as summarizeMessageText, y as setSpanStatus } from "./logging-LQnLlPf8.js";
3
+ import { m as startInactiveSpan, v as withActiveSpan } from "./sentry-CgWZM_kV.js";
4
+ import { a as toCanonicalInputMessage, c as toGenAiPayloadMetadata, i as runWithConversationPrivacy, l as toGenAiPayloadTraceAttributes, o as toCanonicalOutputMessage, r as resolveConversationPrivacy, s as toGenAiMessagesTraceAttributes, t as canExposeConversationPayload } from "./conversation-privacy-D5Byl5eC.js";
5
+ import { i as getChatConfig, n as botConfig, v as normalizeSlackEmojiName } from "./config-CCI82OTD.js";
6
6
  import { a as formatModelProfiles, c as profileConfig, i as formatModelProfile, l as TURN_REASONING_LEVELS, n as ModelProfileNotConfiguredError, o as modelIdForProfile, s as modelProfileSchema } from "./model-profile-BwU8mqwD.js";
7
7
  import { a as isUserActor, d as parseSlackTeamId, f as parseSlackUserId, g as parseSlackMessageTs, l as parseSlackChannelId, s as parseStoredSlackActor, u as parseSlackChannelReferenceId } from "./actor-BTtLaeWy.js";
8
- import { D as readUserTimezone, M as parseDestination, O as saveUserTimezone, P as sameDestination$1, b as addAgentTurnUsage, c as getConversationEventStore, d as getDb, f as getSqlExecutor, l as getConversationMessageSearchStore, r as readActorIdentity, u as getConversationStore, w as hasAgentTurnUsage } from "./viewer-EftpWXcF.js";
8
+ import { F as sameDestination$1, N as parseDestination, T as hasAgentTurnUsage, c as getConversationEventStore, d as getDb, f as getSqlExecutor, l as getConversationMessageSearchStore, r as readActorIdentity, u as getConversationStore, x as addAgentTurnUsage } from "./viewer-D6stAU7h.js";
9
9
  import { n as juniorDestinations } from "./destinations-BeBiJeEM.js";
10
- import { A as juniorArtifacts, D as juniorConversations, O as juniorIdentities, S as juniorConversationAnnotations, T as juniorAttachments, _ as juniorConversationBriefs, a as juniorSchedulerRuns, f as AGENT_INVOCATION_MAILBOX_STATUSES, l as juniorStats, o as juniorSchedulerTasks, s as juniorAutomationVersions, u as juniorEventAutomations } from "./executor-BQV1Zc1z.js";
10
+ import { A as juniorArtifacts, D as juniorConversations, O as juniorIdentities, S as juniorConversationAnnotations, T as juniorAttachments, _ as juniorConversationBriefs, a as juniorSchedulerRuns, f as AGENT_INVOCATION_MAILBOX_STATUSES, l as juniorStats } from "./executor-CewKi7EG.js";
11
11
  import { d as instructionActors, f as instructionProvenanceFor, p as sameActorIdentity, t as KNOWN_CONVERSATION_EVENT_TYPES, u as contextProvenance } from "./history-BLTUgFYO.js";
12
12
  import { a as listReferenceFiles, c as soulPathCandidates, l as worldPathCandidates } from "./package-discovery-BVvSnQBB.js";
13
- import { a as resolveAuthTokenPlaceholder, c as fetchWithBoundedOAuthErrorBodies, d as credentialContextForActor, f as credentialContextSchema, h as CredentialUnavailableError, i as resolvePluginHeaderEnvRefs, l as getOAuthProviderErrorAttributes, m as credentialUserSubjectId, o as resolvePluginCommandEnv, p as credentialSubjectSchema, s as OAuthProviderError } from "./registry-BJME30oR.js";
14
- import { n as pluginCatalogRuntime } from "./catalog-runtime-DpB_hG_I.js";
15
- import { a as parseSkillInvocation, i as parseSkillFile, n as findSkillByName, r as loadSkillsByName, t as discoverSkills } from "./skills-CMl248J3.js";
16
- import { C as isToolResultError, D as stripRuntimeTurnContext, E as retainRuntimeTurnContext, H as recordAuthorizationRequested, I as loadTurnRoute, K as recordToolExecutionStarted, L as openConversationProjection, M as loadConnectedMcpProviders, N as loadConversationProjection, O as trimTrailingAssistantMessages, R as recordAgentsInstructionsUpdated, S as isContinuablePiBoundary, T as normalizeToolNameFromResult, U as recordGuardianActionReviewed, W as recordMcpProviderConnected, _ as getTerminalAssistantMessages, _t as historyItemFromPiMessage, d as TurnSliceLimitExceededError, f as assertTurnToolCallLimit, g as getPiMessageRole, gt as getAgentContextInputLimitTokens, h as extractAssistantText, ht as getAgentContextCompactionTriggerTokens, k as TURN_CONTEXT_TAG, m as isTurnExecutionLimitExceededError, mt as estimateTextTokens, n as loadTurnCheckpoint, nt as resolveTaskTitle, q as recordTurnRoute, r as saveTurnCheckpoint, t as continuableMessages, tt as fallbackShortTitle, v as getUserMessageInstructionText, vt as piMessageFromHistoryItem, w as isToolResultMessage, x as isAssistantMessage, y as hasRuntimeTurnContext, z as recordAttachmentsDelivered } from "./checkpoint-CN7yMfSo.js";
17
- import { A as getWorkspaceSnapshotBuilder, B as cancelWatch, Bt as saveObjectAnnotations, C as WorkspaceValidationError, D as setWorkspaceSnapshotBuild, E as setWorkspaceSnapshot, F as RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS, G as findMatchingWatches, H as createTimerWatch, I as RESOURCE_SUBSCRIPTION_MAX_TTL_MS, It as matchSlackAttachments, K as listWatches, L as RESOURCE_WATCH_TOOL_SOURCE, M as workspaceRepoCheckoutPath, N as resolveChannelCapabilities, O as SANDBOX_RUNTIME, P as getSlackToolContext, R as STOP_WATCHING_TOOL_NAME, S as updateWorkspace, T as loadSnapshotsForProfile, U as createWatch, Vt as createPluginAnnotations, W as deliverWatch, _ as deleteWorkspace, at as getSlackClient, b as listWorkspaces, c as getPluginTools, ct as normalizeSlackConversationId, et as verifyTimerWatchCredentialSubject, g as createWorkspace, gt as getDashboardTaskLink, j as tryWorkspaceRepoCheckoutPath, k as deleteWorkspaceSnapshotBuilders, l as getPluginUserPromptContributions, mt as getDashboardConversationLink, n as applyPluginFormatMarkdown, nt as downloadPrivateSlackFile, ot as isConversationScopedChannel, pt as JUNIOR_THREAD_STATE_TTL_MS, r as createPluginHookRunner, rt as getFilePermalink, s as getPluginSystemPromptContributions, st as isDmChannel$1, tt as SlackActionError, u as getPlugins, ut as withSlackRetries, v as getWorkspace, w as invalidateReadySnapshot, xt as createPluginLogger, y as getWorkspaceByName, z as cancelSubscriptions } from "./agent-hooks-DzfA5m3R.js";
13
+ import { a as resolveAuthTokenPlaceholder, c as fetchWithBoundedOAuthErrorBodies, d as credentialContextForActor, f as credentialContextSchema, h as CredentialUnavailableError, i as resolvePluginHeaderEnvRefs, l as getOAuthProviderErrorAttributes, m as credentialUserSubjectId, o as resolvePluginCommandEnv, p as credentialSubjectSchema, s as OAuthProviderError } from "./registry-DPDH6s5G.js";
14
+ import { n as pluginCatalogRuntime } from "./catalog-runtime-Tz0O2MZe.js";
15
+ import { a as parseSkillInvocation, i as parseSkillFile, n as findSkillByName, r as loadSkillsByName, t as discoverSkills } from "./skills-BtHhSATy.js";
16
+ import { A as formatSlackLink, Ct as loadConversationProjection, D as automationRevision, Dt as openConversationProjection, Et as loadTurnRoute, F as addReactionToMessage, G as loadTurnCheckpoint, Gt as fallbackShortTitle, I as getSlackMessagePermalink, It as recordToolExecutionStarted, K as saveTurnCheckpoint, Kt as resolveTaskTitle, L as openSlackDirectMessage, Lt as recordTurnRoute, Mt as recordAuthorizationRequested, N as slackMention, Nt as recordGuardianActionReviewed, Ot as recordAgentsInstructionsUpdated, Pt as recordMcpProviderConnected, R as postSlackEphemeralMessage, St as loadConnectedMcpProviders, V as uploadFilesToConversation, W as continuableMessages, _t as trimTrailingAssistantMessages, a as readScheduledAutomation, an as getAgentContextCompactionTriggerTokens, at as getPiMessageRole, b as saveEventAutomation, cn as piMessageFromHistoryItem, ct as hasRuntimeTurnContext, dt as isContinuablePiBoundary, et as TurnSliceLimitExceededError, f as createEventAutomation, ft as isToolResultError, g as getEventAutomation, gt as stripRuntimeTurnContext, ht as retainRuntimeTurnContext, in as estimateTextTokens, it as extractAssistantText, k as escapeSlackMrkdwnText, kt as recordAttachmentsDelivered, mt as normalizeToolNameFromResult, o as saveScheduledAutomation, on as getAgentContextInputLimitTokens, ot as getTerminalAssistantMessages, p as deleteEventAutomation, pt as isToolResultMessage, r as listScheduledAutomationsForTeam, rt as isTurnExecutionLimitExceededError, sn as historyItemFromPiMessage, st as getUserMessageInstructionText, t as createScheduledAutomation, tt as assertTurnToolCallLimit, ut as isAssistantMessage, v as listEventAutomationsForTeam, vt as TURN_CONTEXT_TAG, x as setEventAutomationStatus, yt as ToolInputError, z as postSlackMessage } from "./tasks-CO52vLkI.js";
17
+ import { A as getWorkspaceSnapshotBuilder, B as cancelWatch, Bt as isConversationScopedChannel, C as WorkspaceValidationError, D as setWorkspaceSnapshotBuild, E as setWorkspaceSnapshot, F as RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS, Ft as SlackActionError, G as findMatchingWatches, H as createTimerWatch, Ht as normalizeSlackConversationId, I as RESOURCE_SUBSCRIPTION_MAX_TTL_MS, It as downloadPrivateSlackFile, K as listWatches, L as RESOURCE_WATCH_TOOL_SOURCE, Lt as getFilePermalink, M as workspaceRepoCheckoutPath, Mt as defaultTimezone, N as resolveChannelCapabilities, Nt as resolveUserTimezone, O as SANDBOX_RUNTIME, Ot as saveObjectAnnotations, P as getSlackToolContext, R as STOP_WATCHING_TOOL_NAME, S as updateWorkspace, T as loadSnapshotsForProfile, U as createWatch, Vt as isDmChannel$1, W as deliverWatch, Wt as withSlackRetries, _ as deleteWorkspace, b as listWorkspaces, c as getPluginTools, et as verifyTimerWatchCredentialSubject, g as createWorkspace, it as getDashboardTaskLink, j as tryWorkspaceRepoCheckoutPath, k as deleteWorkspaceSnapshotBuilders, kt as createPluginAnnotations, l as getPluginUserPromptContributions, lt as createPluginLogger, nt as getDashboardConversationLink, r as createPluginHookRunner, s as getPluginSystemPromptContributions, tt as JUNIOR_THREAD_STATE_TTL_MS, u as getPlugins, v as getWorkspace, w as invalidateReadySnapshot, wt as matchSlackAttachments, y as getWorkspaceByName, z as cancelSubscriptions, zt as getSlackClient } from "./agent-hooks-C5zYOaJk.js";
18
18
  import { F as isVisionImageMediaType, G as removedCardSchema, L as briefLinkSchema, R as briefOutcomeStatusSchema, z as conversationBriefSchema } from "./conversation-DwD-vquD.js";
19
- import { A as SANDBOX_DATA_ROOT, C as getDefaultRedisStateAdapterFor, D as fenceLock, E as acquireActiveLock, F as sandboxSkillFile, I as sandboxSkillPathResolution, M as SANDBOX_SKILLS_ROOT, N as SANDBOX_WORKSPACE_ROOT, O as withLock, P as sandboxSkillDir, _ as workspaceSnapshotNotReadyUserMessage, a as isAbortError, b as buildNonInteractiveShellScript, c as isSandboxCommandStreamInterruptedError, d as isSnapshottingError, f as throwSandboxOperationError, g as isWorkspaceSnapshotNotReadyError, h as getWorkspaceSnapshotNotReadyError, i as ensureDockerDaemon, j as SANDBOX_REPOS_ROOT, k as sleep$2, l as isSandboxMissingError, m as WorkspaceSnapshotNotReadyError, n as hash, o as isAlreadyExistsError, p as wrapSandboxSetupError, r as isStale, s as isSandboxApiTransientError, t as create, u as isSandboxUnavailableError, v as getVercelSandboxCredentials, w as getStateAdapter, x as runNonInteractiveCommand, y as buildCommandScript } from "./profile-XzHSfIah.js";
20
- import { n as canReusePendingAuthLink, t as abandonReplacedPendingAuth } from "./pending-auth-DauOaies.js";
21
- import { a as NO_REPLY_MARKER, c as CooperativeTurnYieldError, i as sanitizeAssistantText, l as TurnInputCommitLostError, n as decideReply, o as isNoReplyMarker, p as isTurnInputCommitLostError, r as getAssistantReplyText, t as authorizationId } from "./oauth-authorization-Bdu39G-H.js";
19
+ import { A as SANDBOX_DATA_ROOT, C as getDefaultRedisStateAdapterFor, D as fenceLock, E as acquireActiveLock, F as sandboxSkillFile, I as sandboxSkillPathResolution, M as SANDBOX_SKILLS_ROOT, N as SANDBOX_WORKSPACE_ROOT, O as withLock, P as sandboxSkillDir, _ as workspaceSnapshotNotReadyUserMessage, a as isAbortError, b as buildNonInteractiveShellScript, c as isSandboxCommandStreamInterruptedError, d as isSnapshottingError, f as throwSandboxOperationError, g as isWorkspaceSnapshotNotReadyError, h as getWorkspaceSnapshotNotReadyError, i as ensureDockerDaemon, j as SANDBOX_REPOS_ROOT, k as sleep$2, l as isSandboxMissingError, m as WorkspaceSnapshotNotReadyError, n as hash, o as isAlreadyExistsError, p as wrapSandboxSetupError, r as isStale, s as isSandboxApiTransientError, t as create, u as isSandboxUnavailableError, v as getVercelSandboxCredentials, w as getStateAdapter, x as runNonInteractiveCommand, y as buildCommandScript } from "./profile-mWuVZ8ht.js";
20
+ import { n as canReusePendingAuthLink, t as abandonReplacedPendingAuth } from "./pending-auth-CT3ojhKe.js";
21
+ import { a as NO_REPLY_MARKER, c as CooperativeTurnYieldError, i as sanitizeAssistantText, l as TurnInputCommitLostError, n as decideReply, o as isNoReplyMarker, p as isTurnInputCommitLostError, r as getAssistantReplyText, t as authorizationId } from "./oauth-authorization-Clzkm-Ts.js";
22
+ import { i as isValidTimeZone, r as getZonedDateTimeParts } from "./cadence-D00SCzT1.js";
22
23
  import { r as EVENT_AUTHOR_ID } from "./prompt-CMU-B4FA.js";
23
- import { a as createSandboxSession, c as postinstall, d as getSandboxResources, i as bindSandboxFileSystem, l as setSnapshotSpanAttributes, n as isMissingError, o as stopSession, r as resolve$1, s as dependencies } from "./resolve-ZKXfsZuZ.js";
24
- import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline, n as sendNextWorkspaceSnapshotJob, r as sendWorkspaceSnapshotJob } from "./job-queue-D82gksYM.js";
25
- import { A as eventAutomationSchema, D as scheduledRunSchema, E as scheduledAutomationSchema, M as compileScheduleIntent, N as isValidTimeZone$1, O as automationInstructionToolSchema, P as scheduleIntentSchema, R as getZonedDateTimeParts, S as automationVersionSchema, T as SCHEDULED_AUTOMATION_SYSTEM_ACTOR, j as ScheduleIntentError, k as automationTitleSchema, z as taskOutcomeInputSchema } from "./automation-CrQGQ2MT.js";
24
+ import { a as createSandboxSession, c as postinstall, d as getSandboxResources, i as bindSandboxFileSystem, l as setSnapshotSpanAttributes, n as isMissingError, o as stopSession, r as resolve$1, s as dependencies } from "./resolve-BAN8BSGy.js";
25
+ import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline, n as sendNextWorkspaceSnapshotJob, r as sendWorkspaceSnapshotJob } from "./job-queue-C6o2aJQ6.js";
26
+ import { F as taskOutcomeInputSchema, M as compileScheduleIntent, N as scheduleIntentSchema, O as automationInstructionToolSchema, T as SCHEDULED_AUTOMATION_SYSTEM_ACTOR, j as ScheduleIntentError, k as automationTitleSchema } from "./automation-gznOMaQ2.js";
26
27
  import { r as isExperimentalFeatureEnabled } from "./experimental-Qn9rKLgx.js";
27
28
  import { n as privateTraceResultAttributes } from "./private-trace-result-BLvBjy_e.js";
28
29
  import { streamSimple } from "@earendil-works/pi-ai/compat";
@@ -33,8 +34,8 @@ import { isDeepStrictEqual } from "node:util";
33
34
  import { AsyncLocalStorage } from "node:async_hooks";
34
35
  import { z } from "zod";
35
36
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
36
- import { and, asc, desc, eq, gt, gte, inArray, isNotNull, isNull, lt, lte, ne, notExists, notInArray, or, sql } from "drizzle-orm";
37
- import { EgressAuthRequired, EgressPolicyDenied, PluginToolInputError, actorSchema, destinationSchema, eventMatchSchema, eventMatches, eventSchema, missingToolAnnotationKeys, normalizeEventIdentifier, objectTypeSchema, ownedObjectAnnotationSchema, pluginAuthorizationSchema, pluginCredentialHeaderTransformSchema, pluginCredentialResultSchema, pluginGrantSchema, pluginProviderAccountSchema, pluginStoredTokensSchema, slackDestinationSchema, sourceSchema, stableEventMatchKey, taskOutcomeSchema, watchResultSchema } from "@sentry/junior-plugin-api";
37
+ import { and, asc, desc, eq, gt, gte, inArray, isNotNull, isNull, lt, lte, ne, notExists, or, sql } from "drizzle-orm";
38
+ import { EgressAuthRequired, EgressPolicyDenied, PluginToolInputError, actorSchema, destinationSchema, eventMatchSchema, eventSchema, missingToolAnnotationKeys, normalizeEventIdentifier, objectTypeSchema, ownedObjectAnnotationSchema, pluginAuthorizationSchema, pluginCredentialHeaderTransformSchema, pluginCredentialResultSchema, pluginGrantSchema, pluginProviderAccountSchema, pluginStoredTokensSchema, sourceSchema, stableEventMatchKey, watchResultSchema } from "@sentry/junior-plugin-api";
38
39
  import { alias } from "drizzle-orm/pg-core";
39
40
  import { Agent, estimateContextTokens } from "@earendil-works/pi-agent-core";
40
41
  import fs from "node:fs/promises";
@@ -221,15 +222,6 @@ var SkillSandbox = class {
221
222
  }
222
223
  };
223
224
  //#endregion
224
- //#region src/chat/tools/execution/tool-input-error.ts
225
- /** Thrown when a tool fails due to invalid model/user input, not a system error. */
226
- var ToolInputError = class extends Error {
227
- constructor(message, options) {
228
- super(message, options);
229
- this.name = "ToolInputError";
230
- }
231
- };
232
- //#endregion
233
225
  //#region src/chat/tools/sandbox/file-utils.ts
234
226
  const MAX_TEXT_CHARS = 6e4;
235
227
  const RIPGREP_EXCLUDED_GLOBS = ["!**/.git/**", "!**/node_modules/**"];
@@ -266,7 +258,8 @@ function isMissingPathError(error) {
266
258
  }
267
259
  /** Reject an expected missing search/list path through the tool-error channel. */
268
260
  function missingPathSearchResult(params) {
269
- throw new ToolInputError(`Path not found: ${params.displayPath ?? params.missingPath ?? params.path}`);
261
+ const textPath = params.displayPath ?? params.missingPath ?? params.path;
262
+ throw new ToolInputError(`Path not found: ${textPath}`);
270
263
  }
271
264
  function escapeRegExp(value) {
272
265
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -810,10 +803,102 @@ function shouldEmitDevAgentTrace() {
810
803
  return process.env.NODE_ENV === "development";
811
804
  }
812
805
  //#endregion
806
+ //#region src/chat/automation-result.ts
807
+ /**
808
+ * Automation run result.
809
+ *
810
+ * A Scheduled automation or Event automation run ends with one declared
811
+ * result. The run does not deliver its final assistant text. The work owner
812
+ * reads the declared result and applies the stored outcomes.
813
+ */
814
+ /** Model-facing tool that ends an Automation run. */
815
+ const FINISH_AUTOMATION_RUN_TOOL_NAME = "finishAutomationRun";
816
+ /** One declared result for an Automation run. */
817
+ const automationResultSchema = z.discriminatedUnion("result", [
818
+ z.object({
819
+ result: z.literal("send_message"),
820
+ message: z.string().transform(sanitizeAssistantText).pipe(z.string().min(1))
821
+ }),
822
+ z.object({
823
+ result: z.literal("no_action"),
824
+ reason: z.string().trim().min(1)
825
+ }),
826
+ z.object({
827
+ result: z.literal("misconfigured"),
828
+ reason: z.string().trim().min(1)
829
+ })
830
+ ]);
831
+ /**
832
+ * Return whether the runtime delivers the final assistant text of a run. It
833
+ * does not for a dispatch with no outcomes. It does not for an Automation
834
+ * run, which posts only its declared message, after the run.
835
+ */
836
+ function deliversFinalText(dispatch) {
837
+ return dispatch?.outcomes?.length !== 0 && !dispatch?.declaresResult;
838
+ }
839
+ /**
840
+ * Return the text to post after a run ends, if any. A chat Turn already
841
+ * delivered a successful reply, so it posts only its failure reply. An
842
+ * Automation run posts only a declared message. It never posts a failure
843
+ * reply to its outcomes. The failed execution shows on the Automation.
844
+ */
845
+ function finishedRunReply(result, dispatch) {
846
+ if (!dispatch?.declaresResult) return result.diagnostics.outcome === "success" ? void 0 : result.text;
847
+ return result.diagnostics.outcome === "success" && result.automation?.result === "send_message" && dispatch.outcomes?.length !== 0 ? result.text : void 0;
848
+ }
849
+ /**
850
+ * Read the last successful declared result from agent history items.
851
+ *
852
+ * Pass the whole run history, not only the current slice. An Automation run
853
+ * owns its dispatch Conversation, and a resumed slice must see a result that
854
+ * an earlier slice already saved.
855
+ */
856
+ function readAutomationResult(messages) {
857
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
858
+ const message = messages[index];
859
+ if (!isToolResultMessage(message) || isToolResultError(message) || normalizeToolNameFromResult(message) !== "finishAutomationRun") continue;
860
+ const parsed = automationResultSchema.safeParse(message.details);
861
+ if (parsed.success) return parsed.data;
862
+ }
863
+ }
864
+ /**
865
+ * Map one finished agent run to its dispatch outcome. A declared
866
+ * `misconfigured` result becomes a blocked dispatch with the declared reason.
867
+ */
868
+ function runDispatchOutcome(result) {
869
+ if (result.diagnostics.outcome !== "success") return {
870
+ errorMessage: result.diagnostics.errorMessage ?? `Agent turn ended with ${result.diagnostics.outcome}.`,
871
+ outcome: "failed"
872
+ };
873
+ if (result.automation?.result === "misconfigured") return {
874
+ errorMessage: result.automation.reason,
875
+ outcome: "blocked"
876
+ };
877
+ return { outcome: "completed" };
878
+ }
879
+ const MISSING_RESULT_REMINDER = "This automation run has not ended. Your text was not delivered. Call `finishAutomationRun` now with one result.";
880
+ /**
881
+ * Return agent history plus a reminder when the model stopped without a
882
+ * declared result, or undefined when the run already declared one.
883
+ */
884
+ function remindMissingAutomationResult(messages) {
885
+ const last = messages.at(-1);
886
+ if (!isAssistantMessage(last) || last.stopReason !== "stop" || readAutomationResult(messages)) return;
887
+ return [...messages, {
888
+ role: "user",
889
+ content: [{
890
+ type: "text",
891
+ text: MISSING_RESULT_REMINDER
892
+ }],
893
+ timestamp: Date.now()
894
+ }];
895
+ }
896
+ //#endregion
813
897
  //#region src/chat/services/turn-result.ts
814
898
  /** Process raw agent messages into a structured AgentRunResult. */
815
899
  function buildTurnResult(input) {
816
- const { newMessages, toolCalls, sandboxRef, durationMs, shouldTrace, usage, executionProfile, modelId } = input;
900
+ if (input.requireAutomationResult) return buildAutomationTurnResult(input);
901
+ const { newMessages, sandboxRef, shouldTrace } = input;
817
902
  const toolResults = newMessages.filter(isToolResultMessage);
818
903
  const assistantMessages = newMessages.filter(isAssistantMessage);
819
904
  const terminalAssistantMessages = getTerminalAssistantMessages(newMessages);
@@ -856,29 +941,68 @@ function buildTurnResult(input) {
856
941
  "app.ai.assistant_messages": assistantMessages.length,
857
942
  ...stopReason ? { "gen_ai.response.finish_reasons": [stopReason] } : void 0
858
943
  });
859
- const resolvedDiagnostics = {
944
+ return {
945
+ text: primaryText,
946
+ sandboxRef,
947
+ piMessages: input.piMessages,
948
+ diagnostics: buildDiagnostics(input, {
949
+ outcome,
950
+ usedPrimaryText,
951
+ stopReason,
952
+ errorMessage,
953
+ lastAssistant
954
+ })
955
+ };
956
+ }
957
+ function buildDiagnostics(input, result) {
958
+ const { outcome, errorMessage, lastAssistant } = result;
959
+ const toolResults = input.newMessages.filter(isToolResultMessage);
960
+ return {
860
961
  outcome,
861
- modelId,
862
- assistantMessageCount: assistantMessages.length,
863
- reasoningLevel: executionProfile.reasoningLevel,
864
- toolCalls,
962
+ modelId: input.modelId,
963
+ assistantMessageCount: input.newMessages.filter(isAssistantMessage).length,
964
+ reasoningLevel: input.executionProfile.reasoningLevel,
965
+ toolCalls: input.toolCalls,
865
966
  toolResultCount: toolResults.length,
866
- toolErrorCount,
867
- usedPrimaryText,
868
- durationMs,
869
- usage,
870
- stopReason,
967
+ toolErrorCount: toolResults.filter((toolResult) => toolResult.isError).length,
968
+ usedPrimaryText: result.usedPrimaryText,
969
+ durationMs: input.durationMs,
970
+ usage: input.usage,
971
+ stopReason: result.stopReason,
871
972
  errorMessage,
872
973
  providerError: outcome === "provider_error" && errorMessage ? createProviderError(errorMessage, {
873
- modelId,
874
- retryable: lastAssistant !== void 0 && isAssistantMessage(lastAssistant) && isRetryableAssistantError(lastAssistant)
974
+ modelId: input.modelId,
975
+ retryable: isAssistantMessage(lastAssistant) && isRetryableAssistantError(lastAssistant)
875
976
  }) : void 0
876
977
  };
978
+ }
979
+ /**
980
+ * Build the result of an Automation run from its declared result. Final
981
+ * assistant text is not used. A run without a declared result fails.
982
+ */
983
+ function buildAutomationTurnResult(input) {
984
+ const lastAssistant = input.newMessages.filter(isAssistantMessage).at(-1);
985
+ const stopReason = lastAssistant?.stopReason;
986
+ const automation = readAutomationResult(input.piMessages ?? input.newMessages);
987
+ const outcome = automation ? "success" : stopReason === "error" ? "provider_error" : "execution_failure";
988
+ const errorMessage = outcome === "provider_error" ? lastAssistant?.errorMessage : outcome === "execution_failure" ? "Automation run ended without a declared result." : void 0;
989
+ if (input.shouldTrace) logInfo("agent.message.generated", {
990
+ "app.message.kind": "automation_result",
991
+ "app.ai.outcome": outcome,
992
+ "app.automation.result": automation?.result ?? "missing"
993
+ });
877
994
  return {
878
- text: primaryText,
879
- sandboxRef,
995
+ text: automation?.result === "send_message" ? automation.message : "",
996
+ ...automation ? { automation } : void 0,
997
+ sandboxRef: input.sandboxRef,
880
998
  piMessages: input.piMessages,
881
- diagnostics: resolvedDiagnostics
999
+ diagnostics: buildDiagnostics(input, {
1000
+ outcome,
1001
+ usedPrimaryText: automation?.result === "send_message",
1002
+ stopReason,
1003
+ errorMessage,
1004
+ lastAssistant
1005
+ })
882
1006
  };
883
1007
  }
884
1008
  //#endregion
@@ -1741,11 +1865,19 @@ const SLACK_ACTION_RULES = [
1741
1865
  "- Ambient reaction requests target the current inbound message; do not ask for a message reference.",
1742
1866
  `- When no visible thread reply is requested or useful, keep tool-calling messages text-free and make the final message exactly ${NO_REPLY_MARKER}.`
1743
1867
  ];
1868
+ const AUTOMATION_RUN_RULES = [
1869
+ "- This run comes from a stored automation, not from a person. The stored instruction is the job, and its creator already approved it.",
1870
+ "- Nobody reads this run while it executes. Do not ask questions, ask for approval, or offer options. Infer conservatively and do the work now.",
1871
+ "- Check every condition in the instruction before any action with side effects. If a condition is not met or cannot be verified, stop before you act.",
1872
+ "- A tool result with `timed_out: true` means that attempt did not finish. Before you retry work that may have side effects, inspect authoritative state and do not repeat a mutation that already applied.",
1873
+ `- Assistant text is never delivered. End the run with \`${FINISH_AUTOMATION_RUN_TOOL_NAME}\`. Do not post the result with another tool.`
1874
+ ];
1744
1875
  const SAFETY_RULES = [
1745
1876
  "- Stay within the user's request and the runtime's available capabilities; do not pursue independent goals, persistence, replication, credential gathering, or access expansion.",
1746
1877
  "- Respect stop, pause, audit, and approval boundaries. Do not bypass safeguards or persuade the user to weaken them.",
1747
1878
  "- Do not change system prompts, tool policies, security settings, credentials, or runtime configuration unless the user explicitly requests that exact administrative action and an available tool permits it."
1748
1879
  ];
1880
+ const AUTOMATION_FAILURE_RULES = ["- Keep commands, error output, raw tool payloads, and internal routing metadata out of the message.", `- When a failure stops the job, put its cause in the \`${FINISH_AUTOMATION_RUN_TOOL_NAME}\` reason, not in the message.`];
1749
1881
  const FAILURE_RULES = [
1750
1882
  "- For tool/runtime failures, run the named check before diagnosing and report the exact failed command plus stderr/exit code.",
1751
1883
  "- If a fact cannot be verified after focused checks, say what you checked and what blocked a stronger answer.",
@@ -1758,21 +1890,33 @@ function renderRuleSection(tag, lines) {
1758
1890
  `</${tag}>`
1759
1891
  ].join("\n");
1760
1892
  }
1761
- function buildBehaviorSection(platform) {
1762
- const sections = [
1893
+ function buildBehaviorSection(platform, mode) {
1894
+ const runSections = mode === "automation" ? [renderRuleSection("automation-run", AUTOMATION_RUN_RULES)] : [
1895
+ renderRuleSection("task-execution", TASK_EXECUTION_RULES),
1896
+ renderRuleSection("conversation", CONVERSATION_RULES),
1897
+ ...platform === "slack" ? [renderRuleSection("slack-actions", SLACK_ACTION_RULES)] : []
1898
+ ];
1899
+ const automation = mode === "automation";
1900
+ return [
1763
1901
  renderRuleSection("tool-policy", TOOL_POLICY_RULES),
1764
- renderRuleSection("tool-call-style", TOOL_CALL_STYLE_RULES),
1902
+ ...automation ? [] : [renderRuleSection("tool-call-style", TOOL_CALL_STYLE_RULES)],
1765
1903
  renderRuleSection("skill-policy", SKILL_POLICY_RULES),
1766
1904
  renderRuleSection("planning", PLANNING_RULES),
1767
- renderRuleSection("task-execution", TASK_EXECUTION_RULES),
1768
- renderRuleSection("conversation", CONVERSATION_RULES),
1905
+ ...runSections,
1769
1906
  renderRuleSection("safety", SAFETY_RULES),
1770
- renderRuleSection("failure-handling", FAILURE_RULES)
1771
- ];
1772
- if (platform === "slack") sections.splice(6, 0, renderRuleSection("slack-actions", SLACK_ACTION_RULES));
1773
- return sections.join("\n\n");
1907
+ renderRuleSection("failure-handling", automation ? AUTOMATION_FAILURE_RULES : FAILURE_RULES)
1908
+ ].join("\n\n");
1909
+ }
1910
+ function buildAutomationOutputSection(platform) {
1911
+ return [
1912
+ platform === "slack" ? `<output format="slack-markdown">` : `<output format="markdown">`,
1913
+ "- Follow any format in the instruction. Otherwise keep the message as short as the job allows, and do not describe the run.",
1914
+ platform === "slack" ? "- Use Slack-flavored Markdown: **bold** section labels, `code`, [text](url) links, bullet lists, and fenced code blocks. No hash-prefixed headings and no tables." : "- Use concise Markdown: short paragraphs, bullets, links, and fenced code blocks.",
1915
+ "</output>"
1916
+ ].join("\n");
1774
1917
  }
1775
- function buildOutputSection(platform) {
1918
+ function buildOutputSection(platform, mode) {
1919
+ if (mode === "automation") return buildAutomationOutputSection(platform);
1776
1920
  if (platform === "local") return [
1777
1921
  `<output format="markdown">`,
1778
1922
  "- Start with the answer or result, not internal process narration.",
@@ -1840,13 +1984,22 @@ function formatDestinationLines(destination) {
1840
1984
  `- destination.channel_id: ${escapeXml(destination.channelId)}`
1841
1985
  ];
1842
1986
  }
1987
+ function formatOutcomeLines(outcomes) {
1988
+ if (!outcomes) return ["- dispatch.outcomes: send_message to the destination below (legacy default)"];
1989
+ if (outcomes.length === 0) return ["- dispatch.outcomes: none; successful work posts nothing"];
1990
+ return outcomes.map((outcome, index) => {
1991
+ const { channelId, threadTs } = outcome.destination;
1992
+ const recipient = isDmChannel$1(channelId) ? "the creator's direct message" : "a slack channel";
1993
+ const target = [`channel_id=${escapeXml(channelId)}`, ...threadTs ? [`thread_ts=${escapeXml(threadTs)}`] : []].join(" ");
1994
+ return `- dispatch.outcome.${index + 1}: ${outcome.action} to ${recipient} (${target})`;
1995
+ });
1996
+ }
1843
1997
  function buildDispatchSection(params) {
1844
1998
  if (!params) return null;
1845
1999
  const metadataLines = Object.entries(params.metadata ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `- dispatch.metadata.${escapeXml(key)}: ${escapeXml(value)}`);
1846
2000
  return renderTag("dispatch", [
1847
2001
  "- dispatch.execution: execute the dispatched input now",
1848
- "- dispatch.delivery: the runtime delivers the final answer to the destination",
1849
- "- dispatch.delivery_rule: do not request or require a separate posting tool just to deliver the final answer",
2002
+ ...params.declaresResult ? formatOutcomeLines(params.outcomes) : ["- dispatch.delivery: the runtime delivers the final answer to the destination", "- dispatch.delivery_rule: do not request or require a separate posting tool just to deliver the final answer"],
1850
2003
  ...params.actor ? [`- dispatch.actor.platform: ${escapeXml(params.actor.platform)}`, `- dispatch.actor.name: ${escapeXml(params.actor.name)}`] : [],
1851
2004
  ...params.plugin ? [`- dispatch.plugin: ${escapeXml(params.plugin)}`] : [],
1852
2005
  ...formatSourceLines(params.source),
@@ -1896,23 +2049,29 @@ function buildPluginSystemPromptContributions(contributions) {
1896
2049
  for (const contribution of contributions) lines.push(` <plugin-contribution plugin="${escapeXml(contribution.pluginName)}" id="${escapeXml(contribution.id)}">`, escapeXml(contribution.text.trim()), " </plugin-contribution>");
1897
2050
  return renderTagBlock("plugin-system-context", lines.join("\n"));
1898
2051
  }
1899
- function buildStaticSystemPrompt(platform) {
2052
+ function buildStaticSystemPrompt(platform, mode) {
1900
2053
  return [
1901
2054
  platform === "slack" ? SLACK_HEADER : LOCAL_HEADER,
1902
2055
  buildIdentitySection(platform),
1903
2056
  buildPersonalitySection(),
1904
2057
  buildWorldSection(),
1905
- buildBehaviorSection(platform),
1906
- buildOutputSection(platform)
2058
+ buildBehaviorSection(platform, mode),
2059
+ buildOutputSection(platform, mode)
1907
2060
  ].filter((section) => Boolean(section)).join("\n\n");
1908
2061
  }
1909
2062
  const STATIC_SYSTEM_PROMPTS = {
1910
- local: buildStaticSystemPrompt("local"),
1911
- slack: buildStaticSystemPrompt("slack")
2063
+ automation: {
2064
+ local: buildStaticSystemPrompt("local", "automation"),
2065
+ slack: buildStaticSystemPrompt("slack", "automation")
2066
+ },
2067
+ conversation: {
2068
+ local: buildStaticSystemPrompt("local", "conversation"),
2069
+ slack: buildStaticSystemPrompt("slack", "conversation")
2070
+ }
1912
2071
  };
1913
- /** Return the fixed instructions shared by every Conversation and Turn. */
1914
- function buildSystemPrompt(platform) {
1915
- return STATIC_SYSTEM_PROMPTS[platform];
2072
+ /** Return the fixed instructions for one platform and prompt mode. */
2073
+ function buildSystemPrompt(platform, mode = "conversation") {
2074
+ return STATIC_SYSTEM_PROMPTS[mode][platform];
1916
2075
  }
1917
2076
  /** Build volatile runtime context that belongs in the user turn, not the system prompt. */
1918
2077
  function buildTurnContextPrompt(params) {
@@ -2199,9 +2358,10 @@ async function assemblePrompt(args) {
2199
2358
  userContentParts: requestContentParts
2200
2359
  }) : void 0;
2201
2360
  const needsBootstrapContextForPrompt = shouldPromptAgent && !replayedPrompt && !hasRuntimeTurnContext(promptHistoryMessages);
2202
- const platform = args.run.delivery && args.run.location?.provider === "slack" ? "slack" : "local";
2361
+ const promptMode = args.run.dispatch?.declaresResult ? "automation" : "conversation";
2362
+ const platform = (args.run.delivery || promptMode === "automation") && args.run.location?.provider === "slack" ? "slack" : "local";
2203
2363
  const pluginSystemPrompt = buildPluginSystemPromptContributions(await getPluginSystemPromptContributions(platform));
2204
- const baseInstructions = [buildSystemPrompt(platform), pluginSystemPrompt].filter((section) => Boolean(section)).join("\n\n");
2364
+ const baseInstructions = [buildSystemPrompt(platform, promptMode), pluginSystemPrompt].filter((section) => Boolean(section)).join("\n\n");
2205
2365
  const pluginUserPromptContributions = !shouldPromptAgent || replayedPrompt ? [] : await getPluginUserPromptContributions({
2206
2366
  context: args.toolRuntimeContext,
2207
2367
  turnId: args.turnId
@@ -2660,541 +2820,6 @@ function formatSandboxCommandResult(params) {
2660
2820
  });
2661
2821
  }
2662
2822
  //#endregion
2663
- //#region src/chat/slack/post-warning.ts
2664
- const codes = /* @__PURE__ */ new Set([
2665
- "invalid_metadata_format",
2666
- "invalid_metadata_schema",
2667
- "metadata_too_large",
2668
- "invalid_arguments",
2669
- "invalid_blocks",
2670
- "missing_scope",
2671
- "missing_charset",
2672
- "superfluous_charset",
2673
- "message_truncated"
2674
- ]);
2675
- const pathFields = /* @__PURE__ */ new Set([
2676
- "metadata",
2677
- "event_type",
2678
- "event_payload",
2679
- "entities",
2680
- "entity_type",
2681
- "entity_payload",
2682
- "external_ref",
2683
- "id",
2684
- "type",
2685
- "url",
2686
- "attributes",
2687
- "title",
2688
- "text",
2689
- "display_id",
2690
- "display_type",
2691
- "product_name",
2692
- "fields",
2693
- "status",
2694
- "custom_fields",
2695
- "key",
2696
- "label",
2697
- "value",
2698
- "long",
2699
- "format",
2700
- "link",
2701
- "display_order",
2702
- "app_unfurl_url",
2703
- "app_id",
2704
- "created_by",
2705
- "created_at",
2706
- "updated_by",
2707
- "updated_at",
2708
- "full_size_preview",
2709
- "preview_url",
2710
- "mime_type"
2711
- ]);
2712
- const diagnosticWords = new Set(`WARN ERROR Message message metadata was incorrectly formatted The the will be
2713
- ignored as a result For for event entity refer to following errors error
2714
- missing required field fields property properties additional unexpected unknown
2715
- unsupported invalid expected received actual type types must should match matches
2716
- matching does not is are has have of at in on and or an only allowed allow
2717
- value values string number integer boolean object array null minimum maximum
2718
- length min max items item empty nonempty too long short large small size limit
2719
- exceeded exceeds less greater than equal valid format schema validation failed
2720
- failure cannot contain contains found one any all none enum pattern definition
2721
- defined constraint constraints satisfy satisfies with without provided specified
2722
- unrecognized unrecognised recognized recognised extraneous disallowed duplicate
2723
- unique malformed nested root exactly needs need requires requirement supported
2724
- json-pointer input different schema_uri schema_id schemas additionalProperties
2725
- minLength maxLength minItems maxItems anyOf oneOf allOf`.split(/\s+/));
2726
- function summarizeDiagnostic(value) {
2727
- 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) => {
2728
- if (token.startsWith("/")) return token.split("/").slice(0, 17).map((part, index) => index === 0 ? "" : pathFields.has(part) ? part : "*").join("/");
2729
- return codes.has(token) || pathFields.has(token) || diagnosticWords.has(token) ? token : "[value]";
2730
- }).replace(/\s+/g, " ").slice(0, 2e3);
2731
- }
2732
- /** Report Slack warnings once, on the request span, without retrying the post. */
2733
- function captureSlackPostWarning(response, attributes) {
2734
- const warnings = response.response_metadata?.warnings ?? [];
2735
- const messages = response.response_metadata?.messages ?? [];
2736
- const knownCodes = [...new Set(warnings.slice(0, 20).filter((code) => codes.has(code)))].sort();
2737
- const diagnostics = {
2738
- "app.slack.warning_count": warnings.length,
2739
- "app.slack.response_message_count": messages.length,
2740
- "app.slack.diagnostic_codes": knownCodes
2741
- };
2742
- setSpanAttributes(diagnostics);
2743
- if (!warnings.length && !messages.length) return;
2744
- captureMessage("Slack chat.postMessage returned warnings", {
2745
- level: "warning",
2746
- fingerprint: ["slack.chat.postMessage.warning", ...knownCodes.length ? knownCodes : ["unknown"]],
2747
- tags: { "app.slack.method": "chat.postMessage" },
2748
- extra: {
2749
- ...getLogContextAttributes(),
2750
- ...attributes,
2751
- ...diagnostics,
2752
- "app.slack.response_diagnostics": {
2753
- warnings: warnings.slice(0, 20).map(summarizeDiagnostic),
2754
- messages: messages.slice(0, 20).map(summarizeDiagnostic)
2755
- }
2756
- }
2757
- });
2758
- }
2759
- //#endregion
2760
- //#region src/chat/slack/work-object.ts
2761
- const url = z.url({ protocol: /^https?$/ });
2762
- const textField = z.strictObject({
2763
- type: z.literal("string"),
2764
- key: z.string().min(1),
2765
- label: z.string().min(1),
2766
- value: z.string(),
2767
- long: z.boolean().optional(),
2768
- format: z.literal("markdown").optional(),
2769
- link: url.optional()
2770
- }).refine((field) => !field.format || !field.link, { message: "Slack string fields cannot combine format and link" });
2771
- const customField = z.discriminatedUnion("type", [textField, z.strictObject({
2772
- type: z.literal("slack#/types/timestamp"),
2773
- key: z.string().min(1),
2774
- label: z.string().min(1),
2775
- value: z.number().int(),
2776
- link: url.optional()
2777
- })]);
2778
- const payload = z.strictObject({
2779
- attributes: z.strictObject({
2780
- title: z.strictObject({ text: z.string().min(1) }),
2781
- display_id: z.string().optional(),
2782
- display_type: z.string().optional(),
2783
- product_name: z.string().optional(),
2784
- product_icon: z.strictObject({
2785
- url,
2786
- alt_text: z.string().min(1)
2787
- }).optional()
2788
- }),
2789
- custom_fields: z.array(customField).optional(),
2790
- display_order: z.array(z.string()).optional()
2791
- });
2792
- /** Limit IDs to Junior's safe subset; Slack silently rejects JSON punctuation. */
2793
- const slackExternalRefIdSchema = z.string().min(1).regex(/^[A-Za-z0-9_-]+$/, "Unsupported Slack external reference ID");
2794
- const entity = z.strictObject({
2795
- external_ref: z.strictObject({
2796
- id: slackExternalRefIdSchema,
2797
- type: z.string().min(1).optional()
2798
- }),
2799
- url
2800
- });
2801
- /** Validate notification and detail metadata before Slack can ignore bad fields. */
2802
- const slackEntitySchema = z.discriminatedUnion("entity_type", [entity.extend({
2803
- entity_type: z.literal("slack#/entities/item"),
2804
- entity_payload: payload.extend({ fields: z.never().optional() })
2805
- }), entity.extend({
2806
- entity_type: z.literal("slack#/entities/task"),
2807
- entity_payload: payload.extend({ fields: z.strictObject({ status: z.strictObject({ value: z.string() }).optional() }).optional() })
2808
- })]);
2809
- //#endregion
2810
- //#region src/chat/slack/outbound.ts
2811
- const MAX_SLACK_MESSAGE_TEXT_CHARS = 4e4;
2812
- function requireSlackConversationId(channelId, action) {
2813
- const normalized = normalizeSlackConversationId(channelId);
2814
- if (!normalized) throw new Error(`${action} requires a valid channel ID`);
2815
- return normalized;
2816
- }
2817
- function requireSlackThreadTimestamp(threadTs, action) {
2818
- const normalized = threadTs.trim();
2819
- if (!normalized) throw new Error(`${action} requires a thread timestamp`);
2820
- return normalized;
2821
- }
2822
- function requireSlackMessageTimestamp(timestamp, action) {
2823
- const normalized = parseSlackMessageTs(timestamp);
2824
- if (!normalized) throw new Error(`${action} requires a target message timestamp`);
2825
- return normalized;
2826
- }
2827
- function requireSlackMessageText(text, action) {
2828
- if (text.trim().length === 0) throw new Error(`${action} requires non-empty text`);
2829
- if (text.length > MAX_SLACK_MESSAGE_TEXT_CHARS) throw new Error(`${action} text exceeds Slack's 40000 character truncation limit`);
2830
- return text;
2831
- }
2832
- /** Resolve a Slack message permalink without making read-only workflows fail on lookup errors. */
2833
- async function getSlackMessagePermalink(args) {
2834
- try {
2835
- return (await withSlackRetries(() => getSlackClient().chat.getPermalink({
2836
- channel: args.channelId,
2837
- message_ts: args.messageTs
2838
- }), 3, {
2839
- action: "chat.getPermalink",
2840
- idempotent: true,
2841
- spanAttributes: {
2842
- "app.slack.channel_id": args.channelId,
2843
- "app.slack.message_ts": args.messageTs
2844
- }
2845
- })).permalink;
2846
- } catch {
2847
- return;
2848
- }
2849
- }
2850
- /** Post Slack `mrkdwn` text to a conversation or thread via the shared outbound boundary. */
2851
- async function postSlackMessage(input) {
2852
- const channelId = requireSlackConversationId(input.channelId, "Slack message posting");
2853
- const text = requireSlackMessageText(input.text, "Slack message posting");
2854
- const entities = input.entities?.length ? slackEntitySchema.array().parse(input.entities) : void 0;
2855
- const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack thread message posting") : void 0;
2856
- const attributes = {
2857
- "app.slack.channel_id": channelId,
2858
- ...threadTs ? { "app.slack.thread_ts": threadTs } : void 0,
2859
- "app.slack.work_object.count": entities?.length ?? 0
2860
- };
2861
- const response = await withSlackRetries(async () => {
2862
- const response = await getSlackClient().chat.postMessage({
2863
- channel: channelId,
2864
- text,
2865
- unfurl_links: false,
2866
- unfurl_media: false,
2867
- ...input.blocks?.length ? { blocks: input.blocks } : void 0,
2868
- ...entities ? { metadata: { entities } } : void 0,
2869
- ...threadTs ? { thread_ts: threadTs } : void 0
2870
- });
2871
- const messageId = parseSlackMessageTs(response.ts);
2872
- setSpanAttributes({ "messaging.message.id": messageId });
2873
- captureSlackPostWarning(response, {
2874
- ...attributes,
2875
- "messaging.message.id": messageId
2876
- });
2877
- return response;
2878
- }, 3, {
2879
- action: "chat.postMessage",
2880
- attributes
2881
- });
2882
- const messageTs = parseSlackMessageTs(response.ts);
2883
- if (!messageTs) throw new Error("Slack message posted without ts");
2884
- return {
2885
- ts: messageTs,
2886
- ...input.includePermalink ? { permalink: await getSlackMessagePermalink({
2887
- channelId,
2888
- messageTs
2889
- }) } : void 0
2890
- };
2891
- }
2892
- /**
2893
- * Post an ephemeral Slack message. Delivery is best-effort on Slack's side, but
2894
- * request validation and Web API behavior are centralized here.
2895
- */
2896
- async function postSlackEphemeralMessage(input) {
2897
- const channelId = requireSlackConversationId(input.channelId, "Slack ephemeral message posting");
2898
- const userId = parseSlackUserId(input.userId);
2899
- if (!userId) throw new Error("Slack ephemeral message posting requires a user ID");
2900
- const text = requireSlackMessageText(input.text, "Slack ephemeral message posting");
2901
- const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack ephemeral thread message posting") : void 0;
2902
- return { messageTs: (await withSlackRetries(() => getSlackClient().chat.postEphemeral({
2903
- channel: channelId,
2904
- user: userId,
2905
- text,
2906
- ...input.blocks?.length ? { blocks: input.blocks } : void 0,
2907
- ...threadTs ? { thread_ts: threadTs } : void 0
2908
- }), 3, {
2909
- action: "chat.postEphemeral",
2910
- spanAttributes: {
2911
- "app.slack.channel_id": channelId,
2912
- "app.slack.user_id": userId,
2913
- ...threadTs ? { "app.slack.thread_ts": threadTs } : void 0
2914
- }
2915
- })).message_ts };
2916
- }
2917
- /** Upload files into a Slack conversation or thread via the shared outbound file boundary. */
2918
- async function uploadFilesToConversation(input) {
2919
- const channelId = requireSlackConversationId(input.channelId, "Slack file upload");
2920
- const threadTs = input.threadTs ? requireSlackThreadTimestamp(input.threadTs, "Slack file upload") : void 0;
2921
- if (input.files.length === 0) throw new Error("Slack file upload requires at least one file");
2922
- const fileUploads = input.files.map((file) => {
2923
- const filename = file.filename.trim();
2924
- if (!filename) throw new Error("Slack file upload requires every file to have a filename");
2925
- return {
2926
- file: file.data,
2927
- filename
2928
- };
2929
- });
2930
- return { files: ((await withSlackRetries(() => getSlackClient().filesUploadV2({
2931
- channel_id: channelId,
2932
- ...threadTs ? { thread_ts: threadTs } : void 0,
2933
- file_uploads: fileUploads
2934
- }), 3, {
2935
- action: "filesUploadV2",
2936
- spanAttributes: {
2937
- "app.slack.channel_id": channelId,
2938
- ...threadTs ? { "app.slack.thread_ts": threadTs } : void 0
2939
- }
2940
- })).files ?? []).flatMap((completion) => (completion.files ?? []).map((file) => file.id ? { id: file.id } : {})) };
2941
- }
2942
- /** Add a reaction to a Slack message, treating `already_reacted` as idempotent success. */
2943
- async function addReactionToMessage(input) {
2944
- const channelId = requireSlackConversationId(input.channelId, "Slack reaction");
2945
- const timestamp = requireSlackMessageTimestamp(input.timestamp, "Slack reaction");
2946
- const emoji = normalizeSlackEmojiName(input.emoji);
2947
- if (!emoji) throw new Error("Slack reaction requires a valid emoji alias name");
2948
- try {
2949
- await withSlackRetries(() => getSlackClient(input.timeoutMs === void 0 ? void 0 : { timeoutMs: input.timeoutMs }).reactions.add({
2950
- channel: channelId,
2951
- timestamp,
2952
- name: emoji
2953
- }), input.timeoutMs === void 0 ? 3 : 1, {
2954
- action: "reactions.add",
2955
- idempotent: true,
2956
- spanAttributes: {
2957
- "app.slack.channel_id": channelId,
2958
- "app.slack.message_ts": timestamp,
2959
- "app.slack.reaction": emoji
2960
- }
2961
- });
2962
- } catch (error) {
2963
- if (error instanceof SlackActionError && error.code === "already_reacted") return { ok: true };
2964
- throw error;
2965
- }
2966
- return { ok: true };
2967
- }
2968
- /** Remove a reaction from a Slack message, treating `no_reaction` as idempotent success. */
2969
- async function removeReactionFromMessage(input) {
2970
- const channelId = requireSlackConversationId(input.channelId, "Slack reaction removal");
2971
- const timestamp = requireSlackMessageTimestamp(input.timestamp, "Slack reaction removal");
2972
- const emoji = normalizeSlackEmojiName(input.emoji);
2973
- if (!emoji) throw new Error("Slack reaction removal requires a valid emoji alias name");
2974
- try {
2975
- await withSlackRetries(() => getSlackClient().reactions.remove({
2976
- channel: channelId,
2977
- timestamp,
2978
- name: emoji
2979
- }), 3, {
2980
- action: "reactions.remove",
2981
- idempotent: true,
2982
- spanAttributes: {
2983
- "app.slack.channel_id": channelId,
2984
- "app.slack.message_ts": timestamp,
2985
- "app.slack.reaction": emoji
2986
- }
2987
- });
2988
- } catch (error) {
2989
- if (error instanceof SlackActionError && error.code === "no_reaction") return { ok: true };
2990
- throw error;
2991
- }
2992
- return { ok: true };
2993
- }
2994
- //#endregion
2995
- //#region src/chat/slack/status-format.ts
2996
- const SLACK_STATUS_MAX_LENGTH = 50;
2997
- /** Truncate a status string to fit Slack's 50-char assistant status limit. */
2998
- function truncateStatusText(text) {
2999
- const trimmed = text.trim();
3000
- if (!trimmed) return "";
3001
- if (trimmed.length <= SLACK_STATUS_MAX_LENGTH) return trimmed;
3002
- return `${trimmed.slice(0, 47).trimEnd()}...`;
3003
- }
3004
- //#endregion
3005
- //#region src/chat/slack/mrkdwn.ts
3006
- /** Escape dynamic text for Slack mrkdwn without changing intended formatting. */
3007
- function escapeSlackMrkdwnText(text) {
3008
- return text.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3009
- }
3010
- /** Escape a URL for Slack explicit link syntax while preserving query semantics. */
3011
- function escapeSlackLinkUrl(url) {
3012
- return url.replaceAll("&", "&amp;").replaceAll("<", "%3C").replaceAll(">", "%3E");
3013
- }
3014
- /** Build a Slack explicit link from dynamic URL and label values. */
3015
- function formatSlackLink(url, label) {
3016
- return `<${escapeSlackLinkUrl(url)}|${escapeSlackMrkdwnText(label)}>`;
3017
- }
3018
- function readInlineCodeSpan(line, start) {
3019
- if (line[start] !== "`") return;
3020
- let n = 1;
3021
- while (line[start + n] === "`") n++;
3022
- const marker = "`".repeat(n);
3023
- let search = start + n;
3024
- while (search < line.length) {
3025
- const close = line.indexOf(marker, search);
3026
- if (close === -1) return;
3027
- const after = close + n;
3028
- if (line[after] !== "`") return {
3029
- text: line.slice(start, after),
3030
- end: after
3031
- };
3032
- search = after + 1;
3033
- }
3034
- }
3035
- function readExistingSlackAngleToken(line, start) {
3036
- if (line[start] !== "<") return;
3037
- const close = line.indexOf(">", start + 1);
3038
- if (close === -1) return;
3039
- const body = line.slice(start + 1, close);
3040
- if (/^(?:https?:\/\/|@|#|!)/.test(body)) return {
3041
- text: line.slice(start, close + 1),
3042
- end: close + 1
3043
- };
3044
- }
3045
- function readMarkdownLink(line, start) {
3046
- if (line[start] !== "[") return;
3047
- const labelEnd = line.indexOf("](", start + 1);
3048
- if (labelEnd === -1) return;
3049
- const destStart = labelEnd + 2;
3050
- if (!line.startsWith("http://", destStart) && !line.startsWith("https://", destStart)) return;
3051
- const closeParens = line.indexOf(")", destStart);
3052
- if (closeParens === -1) return;
3053
- return {
3054
- text: line.slice(start, closeParens + 1),
3055
- end: closeParens + 1
3056
- };
3057
- }
3058
- function hasUnmatchedClosingParen(text) {
3059
- let balance = 0;
3060
- for (const ch of text) if (ch === "(") balance++;
3061
- else if (ch === ")") balance--;
3062
- return balance < 0;
3063
- }
3064
- function readBareUrl(line, start) {
3065
- let end = start;
3066
- while (end < line.length) {
3067
- const ch = line[end];
3068
- if (/\s/.test(ch) || ch === "<" || ch === ">" || ch === "\"" || ch === "`" || ch === "|" || ch === "*") break;
3069
- end++;
3070
- }
3071
- if (end === start) return;
3072
- let raw = line.slice(start, end);
3073
- let suffix = "";
3074
- const peel = () => {
3075
- suffix = raw.slice(-1) + suffix;
3076
- raw = raw.slice(0, -1);
3077
- };
3078
- const shouldPeel = () => raw.endsWith("_") || /[.,!?;:]$/.test(raw) || raw.endsWith(")") && hasUnmatchedClosingParen(raw);
3079
- while (raw.length > 0 && shouldPeel()) peel();
3080
- if (!/^https?:\/\/.+/.test(raw)) return;
3081
- return {
3082
- url: raw,
3083
- suffix,
3084
- end
3085
- };
3086
- }
3087
- /** Wrap bare http(s) URLs on a single line as Slack explicit `<url>` links. */
3088
- function wrapBareUrlsOnLine(line) {
3089
- let result = "";
3090
- let i = 0;
3091
- while (i < line.length) {
3092
- const codeSpan = readInlineCodeSpan(line, i);
3093
- if (codeSpan) {
3094
- result += codeSpan.text;
3095
- i = codeSpan.end;
3096
- continue;
3097
- }
3098
- const angleToken = readExistingSlackAngleToken(line, i);
3099
- if (angleToken) {
3100
- result += angleToken.text;
3101
- i = angleToken.end;
3102
- continue;
3103
- }
3104
- const mdLink = readMarkdownLink(line, i);
3105
- if (mdLink) {
3106
- result += mdLink.text;
3107
- i = mdLink.end;
3108
- continue;
3109
- }
3110
- if (line.startsWith("https://", i) || line.startsWith("http://", i)) {
3111
- const parsed = readBareUrl(line, i);
3112
- if (parsed) {
3113
- result += `<${parsed.url}>${parsed.suffix}`;
3114
- i = parsed.end;
3115
- continue;
3116
- }
3117
- }
3118
- result += line[i];
3119
- i++;
3120
- }
3121
- return result;
3122
- }
3123
- /**
3124
- * Pre-wrap bare http(s) URLs outside fenced code blocks as Slack explicit
3125
- * links, preventing Slack's auto-linker from consuming adjacent formatting
3126
- * markers into the URL.
3127
- *
3128
- * Uses the same fence-toggle rule as `ensureBlockSpacing` so both passes
3129
- * agree on which lines are code.
3130
- */
3131
- function wrapBareUrls(text) {
3132
- const lines = text.split("\n");
3133
- const out = [];
3134
- let inCodeBlock = false;
3135
- for (const line of lines) {
3136
- if (line.trimStart().startsWith("```")) {
3137
- inCodeBlock = !inCodeBlock;
3138
- out.push(line);
3139
- continue;
3140
- }
3141
- out.push(inCodeBlock ? line : wrapBareUrlsOnLine(line));
3142
- }
3143
- return out.join("\n");
3144
- }
3145
- /** Insert blank lines between content blocks so Slack renders them with visual separation. */
3146
- function ensureBlockSpacing(text) {
3147
- const codeBlockPattern = /^```/;
3148
- const listItemPattern = /^[-*•]\s|^\d+\.\s/;
3149
- const lines = text.split("\n");
3150
- const result = [];
3151
- let inCodeBlock = false;
3152
- for (let i = 0; i < lines.length; i++) {
3153
- const line = lines[i];
3154
- if (codeBlockPattern.test(line.trimStart())) {
3155
- if (!inCodeBlock) {
3156
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
3157
- if (prev !== void 0 && prev.trim() !== "") result.push("");
3158
- }
3159
- inCodeBlock = !inCodeBlock;
3160
- result.push(line);
3161
- continue;
3162
- }
3163
- if (inCodeBlock) {
3164
- result.push(line);
3165
- continue;
3166
- }
3167
- const prev = result.length > 0 ? result[result.length - 1] : void 0;
3168
- if (prev !== void 0 && prev.trim() !== "" && line.trim() !== "" && !(listItemPattern.test(prev.trimStart()) && listItemPattern.test(line.trimStart()))) result.push("");
3169
- result.push(line);
3170
- }
3171
- return result.join("\n");
3172
- }
3173
- /**
3174
- * Normalize model-authored Slack markdown for delivery via `markdown_text`
3175
- * or `{ type: "markdown" }` blocks.
3176
- *
3177
- * Plugins may first rewrite ordinary Markdown. Core then applies Slack-only
3178
- * delivery shaping: bare URL wrapping and block spacing. Slack reply delivery
3179
- * owns chunking and continuation markers separately.
3180
- */
3181
- function normalizeSlackReplyMarkdown(text) {
3182
- let normalized = applyPluginFormatMarkdown(text).replace(/\r\n?/g, "\n").replace(/[ \t]+$/gm, "");
3183
- normalized = wrapBareUrls(normalized);
3184
- normalized = ensureBlockSpacing(normalized);
3185
- return normalized.replace(/\n{3,}/g, "\n\n").trim();
3186
- }
3187
- /** Normalize assistant status text before handing it to Slack. */
3188
- function normalizeSlackStatusText(text) {
3189
- const trimmed = text.trim();
3190
- if (!trimmed) return "";
3191
- return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim());
3192
- }
3193
- /** Render a Slack user mention, which Slack shows as the person's name. */
3194
- function slackMention(userId) {
3195
- return `<@${userId}>`;
3196
- }
3197
- //#endregion
3198
2823
  //#region src/chat/slack/oauth-authorization-message.ts
3199
2824
  const ACCOUNT_SCOPE_NOTE = "This connects only your account. Junior sees only what you can see.";
3200
2825
  /** Present private OAuth authorization with a clear action and text fallback. */
@@ -4063,10 +3688,13 @@ async function maybePostAutomatedTurnLimitNotice(args) {
4063
3688
  }
4064
3689
  //#endregion
4065
3690
  //#region src/chat/task-input.ts
4066
- /** Shared closing lines for the stored outcomes. */
3691
+ /**
3692
+ * Shared closing lines. Automations pass outcomes and end with a declared
3693
+ * result. Watches pass no outcomes and reply in the Conversation.
3694
+ */
4067
3695
  function replyContractLines(outcomes) {
4068
- if (outcomes?.length === 0) return ["Do the work without writing a status message.", "No successful output will be delivered."];
4069
- return ["When you reply, follow any reply format in the instructions.", "Briefly report what you did or what is needed next."];
3696
+ if (!outcomes) return ["When you reply, follow any reply format in the instructions.", "Briefly report what you did or what is needed next."];
3697
+ return [`End with \`${FINISH_AUTOMATION_RUN_TOOL_NAME}\`.`];
4070
3698
  }
4071
3699
  function oneLine(value) {
4072
3700
  return value.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim();
@@ -4091,7 +3719,7 @@ function renderTaskInput(args) {
4091
3719
  "This is a task, not a message from a person.",
4092
3720
  "",
4093
3721
  ...about ? [`About: ${oneLine(about)}`] : [],
4094
- ...creator ? [`Created by: ${oneLine(creator)}. Where the instructions say "me" or "my", write this mention.`] : [],
3722
+ ...creator ? [`Created by: ${oneLine(creator)}. "Me" and "my" in the instructions mean this person: mention them in the message. If the instructions do not say "me" or "my", do not mention this person.`] : [],
4095
3723
  `Instructions: ${instructions}`
4096
3724
  ];
4097
3725
  const guidance = args.guidance?.trim();
@@ -10088,6 +9716,44 @@ function createSpawnAgentTool(spawnAgent) {
10088
9716
  });
10089
9717
  }
10090
9718
  //#endregion
9719
+ //#region src/chat/tools/runtime/finish-automation-run.ts
9720
+ const RESULT_GUIDANCE = ["Use `no_action` when the run worked and nothing should be posted, such as when a condition in the instruction is not met, or when a temporary failure stopped the work.", "Use `misconfigured` when the run cannot do its job until its creator changes the automation: a provider, tool, account, or access that the job needs is missing, a target no longer exists, or the instruction cannot be done. Do not use it for a temporary failure or a condition that is not met."].join(" ");
9721
+ /** Create the tool that ends one Automation run with a declared result. */
9722
+ function createFinishAutomationRunTool(options) {
9723
+ const results = options.sendsMessage ? [
9724
+ "send_message",
9725
+ "no_action",
9726
+ "misconfigured"
9727
+ ] : ["no_action", "misconfigured"];
9728
+ return zodTool({
9729
+ annotations: {
9730
+ destructiveHint: false,
9731
+ idempotentHint: true,
9732
+ openWorldHint: false,
9733
+ readOnlyHint: false
9734
+ },
9735
+ description: options.sendsMessage ? `End this automation run. Use \`send_message\` with the finished reminder, digest, alert, or answer; the runtime posts it to the stored destinations. ${RESULT_GUIDANCE}` : `End this automation run. This automation posts nothing. ${RESULT_GUIDANCE}`,
9736
+ inputSchema: z.object({
9737
+ result: z.enum(results),
9738
+ ...options.sendsMessage ? { message: z.string().optional().describe("Required for send_message. The text to post.") } : void 0,
9739
+ reason: z.string().optional().describe("Required for no_action and misconfigured. One sentence for the creator.")
9740
+ }),
9741
+ outputSchema: juniorToolOutputSchema.extend({ result: z.enum([
9742
+ "send_message",
9743
+ "no_action",
9744
+ "misconfigured"
9745
+ ]) }),
9746
+ execute: async (input) => {
9747
+ const parsed = automationResultSchema.safeParse(input);
9748
+ if (!parsed.success) throw new ToolInputError(input.result === "send_message" ? "send_message requires a non-empty message." : `${input.result} requires a non-empty reason.`);
9749
+ return parsed.data.result === "send_message" && isNoReplyMarker(parsed.data.message) ? {
9750
+ result: "no_action",
9751
+ reason: "The message was the no-reply marker."
9752
+ } : parsed.data;
9753
+ }
9754
+ });
9755
+ }
9756
+ //#endregion
10091
9757
  //#region src/chat/tools/list-watches.ts
10092
9758
  const listedResourceWatchSchema = z.object({
10093
9759
  id: z.string().min(1),
@@ -10432,305 +10098,6 @@ function requireRequesterMention(instruction, actor) {
10432
10098
  if (namesPerson(instruction, actor.fullName)) throw new ToolInputError(`The instruction names ${actor.fullName} without a Slack mention. Write ${mention} for this person.`);
10433
10099
  }
10434
10100
  //#endregion
10435
- //#region src/chat/automations/revision.ts
10436
- /** Storage revisions prevent stale edits from replacing newer values. */
10437
- var AutomationConflictError = class extends ToolInputError {
10438
- constructor() {
10439
- super("This Automation changed after it was loaded. Reload it before saving.");
10440
- this.name = "AutomationConflictError";
10441
- }
10442
- };
10443
- /** Sort object keys so equal values produce equal JSON. */
10444
- function canonicalJson$1(value) {
10445
- if (Array.isArray(value)) return value.map(canonicalJson$1);
10446
- if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, child]) => [key, canonicalJson$1(child)]));
10447
- return value;
10448
- }
10449
- /** Identify the stored values being edited, including lifecycle and creator authority. */
10450
- function automationRevision(task) {
10451
- return createHash("sha256").update(JSON.stringify(canonicalJson$1(task))).digest("hex");
10452
- }
10453
- /** Reject a stale edit while the storage owner holds the write lock. */
10454
- function requireAutomationRevision(task, revision) {
10455
- if (!task || automationRevision(task) !== revision) throw new AutomationConflictError();
10456
- }
10457
- //#endregion
10458
- //#region src/chat/automations/versions.ts
10459
- /** Automation versions keep each saved definition so people can see how it changed. */
10460
- /** Return the versioned part of an Automation. Lifecycle and run state are excluded. */
10461
- function automationDefinition(task) {
10462
- const common = {
10463
- title: task.title?.trim() || null,
10464
- instruction: task.task.text,
10465
- credentialMode: task.credentialMode,
10466
- destination: task.destination,
10467
- outcomes: task.outcomes
10468
- };
10469
- return "schedule" in task ? {
10470
- ...common,
10471
- schedule: task.schedule
10472
- } : {
10473
- ...common,
10474
- trigger: task.trigger
10475
- };
10476
- }
10477
- /** Compare definition values after key sorting. */
10478
- function sameDefinitionValue(left, right) {
10479
- return JSON.stringify(canonicalJson$1(left)) === JSON.stringify(canonicalJson$1(right));
10480
- }
10481
- /**
10482
- * Save a version when the definition changed. Call this in the same
10483
- * transaction and lock as the Automation write. Without `current`, the
10484
- * Automation is new, so the creator saves version 1.
10485
- */
10486
- async function recordAutomationVersion(db, kind, task, current, editedBy) {
10487
- const definition = automationDefinition(task);
10488
- if (current && sameDefinitionValue(automationDefinition(current), definition)) return;
10489
- await db.insert(juniorAutomationVersions).values({
10490
- kind,
10491
- automationId: task.id,
10492
- version: sql`(
10493
- SELECT coalesce(max(${juniorAutomationVersions.version}), 0) + 1
10494
- FROM ${juniorAutomationVersions}
10495
- WHERE ${juniorAutomationVersions.kind} = ${kind}
10496
- AND ${juniorAutomationVersions.automationId} = ${task.id}
10497
- )`,
10498
- createdAtMs: Date.now(),
10499
- editedBy: editedBy ?? (current ? null : task.createdBy),
10500
- definition
10501
- });
10502
- }
10503
- function parseVersionRow(kind, row) {
10504
- return automationVersionSchema.parse({
10505
- kind,
10506
- version: row.version,
10507
- createdAt: new Date(row.createdAtMs).toISOString(),
10508
- editedBy: row.editedBy,
10509
- definition: row.definition
10510
- });
10511
- }
10512
- /** Read one saved definition. */
10513
- async function readAutomationVersion(db, kind, automationId, version) {
10514
- const [row] = await db.select().from(juniorAutomationVersions).where(and(eq(juniorAutomationVersions.kind, kind), eq(juniorAutomationVersions.automationId, automationId), eq(juniorAutomationVersions.version, version))).limit(1);
10515
- return row ? parseVersionRow(kind, row) : void 0;
10516
- }
10517
- /** Read saved definitions, newest first. */
10518
- async function listAutomationVersions(db, kind, automationId, limit) {
10519
- 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));
10520
- }
10521
- //#endregion
10522
- //#region src/chat/event-automations/store.ts
10523
- const retainedEventAutomationSchema = eventAutomationSchema.extend({
10524
- destination: slackDestinationSchema,
10525
- outcomes: z.array(taskOutcomeSchema).max(5)
10526
- });
10527
- /** JSON task payload must not carry SQL-backed columns. */
10528
- function eventAutomationJsonPayload(task) {
10529
- const { status: _status, title: _title, ...payload } = task;
10530
- return payload;
10531
- }
10532
- /** Decode a retained event Automation with its SQL-backed fields. */
10533
- function parseEventAutomationRow(row) {
10534
- const raw = row.task && typeof row.task === "object" ? { ...row.task } : row.task;
10535
- if (raw && typeof raw === "object" && "title" in raw) delete raw.title;
10536
- if (raw && typeof raw === "object" && !("outcomes" in raw) && "destination" in raw) raw.outcomes = [{
10537
- action: "send_message",
10538
- destination: raw.destination
10539
- }];
10540
- const retained = retainedEventAutomationSchema.parse(raw);
10541
- const { threadTs: _threadTs, ...destination } = retained.destination;
10542
- const title = row.title?.trim();
10543
- return {
10544
- ...retained,
10545
- destination,
10546
- outcomes: retained.outcomes.map((outcome) => {
10547
- const { threadTs: _threadTs, ...destination } = outcome.destination;
10548
- return {
10549
- ...outcome,
10550
- destination
10551
- };
10552
- }),
10553
- status: row.status ?? "active",
10554
- ...title ? { title } : void 0
10555
- };
10556
- }
10557
- function activeEventAutomationWhere() {
10558
- return ne(juniorEventAutomations.status, "deleted");
10559
- }
10560
- /** Read one event automation by id, including deleted rows when present. */
10561
- async function getEventAutomation(db, id) {
10562
- const rows = await db.select({
10563
- status: juniorEventAutomations.status,
10564
- task: juniorEventAutomations.task,
10565
- title: juniorEventAutomations.title
10566
- }).from(juniorEventAutomations).where(eq(juniorEventAutomations.id, id)).limit(1);
10567
- return rows[0] ? parseEventAutomationRow(rows[0]) : void 0;
10568
- }
10569
- /** Create one retry-stable event automation, or revive a deleted row with the new payload. */
10570
- async function createEventAutomation(db, task) {
10571
- const parsed = eventAutomationSchema.parse(eventAutomationJsonPayload(task));
10572
- const title = task.title?.trim() || null;
10573
- return db.transaction(async (tx) => {
10574
- const inserted = await tx.insert(juniorEventAutomations).values({
10575
- id: parsed.id,
10576
- teamId: parsed.destination.teamId,
10577
- namespace: parsed.trigger.namespace,
10578
- identifier: parsed.trigger.identifier,
10579
- createdAtMs: parsed.createdAtMs,
10580
- status: "active",
10581
- title,
10582
- task: parsed
10583
- }).onConflictDoNothing().returning({
10584
- status: juniorEventAutomations.status,
10585
- task: juniorEventAutomations.task,
10586
- title: juniorEventAutomations.title
10587
- });
10588
- if (inserted[0]) {
10589
- const created = parseEventAutomationRow(inserted[0]);
10590
- await recordAutomationVersion(tx, "event", created, void 0);
10591
- return created;
10592
- }
10593
- const existing = await getEventAutomation(tx, parsed.id);
10594
- if (!existing) return {
10595
- ...parsed,
10596
- status: "active",
10597
- ...title ? { title } : void 0
10598
- };
10599
- if (existing.status !== "deleted") return existing;
10600
- const rows = await tx.update(juniorEventAutomations).set({
10601
- teamId: parsed.destination.teamId,
10602
- namespace: parsed.trigger.namespace,
10603
- identifier: parsed.trigger.identifier,
10604
- status: "active",
10605
- title,
10606
- task: parsed
10607
- }).where(and(eq(juniorEventAutomations.id, parsed.id), eq(juniorEventAutomations.status, "deleted"))).returning({
10608
- status: juniorEventAutomations.status,
10609
- task: juniorEventAutomations.task,
10610
- title: juniorEventAutomations.title
10611
- });
10612
- if (!rows[0]) return await getEventAutomation(tx, parsed.id) ?? {
10613
- ...parsed,
10614
- status: "active",
10615
- ...title ? { title } : void 0
10616
- };
10617
- const revived = parseEventAutomationRow(rows[0]);
10618
- await recordAutomationVersion(tx, "event", revived, void 0);
10619
- return revived;
10620
- });
10621
- }
10622
- /**
10623
- * Replace an existing non-deleted event automation under a row lock.
10624
- * Pass `editedBy` when a person changes the definition so its version names them.
10625
- */
10626
- async function saveEventAutomation(db, task, expectedRevision, editedBy) {
10627
- const parsed = eventAutomationSchema.parse(eventAutomationJsonPayload(task));
10628
- const title = task.title?.trim() || null;
10629
- return db.transaction(async (tx) => {
10630
- const rows = await tx.select({
10631
- status: juniorEventAutomations.status,
10632
- task: juniorEventAutomations.task,
10633
- title: juniorEventAutomations.title
10634
- }).from(juniorEventAutomations).where(eq(juniorEventAutomations.id, parsed.id)).for("update");
10635
- const current = rows[0] ? parseEventAutomationRow(rows[0]) : void 0;
10636
- if (expectedRevision !== void 0) requireAutomationRevision(current, expectedRevision);
10637
- if (!current || current.status === "deleted") return void 0;
10638
- const saved = parseEventAutomationRow((await tx.update(juniorEventAutomations).set({
10639
- namespace: parsed.trigger.namespace,
10640
- identifier: parsed.trigger.identifier,
10641
- title,
10642
- task: parsed
10643
- }).where(eq(juniorEventAutomations.id, parsed.id)).returning({
10644
- status: juniorEventAutomations.status,
10645
- task: juniorEventAutomations.task,
10646
- title: juniorEventAutomations.title
10647
- }))[0]);
10648
- await recordAutomationVersion(tx, "event", saved, current, editedBy);
10649
- return saved;
10650
- });
10651
- }
10652
- /** Mark one existing event automation deleted while retaining the row for history. */
10653
- async function deleteEventAutomation(db, id) {
10654
- const rows = await db.update(juniorEventAutomations).set({ status: "deleted" }).where(and(eq(juniorEventAutomations.id, id), activeEventAutomationWhere())).returning({
10655
- status: juniorEventAutomations.status,
10656
- task: juniorEventAutomations.task,
10657
- title: juniorEventAutomations.title
10658
- });
10659
- return rows[0] ? parseEventAutomationRow(rows[0]) : void 0;
10660
- }
10661
- /** List live event automations in one Slack workspace. */
10662
- async function listEventAutomationsForTeam(db, teamId) {
10663
- return (await db.select({
10664
- status: juniorEventAutomations.status,
10665
- task: juniorEventAutomations.task,
10666
- title: juniorEventAutomations.title
10667
- }).from(juniorEventAutomations).where(and(eq(juniorEventAutomations.teamId, teamId), activeEventAutomationWhere())).orderBy(asc(juniorEventAutomations.createdAtMs), asc(juniorEventAutomations.id))).map(parseEventAutomationRow);
10668
- }
10669
- function viewerSlackIdentities(user) {
10670
- return user.identities.filter((identity) => identity.provider === "slack" && Boolean(identity.providerTenantId) && Boolean(identity.providerSubjectId));
10671
- }
10672
- /** Return whether one event automation was created by a viewer-linked Slack identity. */
10673
- function eventAutomationBelongsToUser(task, user) {
10674
- return viewerSlackIdentities(user).some((identity) => identity.providerTenantId === task.destination.teamId && identity.providerSubjectId === task.createdBy.slackUserId);
10675
- }
10676
- /**
10677
- * List deleted event automations created by one user, newest first.
10678
- * Used only to keep historical runs after the live automation list drops the row.
10679
- */
10680
- async function listDeletedEventAutomationsCreatedBy(db, user, limit) {
10681
- const identities = viewerSlackIdentities(user);
10682
- const ownership = or(...identities.map((identity) => and(eq(juniorEventAutomations.teamId, identity.providerTenantId), sql`${juniorEventAutomations.task}->'createdBy'->>'slackUserId' = ${identity.providerSubjectId}`)));
10683
- if (!ownership) return [];
10684
- return (await db.select({
10685
- status: juniorEventAutomations.status,
10686
- task: juniorEventAutomations.task,
10687
- title: juniorEventAutomations.title
10688
- }).from(juniorEventAutomations).where(and(ownership, eq(juniorEventAutomations.status, "deleted"))).orderBy(desc(juniorEventAutomations.createdAtMs), desc(juniorEventAutomations.id)).limit(limit)).map(parseEventAutomationRow);
10689
- }
10690
- /**
10691
- * Collect match keys from live event automations for these identifiers and event types.
10692
- * Does not evaluate match values — only reports keys that filters use.
10693
- */
10694
- async function collectEventAutomationMatchKeys(db, input) {
10695
- const eventTypes = new Set(input.eventTypes.map((eventType) => eventType.trim()).filter(Boolean));
10696
- const identifiers = [...new Set(input.identifiers.map((value) => value.trim()).filter(Boolean))];
10697
- if (eventTypes.size === 0 || identifiers.length === 0) return [];
10698
- const rows = await db.select({
10699
- status: juniorEventAutomations.status,
10700
- task: juniorEventAutomations.task,
10701
- title: juniorEventAutomations.title
10702
- }).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));
10703
- const keys = /* @__PURE__ */ new Set();
10704
- for (const row of rows) {
10705
- const task = parseEventAutomationRow(row);
10706
- if (!task.trigger.events.some((eventType) => eventTypes.has(eventType))) continue;
10707
- for (const key of Object.keys(task.trigger.match ?? {})) keys.add(key);
10708
- }
10709
- return [...keys].sort();
10710
- }
10711
- /** Find every live task matching one normalized event. */
10712
- async function findMatchingEventAutomations(db, event, teamId) {
10713
- return (await db.select({
10714
- status: juniorEventAutomations.status,
10715
- task: juniorEventAutomations.task,
10716
- title: juniorEventAutomations.title
10717
- }).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));
10718
- }
10719
- /** Change lifecycle under the same row lock as edits, without changing credentials. */
10720
- async function setEventAutomationStatus(db, id, status, revision) {
10721
- return db.transaction(async (tx) => {
10722
- const rows = await tx.select().from(juniorEventAutomations).where(eq(juniorEventAutomations.id, id)).for("update");
10723
- const current = rows[0] ? parseEventAutomationRow(rows[0]) : void 0;
10724
- requireAutomationRevision(current, revision);
10725
- if (!current || current.status === "deleted") throw new Error("Automation no longer exists.");
10726
- await tx.update(juniorEventAutomations).set({ status }).where(eq(juniorEventAutomations.id, id));
10727
- return {
10728
- ...current,
10729
- status
10730
- };
10731
- });
10732
- }
10733
- //#endregion
10734
10101
  //#region src/chat/task-outcomes.ts
10735
10102
  /** Resolve explicit message outcomes to the Slack Destinations stored on an Automation. */
10736
10103
  async function resolveTaskOutcomes(outcomes, currentDestination, creatorSlackUserId) {
@@ -10744,9 +10111,7 @@ async function resolveTaskOutcomes(outcomes, currentDestination, creatorSlackUse
10744
10111
  });
10745
10112
  continue;
10746
10113
  }
10747
- const userId = creatorSlackUserId;
10748
- const channelId = (await withSlackRetries(() => getSlackClient().conversations.open({ users: userId }), 3, { action: "conversations.open" })).channel?.id;
10749
- if (!channelId) throw new Error("Slack did not return a direct message destination.");
10114
+ const channelId = await openSlackDirectMessage(creatorSlackUserId);
10750
10115
  resolved.push({
10751
10116
  action: "send_message",
10752
10117
  destination: {
@@ -10781,9 +10146,11 @@ const compactEventAutomationResultSchema = z.object({
10781
10146
  id: z.string().min(1),
10782
10147
  status: z.enum([
10783
10148
  "active",
10149
+ "blocked",
10784
10150
  "paused",
10785
10151
  "deleted"
10786
10152
  ]),
10153
+ statusReason: z.string().nullable(),
10787
10154
  title: z.string().min(1).nullable(),
10788
10155
  dashboardUrl: z.string().url().nullable(),
10789
10156
  instruction: z.string().min(1),
@@ -10884,6 +10251,7 @@ function compactEventAutomation(task, catalog, requesterSlackUserId) {
10884
10251
  return compactEventAutomationResultSchema.parse({
10885
10252
  id: task.id,
10886
10253
  status: task.status,
10254
+ statusReason: task.statusReason ?? null,
10887
10255
  title: task.title?.trim() || null,
10888
10256
  dashboardUrl: getDashboardTaskLink(task.id) ?? null,
10889
10257
  instruction: task.task.text,
@@ -11172,23 +10540,25 @@ function createUpdateEventAutomationTool(context, catalog) {
11172
10540
  readOnlyHint: false
11173
10541
  },
11174
10542
  executionMode: "sequential",
11175
- description: "Update the instruction, registered trigger, or credential use for an event automation.",
10543
+ description: "Update the instruction, registered trigger, credential use, or status of an event automation.",
11176
10544
  inputSchema: z.object({
11177
10545
  automationId: z.string().min(1),
11178
10546
  title: automationTitleSchema.optional(),
11179
10547
  instruction: automationInstructionToolSchema.nullable().optional(),
11180
10548
  trigger: registeredEventAutomationTriggerSchema(catalog).nullable().optional(),
11181
10549
  outcomes: z.array(taskOutcomeInputSchema).max(5).nullable().describe("Replacement messages to send after successful work. Use an empty list to send nothing. Omit or use null to leave unchanged.").optional(),
11182
- credentialMode: z.enum(["system", "creator"]).nullable().describe("Set creator to make the task's original creator credentials available, or system to disable them. Creator always means the task's createdBy actor, never the current requester. Only that original creator may enable creator mode. Omit or use null to leave unchanged.").optional()
10550
+ credentialMode: z.enum(["system", "creator"]).nullable().describe("Set creator to make the task's original creator credentials available, or system to disable them. Creator always means the task's createdBy actor, never the current requester. Only that original creator may enable creator mode. Omit or use null to leave unchanged.").optional(),
10551
+ status: z.enum(["active", "paused"]).nullable().describe("Set active to resume a paused or blocked automation, or paused to stop it. Only the creator can change the status. Omit or use null to leave unchanged.").optional()
11183
10552
  }).strict(),
11184
10553
  prepareArguments(args) {
11185
- const { credentialMode, outcomes, instruction, trigger, ...prepared } = args;
10554
+ const { credentialMode, outcomes, instruction, trigger, status, ...prepared } = args;
11186
10555
  return {
11187
10556
  ...prepared,
11188
10557
  ...instruction != null ? { instruction } : void 0,
11189
10558
  ...trigger != null ? { trigger } : void 0,
11190
10559
  ...outcomes != null ? { outcomes } : void 0,
11191
- ...credentialMode != null ? { credentialMode } : void 0
10560
+ ...credentialMode != null ? { credentialMode } : void 0,
10561
+ ...status != null ? { status } : void 0
11192
10562
  };
11193
10563
  },
11194
10564
  outputSchema: eventAutomationToolResultSchema,
@@ -11196,21 +10566,28 @@ function createUpdateEventAutomationTool(context, catalog) {
11196
10566
  const current = await writableEventAutomation(context, input.automationId);
11197
10567
  const { actor } = requireEventAutomationSlackContext(context);
11198
10568
  const isCreator = actor.userId === current.createdBy.slackUserId;
11199
- if (input.title === void 0 && input.instruction === void 0 && input.trigger === void 0 && input.outcomes == null && input.credentialMode == null) throw new ToolInputError("Event automation update requires a change.");
10569
+ const edits = input.title !== void 0 || input.instruction !== void 0 || input.trigger !== void 0 || input.outcomes != null || input.credentialMode != null;
10570
+ if (!edits && input.status == null) throw new ToolInputError("Event automation update requires a change.");
10571
+ if (input.status != null && !isCreator) throw new ToolInputError("Only the creator of this event automation can pause or resume it.");
11200
10572
  if (input.instruction != null && input.instruction !== current.task.text) requireRequesterMention(input.instruction, actor);
11201
- const next = await editEventAutomation(current, {
11202
- title: input.title,
11203
- instruction: input.instruction ?? void 0,
11204
- trigger: input.trigger ?? void 0,
11205
- outcomes: input.outcomes ?? void 0,
11206
- credentialMode: input.credentialMode ?? void 0
11207
- }, isCreator, catalog);
11208
- const saved = await saveEventAutomation(getDb(), next, automationRevision(current), {
11209
- slackUserId: actor.userId,
11210
- ...actor.fullName ? { fullName: actor.fullName } : void 0,
11211
- ...actor.userName ? { userName: actor.userName } : void 0
11212
- });
11213
- if (!saved) throw new ToolInputError("Event automation was not found.");
10573
+ let saved = current;
10574
+ if (edits) {
10575
+ const next = await editEventAutomation(current, {
10576
+ title: input.title,
10577
+ instruction: input.instruction ?? void 0,
10578
+ trigger: input.trigger ?? void 0,
10579
+ outcomes: input.outcomes ?? void 0,
10580
+ credentialMode: input.credentialMode ?? void 0
10581
+ }, isCreator, catalog);
10582
+ const edited = await saveEventAutomation(getDb(), next, automationRevision(current), {
10583
+ slackUserId: actor.userId,
10584
+ ...actor.fullName ? { fullName: actor.fullName } : void 0,
10585
+ ...actor.userName ? { userName: actor.userName } : void 0
10586
+ });
10587
+ if (!edited) throw new ToolInputError("Event automation was not found.");
10588
+ saved = edited;
10589
+ }
10590
+ if (input.status != null) saved = await setEventAutomationStatus(getDb(), saved.id, input.status, automationRevision(saved));
11214
10591
  return eventAutomationToolResult(context.conversationId, saved, catalog, actor.userId);
11215
10592
  }
11216
10593
  });
@@ -11228,193 +10605,6 @@ function createEventAutomationTools(context, catalog) {
11228
10605
  };
11229
10606
  }
11230
10607
  //#endregion
11231
- //#region src/chat/scheduled-automations/tasks.ts
11232
- const SCHEDULER_KEY_PREFIX = "junior:scheduler";
11233
- const retainedScheduledAutomationSchema = scheduledAutomationSchema.omit({
11234
- creatorIdentityId: true,
11235
- title: true
11236
- }).extend({
11237
- destination: slackDestinationSchema,
11238
- outcomes: z.array(taskOutcomeSchema).max(5),
11239
- version: z.number().optional()
11240
- }).strict();
11241
- /** Decode a retained scheduler task row and reject invalid routing context. */
11242
- function parseScheduledAutomationRow(row) {
11243
- const record = z.record(z.string(), z.unknown()).safeParse(row.record);
11244
- if (!record.success) return void 0;
11245
- const { creatorIdentityId: legacyCreatorIdentityId, title: legacyTitle, ...retained } = record.data;
11246
- const current = retained.outcomes === void 0 && retained.destination !== void 0 ? {
11247
- ...retained,
11248
- outcomes: [{
11249
- action: "send_message",
11250
- destination: retained.destination
11251
- }]
11252
- } : retained;
11253
- const parsed = retainedScheduledAutomationSchema.safeParse(current);
11254
- if (!parsed.success) return void 0;
11255
- const { status, version: _version, ...retainedTask } = parsed.data;
11256
- const destination = {
11257
- platform: "slack",
11258
- teamId: retainedTask.destination.teamId,
11259
- channelId: retainedTask.destination.channelId
11260
- };
11261
- const task = {
11262
- ...retainedTask,
11263
- destination,
11264
- outcomes: retainedTask.outcomes.map((outcome) => ({
11265
- ...outcome,
11266
- destination: {
11267
- platform: "slack",
11268
- teamId: outcome.destination.teamId,
11269
- channelId: outcome.destination.channelId
11270
- }
11271
- }))
11272
- };
11273
- const fallbackIdentity = row.creatorIdentityId === null ? z.string().safeParse(legacyCreatorIdentityId) : void 0;
11274
- const creatorIdentityId = row.creatorIdentityId ?? (fallbackIdentity?.success ? fallbackIdentity.data : void 0);
11275
- if (creatorIdentityId === void 0) return void 0;
11276
- const fallbackTitle = row.title === null ? z.string().safeParse(legacyTitle) : void 0;
11277
- const title = (row.title !== null ? row.title : fallbackTitle?.success ? fallbackTitle.data : void 0)?.trim() || void 0;
11278
- return {
11279
- ...task,
11280
- creatorIdentityId,
11281
- status,
11282
- ...title ? { title } : void 0
11283
- };
11284
- }
11285
- /** Keep scan paths from returning current or legacy tombstones. */
11286
- function isListedScheduledAutomation(task) {
11287
- return task !== void 0 && task.status !== "deleted";
11288
- }
11289
- /** Build the stable run id for one task occurrence. */
11290
- function scheduledRunId(taskId, scheduledForMs) {
11291
- return `${taskId}:${scheduledForMs}`;
11292
- }
11293
- /** Acquire the deployed advisory lock for one scheduled automation. */
11294
- function withScheduledAutomationLock(db, taskId, callback) {
11295
- return db.transaction(async (tx) => {
11296
- await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext(${`${SCHEDULER_KEY_PREFIX}:task:${taskId}:lock`}))`);
11297
- return await callback(tx);
11298
- });
11299
- }
11300
- function requireStoredTask(task) {
11301
- const parsed = scheduledAutomationSchema.safeParse(task);
11302
- if (!parsed.success) throw new Error("Scheduled automation routing context is invalid.");
11303
- const { title, ...current } = parsed.data;
11304
- const normalizedTitle = title?.trim() || void 0;
11305
- return {
11306
- ...current,
11307
- ...normalizedTitle ? { title: normalizedTitle } : void 0
11308
- };
11309
- }
11310
- function scheduledAutomationJsonRecord(task) {
11311
- const { title: _title, ...record } = task;
11312
- return record;
11313
- }
11314
- async function upsertScheduledAutomation(db, task) {
11315
- const title = task.title?.trim() || null;
11316
- await db.insert(juniorSchedulerTasks).values({
11317
- createdAtMs: task.createdAtMs,
11318
- creatorIdentityId: task.creatorIdentityId,
11319
- creatorSlackUserId: task.createdBy.slackUserId,
11320
- id: task.id,
11321
- nextRunAtMs: task.nextRunAtMs,
11322
- record: scheduledAutomationJsonRecord(task),
11323
- runNowAtMs: task.runNowAtMs,
11324
- status: task.status,
11325
- teamId: task.destination.teamId,
11326
- title
11327
- }).onConflictDoUpdate({
11328
- target: juniorSchedulerTasks.id,
11329
- set: {
11330
- createdAtMs: sql`excluded.created_at_ms`,
11331
- creatorIdentityId: sql`excluded.creator_identity_id`,
11332
- creatorSlackUserId: sql`excluded.creator_slack_user_id`,
11333
- nextRunAtMs: sql`excluded.next_run_at_ms`,
11334
- record: sql`excluded.record`,
11335
- runNowAtMs: sql`excluded.run_now_at_ms`,
11336
- status: sql`excluded.status`,
11337
- teamId: sql`excluded.team_id`,
11338
- title: sql`excluded.title`
11339
- }
11340
- });
11341
- }
11342
- /** Read and decode one scheduled automation. */
11343
- async function readScheduledAutomation(db, taskId) {
11344
- const rows = await db.select({
11345
- creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
11346
- record: juniorSchedulerTasks.record,
11347
- title: juniorSchedulerTasks.title
11348
- }).from(juniorSchedulerTasks).where(eq(juniorSchedulerTasks.id, taskId)).limit(1);
11349
- return rows[0] ? parseScheduledAutomationRow(rows[0]) : void 0;
11350
- }
11351
- async function readListedScheduledAutomations(db, teamId) {
11352
- return (await db.select({
11353
- creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
11354
- record: juniorSchedulerTasks.record,
11355
- title: juniorSchedulerTasks.title
11356
- }).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);
11357
- }
11358
- /** List decoded scheduled automations in stable creation order. */
11359
- async function listScheduledAutomations(db) {
11360
- return await readListedScheduledAutomations(db);
11361
- }
11362
- /** List decoded scheduled automations for one team in stable creation order. */
11363
- async function listScheduledAutomationsForTeam(db, teamId) {
11364
- return await readListedScheduledAutomations(db, teamId);
11365
- }
11366
- async function writeScheduledAutomation(db, task, current, editedBy) {
11367
- 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")));
11368
- if (task.status === "deleted" && current?.status !== "deleted") await skipPendingRunsForDeletedTask(db, task);
11369
- await upsertScheduledAutomation(db, task);
11370
- await recordAutomationVersion(db, "scheduled", task, current, editedBy);
11371
- }
11372
- async function skipPendingRunsForDeletedTask(db, task) {
11373
- const errorMessage = `Scheduled automation ${task.id} was deleted before the run started.`;
11374
- const rows = await db.select({ record: juniorSchedulerRuns.record }).from(juniorSchedulerRuns).where(and(eq(juniorSchedulerRuns.taskId, task.id), eq(juniorSchedulerRuns.status, "pending")));
11375
- for (const row of rows) {
11376
- const parsed = scheduledRunSchema.safeParse(row.record);
11377
- if (!parsed.success || parsed.data.status !== "pending") continue;
11378
- const next = scheduledRunSchema.parse({
11379
- ...parsed.data,
11380
- completedAtMs: task.updatedAtMs,
11381
- errorMessage,
11382
- status: "skipped"
11383
- });
11384
- await db.update(juniorSchedulerRuns).set({
11385
- record: next,
11386
- status: next.status
11387
- }).where(and(eq(juniorSchedulerRuns.id, next.id), eq(juniorSchedulerRuns.status, "pending")));
11388
- }
11389
- }
11390
- /** Create a scheduled automation once under its retry-stable task lock. */
11391
- async function createScheduledAutomation(db, task) {
11392
- const next = requireStoredTask(task);
11393
- return await withScheduledAutomationLock(db, task.id, async (tx) => {
11394
- const current = await readScheduledAutomation(tx, task.id);
11395
- if (current) return current;
11396
- await writeScheduledAutomation(tx, next, void 0);
11397
- return next;
11398
- });
11399
- }
11400
- /**
11401
- * Save a scheduled automation and clear its blocked occurrence on reactivation.
11402
- * Pass `editedBy` when a person changes the definition so its version names them.
11403
- */
11404
- async function saveScheduledAutomation(db, task, expectedRevision, editedBy) {
11405
- const next = requireStoredTask(task);
11406
- await withScheduledAutomationLock(db, task.id, async (tx) => {
11407
- const current = await readScheduledAutomation(tx, task.id);
11408
- if (expectedRevision !== void 0) requireAutomationRevision(current, expectedRevision);
11409
- await writeScheduledAutomation(tx, next, current, editedBy);
11410
- });
11411
- return next;
11412
- }
11413
- /** Save a scheduled automation inside a task lock already held by the caller. */
11414
- async function saveScheduledAutomationInLock(db, task, current) {
11415
- await writeScheduledAutomation(db, requireStoredTask(task), current);
11416
- }
11417
- //#endregion
11418
10608
  //#region src/chat/scheduled-automations/telemetry.ts
11419
10609
  /** Safe structured telemetry attributes for scheduled-automation lifecycle events. */
11420
10610
  /** Correlation attributes shared by scheduled-automation create and run events. */
@@ -11451,76 +10641,6 @@ function logScheduledAutomationRunSkipped(task, run, extras = {}) {
11451
10641
  logInfo("scheduled_automation.run.skipped", scheduledAutomationRunAttributes(task, run, extras));
11452
10642
  }
11453
10643
  //#endregion
11454
- //#region src/chat/slack/user.ts
11455
- const slackUserInfoSchema = z.object({
11456
- ok: z.literal(true),
11457
- user: z.object({
11458
- name: z.string().nullish(),
11459
- tz: z.string().nullish(),
11460
- profile: z.object({
11461
- email: z.string().nullish(),
11462
- real_name: z.string().nullish()
11463
- }).optional()
11464
- })
11465
- });
11466
- const USER_CACHE_TTL_MS = 3e5;
11467
- const userCache = /* @__PURE__ */ new Map();
11468
- function userCacheKey(teamId, userId) {
11469
- return `${teamId}:${userId}`;
11470
- }
11471
- function readFromCache(teamId, userId) {
11472
- const hit = userCache.get(userCacheKey(teamId, userId));
11473
- if (!hit) return null;
11474
- if (hit.expiresAt < Date.now()) {
11475
- userCache.delete(userCacheKey(teamId, userId));
11476
- return null;
11477
- }
11478
- return hit.value;
11479
- }
11480
- function writeToCache(teamId, userId, value) {
11481
- userCache.set(userCacheKey(teamId, userId), {
11482
- value,
11483
- expiresAt: Date.now() + USER_CACHE_TTL_MS
11484
- });
11485
- }
11486
- /** Fetch Slack user profile info with in-memory TTL cache to avoid repeated API calls. */
11487
- async function lookupSlackUser(teamId, userId) {
11488
- if (!teamId || !userId) return null;
11489
- const cached = readFromCache(teamId, userId);
11490
- if (cached) return cached;
11491
- const token = getSlackBotToken();
11492
- if (!token) return null;
11493
- try {
11494
- const response = await fetch(`https://slack.com/api/users.info?user=${encodeURIComponent(userId)}`, { headers: { authorization: `Bearer ${token}` } });
11495
- if (!response.ok) {
11496
- logWarn("slack.user.lookup.failed", {
11497
- "enduser.id": userId,
11498
- "app.slack.team_id": teamId,
11499
- "http.response.status_code": response.status
11500
- });
11501
- return null;
11502
- }
11503
- const parsed = slackUserInfoSchema.safeParse(await response.json());
11504
- if (!parsed.success) return null;
11505
- const payload = parsed.data;
11506
- const result = {
11507
- userName: payload.user.name?.trim() || void 0,
11508
- fullName: payload.user.profile?.real_name?.trim() || void 0,
11509
- email: payload.user.profile?.email?.trim() || void 0,
11510
- timezone: payload.user.tz?.trim() || void 0
11511
- };
11512
- writeToCache(teamId, userId, result);
11513
- return result;
11514
- } catch (error) {
11515
- logWarn("slack.user.lookup.failed", {
11516
- "enduser.id": userId,
11517
- "app.slack.team_id": teamId,
11518
- "exception.message": error instanceof Error ? error.message : String(error)
11519
- });
11520
- return null;
11521
- }
11522
- }
11523
- //#endregion
11524
10644
  //#region src/chat/scheduled-automations/identity.ts
11525
10645
  const SLACK_USER_ID_PATTERN = /^[UW][A-Z0-9]{5,}$/;
11526
10646
  function clean(value) {
@@ -11551,7 +10671,6 @@ function sanitizeScheduledAutomationPrincipal(principal) {
11551
10671
  //#endregion
11552
10672
  //#region src/chat/scheduled-automations/tool-support.ts
11553
10673
  const TASK_ID_PREFIX = "sched";
11554
- const DEFAULT_SCHEDULE_TIMEZONE = "America/Los_Angeles";
11555
10674
  const compactTaskResultSchema = z.object({
11556
10675
  id: z.string(),
11557
10676
  title: z.string().nullable(),
@@ -11768,23 +10887,6 @@ function buildTaskId(args) {
11768
10887
  function normalizeStatus(value) {
11769
10888
  if (value === "active" || value === "blocked") return value;
11770
10889
  }
11771
- /** Centralize scheduler timezone defaulting for all concrete tool entry points. */
11772
- function getDefaultScheduleTimezone() {
11773
- return process.env.JUNIOR_TIMEZONE?.trim() || DEFAULT_SCHEDULE_TIMEZONE;
11774
- }
11775
- /**
11776
- * Pick the timezone for a new schedule when the user names none: the
11777
- * creator's Slack profile, then the saved user timezone, then the install
11778
- * default. A valid Slack timezone is saved on the linked user.
11779
- */
11780
- async function resolveCreatorScheduleTimezone(args) {
11781
- const timezone = (await lookupSlackUser(args.teamId, args.slackUserId))?.timezone;
11782
- if (timezone && isValidTimeZone$1(timezone)) {
11783
- if (args.userId) await saveUserTimezone(args.db, args.userId, timezone, args.nowMs);
11784
- return timezone;
11785
- }
11786
- return (args.userId ? await readUserTimezone(args.db, args.userId) : void 0) ?? getDefaultScheduleTimezone();
11787
- }
11788
10890
  //#endregion
11789
10891
  //#region src/chat/scheduled-automations/tools/create.ts
11790
10892
  /** Create a tool that stores a scheduled automation for the active Slack context. */
@@ -11832,11 +10934,13 @@ function createSlackScheduleCreateAutomationTool(context) {
11832
10934
  const identity = creator?.identity;
11833
10935
  if (!identity || identity.provider !== "slack" || identity.providerTenantId !== destination.teamId || identity.providerSubjectId !== actor.slackUserId) throwToolInputError("Scheduled automation creator identity is unavailable.");
11834
10936
  const nowMs = context.now?.() ?? Date.now();
11835
- const defaultTimezone = input.schedule.timezone ?? await resolveCreatorScheduleTimezone({
10937
+ const defaultTimezone = input.schedule.timezone ?? await resolveUserTimezone({
11836
10938
  db,
11837
10939
  nowMs,
11838
- slackUserId: actor.slackUserId,
11839
- teamId: destination.teamId,
10940
+ slack: {
10941
+ teamId: destination.teamId,
10942
+ userId: actor.slackUserId
10943
+ },
11840
10944
  ...creator.user ? { userId: creator.user.id } : void 0
11841
10945
  });
11842
10946
  let compiled;
@@ -11857,7 +10961,7 @@ function createSlackScheduleCreateAutomationTool(context) {
11857
10961
  instruction: input.instruction,
11858
10962
  title: input.title
11859
10963
  });
11860
- const committed = await createScheduledAutomation(db, {
10964
+ const task = {
11861
10965
  id,
11862
10966
  createdAtMs: nowMs,
11863
10967
  updatedAtMs: nowMs,
@@ -11874,7 +10978,8 @@ function createSlackScheduleCreateAutomationTool(context) {
11874
10978
  outcomes: await resolveTaskOutcomes(input.outcomes, destination, actor.slackUserId),
11875
10979
  task: { text: input.instruction },
11876
10980
  ...title ? { title } : void 0
11877
- });
10981
+ };
10982
+ const committed = await createScheduledAutomation(db, task);
11878
10983
  if (!sameDestination(committed, destination) || committed.createdBy.slackUserId !== actor.slackUserId) throwToolInputError("Scheduled automation operation identity is invalid.");
11879
10984
  logInfo("scheduled_automation.create.completed", scheduledAutomationAttributes(committed));
11880
10985
  return scheduleAutomationToolResult(context.conversationId, committed, actor.slackUserId);
@@ -12056,7 +11161,7 @@ async function editScheduledAutomation(current, input, isCreator, nowMs) {
12056
11161
  let compiled;
12057
11162
  if (input.schedule !== void 0) try {
12058
11163
  compiled = compileScheduleIntent({
12059
- defaultTimezone: current.schedule.timezone || getDefaultScheduleTimezone(),
11164
+ defaultTimezone: current.schedule.timezone || defaultTimezone(),
12060
11165
  intent: input.schedule,
12061
11166
  nowMs
12062
11167
  });
@@ -15121,7 +14226,7 @@ async function resolveStoredProvider(teamId, provider, query) {
15121
14226
  /** Create the tool that resolves people by identity provider and query. */
15122
14227
  function createUserLookupTool(teamId, providers) {
15123
14228
  return zodTool({
15124
- description: "Look up people by identity provider and query. Pass an enabled `provider` and its provider subject ID or handle. The `slack` provider also accepts email and name searches. Returns Slack `mention` values (`<@U…>`) for the current workspace. Use a mention only when one person clearly matches; otherwise ask which person the user means.",
14229
+ description: "Look up people by identity provider and query. Pass an enabled `provider` and its provider subject ID or handle. For `slack`, pass the person's Slack user ID (`U…`) whenever the conversation shows one, such as the requester's `author_id`, a participant's `actor_id`, or a `<@U…>` mention. The `slack` provider also accepts email and name searches. Use name search only for people with no known ID, because it can miss people in large workspaces. Returns Slack `mention` values (`<@U…>`) for the current workspace. Use a mention only when one person clearly matches; otherwise ask which person the user means.",
15125
14230
  annotations: {
15126
14231
  destructiveHint: false,
15127
14232
  idempotentHint: true,
@@ -15130,7 +14235,7 @@ function createUserLookupTool(teamId, providers) {
15130
14235
  },
15131
14236
  inputSchema: z.object({
15132
14237
  provider: z.enum(providers).describe("Enabled identity provider to query."),
15133
- query: z.string().trim().min(1).describe("Provider subject ID, handle, Slack email, or Slack name.")
14238
+ query: z.string().trim().min(1).describe("Provider subject ID, handle, Slack email, or Slack name. Prefer a Slack user ID (U…) when one is known.")
15134
14239
  }).strict(),
15135
14240
  outputSchema: juniorToolOutputSchema,
15136
14241
  execute: async ({ provider, query }) => {
@@ -15213,14 +14318,6 @@ function createSystemTimeTool() {
15213
14318
  }
15214
14319
  });
15215
14320
  }
15216
- function isValidTimeZone(timezone) {
15217
- try {
15218
- new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
15219
- return true;
15220
- } catch {
15221
- return false;
15222
- }
15223
- }
15224
14321
  function getTimeZoneOffsetMinutes(timestampMs, timezone) {
15225
14322
  const parts = getZonedDateTimeParts(timestampMs, timezone);
15226
14323
  const asUtcMs = Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
@@ -17032,6 +16129,7 @@ function createTools(availableSkills, hooks = {}, context, options = {}) {
17032
16129
  }
17033
16130
  if (context.handoff) tools.handoff = createHandoffTool(context.handoff);
17034
16131
  if (context.spawnAgent) tools.spawnAgent = createSpawnAgentTool(context.spawnAgent);
16132
+ if (context.automation) tools[FINISH_AUTOMATION_RUN_TOOL_NAME] = createFinishAutomationRunTool(context.automation);
17035
16133
  if (context.mcpToolManager) {
17036
16134
  tools.searchMcpTools = createSearchMcpToolsTool(context.mcpToolManager);
17037
16135
  tools.callMcpTool = createCallMcpToolTool(context.mcpToolManager);
@@ -19074,6 +18172,22 @@ async function requestBodyBytes(request) {
19074
18172
  if (request.method === "GET" || request.method === "HEAD" || request.body === null) return;
19075
18173
  return await request.arrayBuffer();
19076
18174
  }
18175
+ /**
18176
+ * Explain a request body that the runtime delivered already used.
18177
+ *
18178
+ * The forwarded headers cannot show whether the body was empty, so the proxy
18179
+ * does not send a credentialed write upstream without its payload.
18180
+ */
18181
+ function unreadableBodyResponse(eventId) {
18182
+ return Response.json({
18183
+ source: "junior-egress",
18184
+ error: "Junior's sandbox egress proxy could not read the request body, so it did not send the request upstream.",
18185
+ ...eventId ? { sentryEventId: eventId } : void 0
18186
+ }, {
18187
+ status: 500,
18188
+ headers: { "cache-control": "no-store" }
18189
+ });
18190
+ }
19077
18191
  /** Identify GitHub writes whose body determines whether a grant is safe. */
19078
18192
  function githubBodyInspection(input) {
19079
18193
  if (input.provider !== "github" || input.requestMethod.toUpperCase() !== "POST" || input.upstreamUrl.hostname.toLowerCase() !== "api.github.com") return;
@@ -19229,11 +18343,28 @@ async function recordSandboxPermissionDenied(input) {
19229
18343
  */
19230
18344
  async function executeCredentialedEgressRequest(input) {
19231
18345
  const { activeEgressId, credentialContext, deps, operation, provider, request, upstreamUrl } = input;
18346
+ let body;
18347
+ try {
18348
+ body = await requestBodyBytes(request);
18349
+ } catch (error) {
18350
+ if (!(error instanceof TypeError)) throw error;
18351
+ return unreadableBodyResponse(logException(error, "sandbox.egress.request_body.unreadable", {
18352
+ ...egressAttributes({
18353
+ egressId: activeEgressId,
18354
+ host: upstreamUrl.hostname,
18355
+ method: request.method,
18356
+ path: upstreamUrl.pathname,
18357
+ provider,
18358
+ status: 500
18359
+ }),
18360
+ ...routingAttributes(request, upstreamUrl)
18361
+ }));
18362
+ }
19232
18363
  const bodyForGrantSelection = githubBodyInspection({
19233
18364
  provider,
19234
18365
  requestMethod: request.method,
19235
18366
  upstreamUrl
19236
- }) ? await requestBodyBytes(request) : void 0;
18367
+ }) ? body : void 0;
19237
18368
  let grantSelection;
19238
18369
  try {
19239
18370
  grantSelection = await selectSandboxEgressGrant({
@@ -19325,7 +18456,6 @@ async function executeCredentialedEgressRequest(input) {
19325
18456
  return Response.json({ error: "Credential lease does not cover forwarded host" }, { status: 403 });
19326
18457
  }
19327
18458
  const fetchImpl = deps.fetch ?? fetch;
19328
- const body = bodyForGrantSelection ?? await requestBodyBytes(request);
19329
18459
  let retriedAfter403 = false;
19330
18460
  while (true) {
19331
18461
  const headers = requestHeaders(request, lease, upstreamUrl.hostname, deps.tracePropagation ?? {});
@@ -19955,7 +19085,8 @@ async function wireAgentTools(args) {
19955
19085
  surface: args.surface,
19956
19086
  ...args.currentActor ? { resolveActorIdentity: async () => await readActorIdentity(args.currentActor) } : void 0,
19957
19087
  ...args.durability.spawnAgent ? { spawnAgent: args.durability.spawnAgent } : void 0,
19958
- ...args.requestHandoff ? { handoff: args.requestHandoff } : void 0
19088
+ ...args.requestHandoff ? { handoff: args.requestHandoff } : void 0,
19089
+ ...args.run.dispatch?.declaresResult ? { automation: { sendsMessage: args.run.dispatch.outcomes?.length !== 0 } } : void 0
19959
19090
  };
19960
19091
  const toolRoute = resolveToolRuntimeRoute({
19961
19092
  actor: args.currentActor,
@@ -20788,7 +19919,11 @@ async function compactActiveContextIfNeeded(args, deps) {
20788
19919
  //#endregion
20789
19920
  //#region src/chat/agent/handoff.ts
20790
19921
  /** Tools that must be the only tool call in their assistant message. */
20791
- const EXCLUSIVE_TOOL_NAMES = [HANDOFF_TOOL_NAME, "switchWorkspace"];
19922
+ const EXCLUSIVE_TOOL_NAMES = [
19923
+ HANDOFF_TOOL_NAME,
19924
+ "switchWorkspace",
19925
+ FINISH_AUTOMATION_RUN_TOOL_NAME
19926
+ ];
20792
19927
  /** Return the exclusive tool name when one is present among the calls. */
20793
19928
  function exclusiveToolName(toolCalls) {
20794
19929
  for (const name of EXCLUSIVE_TOOL_NAMES) if (toolCalls.some((call) => call.name === name)) return name;
@@ -20987,6 +20122,7 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
20987
20122
  const conversationId = run.conversationId;
20988
20123
  const turnId = run.turnId;
20989
20124
  const runId = run.runId;
20125
+ const requireAutomationResult = run.dispatch?.declaresResult === true;
20990
20126
  const input = {
20991
20127
  actor: run.instruction.actor,
20992
20128
  includeConversationContextWithPiMessages: run.instruction.includeConversationContextWithHistory,
@@ -21511,11 +20647,13 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
21511
20647
  };
21512
20648
  }
21513
20649
  },
21514
- afterToolCall: async ({ result, toolCall }, signal) => {
20650
+ afterToolCall: async ({ isError, result, toolCall }, signal) => {
21515
20651
  const timedOutResult = runResume.timedOut && signal?.aborted ? projectTimedOutToolResult(result) : void 0;
21516
20652
  const sourceResult = timedOutResult ?? result;
21517
20653
  const projectedResult = wiring.projectActionReviewResult(toolCall.id, sourceResult);
21518
- return timedOutResult || projectedResult !== result ? projectedResult : void 0;
20654
+ const override = timedOutResult || projectedResult !== result ? projectedResult : void 0;
20655
+ if (toolCall.name === "finishAutomationRun" && !isError && !override) return { terminate: true };
20656
+ return override;
21519
20657
  },
21520
20658
  prepareNextTurnWithContext: async (nextTurn, hookSignal) => {
21521
20659
  try {
@@ -21564,6 +20702,7 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
21564
20702
  runResume.setBeforeMessageCount(agent.state.messages.length);
21565
20703
  if (shouldPromptAgent) runResume.setTurnStartMessageIndex(agent.state.messages.length);
21566
20704
  const authPauseOutcome = await withSpan(`invoke_agent ${botConfig.userName}`, "gen_ai.invoke_agent", spanContext, async () => {
20705
+ if (requireAutomationResult && readAutomationResult(agent.state.messages)) return;
21567
20706
  const promptTimestamp = restoredPromptTimestamp ?? Date.now();
21568
20707
  const contextMessage = shouldPromptAgent && promptContextContentParts.length > 0 ? {
21569
20708
  role: "user",
@@ -21681,6 +20820,7 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
21681
20820
  run = shouldPromptAgent && !capacityUpdate ? agent.prompt(freshPromptMessage) : agent.continue();
21682
20821
  let discardedRetryUsage;
21683
20822
  let actionConfirmationRetryUsed = false;
20823
+ let automationResultReminderUsed = !requireAutomationResult;
21684
20824
  let providerRetryAttempt = 0;
21685
20825
  let emptyOutputAttempt = 0;
21686
20826
  const prepareRetry = async (messages) => {
@@ -21720,6 +20860,15 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
21720
20860
  run = agent.continue();
21721
20861
  continue;
21722
20862
  }
20863
+ const automationResultReminder = automationResultReminderUsed ? void 0 : remindMissingAutomationResult(agent.state.messages);
20864
+ if (automationResultReminder) {
20865
+ automationResultReminderUsed = true;
20866
+ agent.state.messages = automationResultReminder;
20867
+ await runResume.persistSafeBoundary(automationResultReminder);
20868
+ logWarn("agent.turn.automation_result.reminded", {});
20869
+ run = agent.continue();
20870
+ continue;
20871
+ }
21723
20872
  const emptyOutputContinuation = nextEmptyOutputContinuation({
21724
20873
  attempt: emptyOutputAttempt,
21725
20874
  lastAssistant,
@@ -21786,7 +20935,8 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
21786
20935
  usage: turnUsage,
21787
20936
  executionProfile: turnRoute,
21788
20937
  assistantUserName: botConfig.userName,
21789
- modelId: activeModelId
20938
+ modelId: activeModelId,
20939
+ requireAutomationResult
21790
20940
  })
21791
20941
  };
21792
20942
  } catch (error) {
@@ -21879,7 +21029,7 @@ function createAgentRunner(execute, options) {
21879
21029
  } } : void 0
21880
21030
  };
21881
21031
  if (run.instruction.storedAttachments?.length) {
21882
- const { loadInputImages } = await import("./images-C_0iwPsW.js").then((n) => n.n);
21032
+ const { loadInputImages } = await import("./images-DY6Pxq4w.js").then((n) => n.n);
21883
21033
  const storage = nextRun.environment?.attachmentStorage;
21884
21034
  if (!storage) throw new Error("Attachment storage is unavailable.");
21885
21035
  nextRun.instruction = {
@@ -21895,4 +21045,4 @@ function createAgentRunner(execute, options) {
21895
21045
  } };
21896
21046
  }
21897
21047
  //#endregion
21898
- export { eventAutomationBelongsToUser as $, CONVERSATION_WORK_CHECK_IN_INTERVAL_MS as $t, resolveSlackChannelIdFromMessage as A, slackEntitySchema as An, beginConversationResume as At, parseScheduledAutomationRow as B, setConfigDefaults as Bn, ensureConversationWake as Bt, renderBlockText as C, normalizeSlackReplyMarkdown as Cn, timerWatchCredentialSubject as Ct, storeAttachment as D, addReactionToMessage as Dn, ackMessages as Dt, requestAttachmentDeletion as E, truncateStatusText as En, pluginEventCatalog as Et, lookupSlackUser as F, actorFromRun as Fn, completeConversationStop as Ft, withScheduledAutomationLock as G, recordAttemptFailure as Gt, saveScheduledAutomation as H, getConversationWorkState as Ht, logScheduledAutomationRunSkipped as I, toolInvocationDestination as In, completeConversationWork as It, eventAutomationTriggerAvailable as J, removeActiveConversation as Jt, editEventAutomation as K, recordConversationRetry as Kt, scheduledAutomationRunAttributes as L, AuthorizationFlowDisabledError as Ln, countPendingConversationMessages as Lt, getConversationInfo as M, createUserTokenStore as Mn, checkInConversationWork as Mt, listThreadReplies as N, buildSteeringPiMessage as Nn, clearConsumedConversationWake as Nt, parseSlackThreadId as O, postSlackMessage as On, appendAndEnqueueExclusiveInboundMessage as Ot, editScheduledAutomation as P, RetryableDeliveryError as Pn, clearExpiredConversationLease as Pt, deleteEventAutomation as Q, stopConversationWork as Qt, listScheduledAutomations as R, shouldEmitDevAgentTrace as Rn, deadLetterAttempt as Rt, renderAttachmentText as S, formatSlackLink as Sn, isEventMailboxMetadata as St, readLiveAttachment as T, slackMention as Tn, eventGuidance as Tt, saveScheduledAutomationInLock as U, hasRunnableConversationWork as Ut, readScheduledAutomation as V, getConversation as Vt, scheduledRunId as W, listActiveConversationIds as Wt, resolveTaskOutcomes as X, requestConversationWork as Xt, effectiveTaskOutcomes as Y, requestAnotherSlice as Yt, collectEventAutomationMatchKeys as Z, startConversationWork as Zt, resolvePluginOAuthAccount as _, formatProviderLabel as _n, AgentInvocationLimitError as _t, buildAuthPauseResponse as a, renderTaskInput as an, setEventAutomationStatus as at, unpublishedArtifactHeaders as b, startOAuthFlow as bn, ingestEvent as bt, deleteMcpAuthSessionsForUserProvider as c, resolveSandboxEgressProviderForHost as cn, readAutomationVersion as ct, getMcpAuthSession as d, SANDBOX_EGRESS_PROXY_PATH as dn, automationRevision as dt, CONVERSATION_WORK_STALE_ENQUEUE_MS as en, findMatchingEventAutomations as et, getMcpStoredOAuthCredentials as f, clearSandboxEgressSignals as fn, requireAutomationRevision as ft, readStats as g, sandboxEgressSignalsResponseSchema as gn, AgentInvocationBusyError as gt, latestProgressStatus as h, parseSandboxEgressCredentialToken as hn, createAgentInvocationSchema as ht, executeCredentialedEgressRequest as i, isInvalidConversationRecordError as in, saveEventAutomation as it, resolveSlackChannelIdFromThreadId as j, slackExternalRefIdSchema as jn, cancelHumanFacingPendingMessages as jt, readSlackRawMessageContext as k, removeReactionFromMessage as kn, appendAndEnqueueInboundMessage as kt, deleteMcpServerSessionId as l, normalizeSandboxEgressTracePropagationDomains as ln, sameDefinitionValue as lt, PluginCredentialFailureError as m, consumeSandboxEgressPermissionDeniedSignal as mn, agentInvocationSchema as mt, executeAgentRun as n, hasRunnableConversationWork$1 as nn, listDeletedEventAutomationsCreatedBy as nt, finalizeMcpAuthorization as o, maybePostAutomatedTurnLimitNotice as on, automationDefinition as ot, putMcpStoredOAuthCredentials as p, consumeSandboxEgressAuthRequiredSignal as pn, agentBindingSchema as pt, AutomationEditError as q, releaseConversationWork as qt, createContextCompactor as r, isFinalAttempt as rn, parseEventAutomationRow as rt, deleteMcpAuthSession as s, recordFinishedTurnForAutomatedLimit as sn, listAutomationVersions as st, createAgentRunner as t, hasConversationStop as tn, getEventAutomation as tt, deleteMcpStoredOAuthCredentials as u, shouldPropagateSandboxEgressTrace as un, AutomationConflictError as ut, publicArtifactHeaders as v, parseOAuthStatePayload as vn, getMcpProviderErrorAttributes as vt, collectAttachmentGarbage as w, normalizeSlackStatusText as wn, getEventCatalog as wt, lookupSlackUserProfile as x, escapeSlackMrkdwnText as xn, EVENT_WAIT_MS as xt, readPublicArtifact as y, resolveBaseUrl as yn, processWorkspaceSnapshotJob as yt, listScheduledAutomationsForTeam as z, getConfigDefaults as zn, drainConversationMailbox as zt };
21048
+ export { getEventCatalog as $, actorFromRun as $t, resolveSlackChannelIdFromMessage as A, hasConversationStop as At, effectiveTaskOutcomes as B, SANDBOX_EGRESS_PROXY_PATH as Bt, renderBlockText as C, removeActiveConversation as Ct, storeAttachment as D, stopConversationWork as Dt, requestAttachmentDeletion as E, startConversationWork as Et, logScheduledAutomationRunSkipped as F, maybePostAutomatedTurnLimitNotice as Ft, AgentInvocationBusyError as G, sandboxEgressSignalsResponseSchema as Gt, agentBindingSchema as H, consumeSandboxEgressAuthRequiredSignal as Ht, scheduledAutomationRunAttributes as I, recordFinishedTurnForAutomatedLimit as It, processWorkspaceSnapshotJob as J, resolveBaseUrl as Jt, AgentInvocationLimitError as K, formatProviderLabel as Kt, editEventAutomation as L, resolveSandboxEgressProviderForHost as Lt, getConversationInfo as M, isFinalAttempt as Mt, listThreadReplies as N, isInvalidConversationRecordError as Nt, parseSlackThreadId as O, CONVERSATION_WORK_CHECK_IN_INTERVAL_MS as Ot, editScheduledAutomation as P, renderTaskInput as Pt, timerWatchCredentialSubject as Q, RetryableDeliveryError as Qt, AutomationEditError as R, normalizeSandboxEgressTracePropagationDomains as Rt, renderAttachmentText as S, releaseConversationWork as St, readLiveAttachment as T, requestConversationWork as Tt, agentInvocationSchema as U, consumeSandboxEgressPermissionDeniedSignal as Ut, resolveTaskOutcomes as V, clearSandboxEgressSignals as Vt, createAgentInvocationSchema as W, parseSandboxEgressCredentialToken as Wt, EVENT_WAIT_MS as X, createUserTokenStore as Xt, ingestEvent as Y, startOAuthFlow as Yt, isEventMailboxMetadata as Z, buildSteeringPiMessage as Zt, resolvePluginOAuthAccount as _, getConversationWorkState as _t, buildAuthPauseResponse as a, shouldEmitDevAgentTrace as an, beginConversationResume as at, unpublishedArtifactHeaders as b, recordAttemptFailure as bt, deleteMcpAuthSessionsForUserProvider as c, clearConsumedConversationWake as ct, getMcpAuthSession as d, completeConversationWork as dt, toolInvocationDestination as en, eventGuidance as et, getMcpStoredOAuthCredentials as f, countPendingConversationMessages as ft, readStats as g, getConversation as gt, latestProgressStatus as h, ensureConversationWake as ht, executeCredentialedEgressRequest as i, runDispatchOutcome as in, appendAndEnqueueInboundMessage as it, resolveSlackChannelIdFromThreadId as j, hasRunnableConversationWork$1 as jt, readSlackRawMessageContext as k, CONVERSATION_WORK_STALE_ENQUEUE_MS as kt, deleteMcpServerSessionId as l, clearExpiredConversationLease as lt, PluginCredentialFailureError as m, drainConversationMailbox as mt, executeAgentRun as n, deliversFinalText as nn, ackMessages as nt, finalizeMcpAuthorization as o, getConfigDefaults as on, cancelHumanFacingPendingMessages as ot, putMcpStoredOAuthCredentials as p, deadLetterAttempt as pt, getMcpProviderErrorAttributes as q, parseOAuthStatePayload as qt, createContextCompactor as r, finishedRunReply as rn, appendAndEnqueueExclusiveInboundMessage as rt, deleteMcpAuthSession as s, setConfigDefaults as sn, checkInConversationWork as st, createAgentRunner as t, AuthorizationFlowDisabledError as tn, pluginEventCatalog as tt, deleteMcpStoredOAuthCredentials as u, completeConversationStop as ut, publicArtifactHeaders as v, hasRunnableConversationWork as vt, collectAttachmentGarbage as w, requestAnotherSlice as wt, lookupSlackUserProfile as x, recordConversationRetry as xt, readPublicArtifact as y, listActiveConversationIds as yt, eventAutomationTriggerAvailable as z, shouldPropagateSandboxEgressTrace as zt };