@sentry/junior 0.225.0 → 0.227.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 (65) hide show
  1. package/dist/{agent-hooks-CRuRcOg9.js → agent-hooks-bEwEJhlg.js} +231 -17
  2. package/dist/{agent-runner-Cad1RtWn.js → agent-runner-BGDSjciH.js} +2899 -159
  3. package/dist/api/schema.d.ts +417 -15
  4. package/dist/api/schema.js +4 -4
  5. package/dist/api.js +57 -32
  6. package/dist/{app-BbSD6EtD.d.ts → app-CjRIKZb5.d.ts} +1 -1
  7. package/dist/app.d.ts +1 -1
  8. package/dist/app.js +971 -7685
  9. package/dist/{briefs-BtnkPWT5.js → briefs-BkM_ZW4B.js} +4 -4
  10. package/dist/{catalog-runtime-pv4grNNb.js → catalog-runtime-Cw9V2jpn.js} +1 -1
  11. package/dist/{checkpoint-6fkAdB3g.js → checkpoint-oZYD3-Ar.js} +77 -11
  12. package/dist/cli/chat.js +13 -13
  13. package/dist/cli/check.js +3 -3
  14. package/dist/cli/main.js +1 -1
  15. package/dist/cli/plugins.js +3 -3
  16. package/dist/cli/snapshot-create.js +3 -3
  17. package/dist/cli/upgrade.js +3 -3
  18. package/dist/{client-ihmkx60d.js → client-Pcxo3lGn.js} +2 -2
  19. package/dist/{config-DMg679BW.js → config-CYvEow1F.js} +2 -2
  20. package/dist/{context-Bplcgypl.js → context-0Et9iVSB.js} +1 -1
  21. package/dist/{conversation-Bygi39Xj.js → conversation-BnhOMfEB.js} +113 -25
  22. package/dist/{conversation-privacy-ZYy05Y3q.js → conversation-privacy-hQmyZbOC.js} +1 -1
  23. package/dist/{credential-sync-DCB2SwnQ.js → credential-sync-CUGbiiCM.js} +15 -135
  24. package/dist/{executor-B9R4cHte.js → executor-BXMlFrV6.js} +1 -1
  25. package/dist/history-nukv1YN8.d.ts +127 -0
  26. package/dist/images-8siTAUNW.js +82 -0
  27. package/dist/instrumentation.js +2 -2
  28. package/dist/{vercel-queue-DIkVcfK4.js → job-queue-AI0_QAgc.js} +61 -2
  29. package/dist/{logging-CcTvYQ8A.js → logging-BPBx6WF-.js} +1 -1
  30. package/dist/nitro.d.ts +1 -1
  31. package/dist/nitro.js +3 -4
  32. package/dist/{oauth-authorization-BS4vPJmu.js → oauth-authorization-Cq1GaLIP.js} +1 -1
  33. package/dist/{oauth-callback-server-DnlQsGBJ.js → oauth-callback-server-CnF03VYh.js} +2 -2
  34. package/dist/pending-auth-CHUjr_6d.js +59 -0
  35. package/dist/{profile-CV97YuiL.js → profile-upzC-wN1.js} +4 -4
  36. package/dist/{prompt-CL8zxsmm.js → prompt-CZIQcPHy.js} +1 -1
  37. package/dist/{registry-C8I4IejN.js → registry-DM5aMhjm.js} +2 -2
  38. package/dist/{resolve-DXVAzYa3.js → resolve-sF6Lg7Ay.js} +2 -2
  39. package/dist/{runner-Die1RJ_z.js → runner-Bu2wYpK8.js} +10 -10
  40. package/dist/{sandbox-egress-signals-BZepyVh1.js → sandbox-egress-signals-Dgerhizb.js} +1 -1
  41. package/dist/{schema-BE4uso4i.js → schema-BSiwcvjm.js} +2 -2
  42. package/dist/{skills-DUcDEgDI.js → skills-CAI5s___.js} +2 -2
  43. package/dist/{spawn-o15i3tJu.js → spawn-DRK9TBgY.js} +2 -2
  44. package/dist/{task-queue-D3PLY59C.js → task-queue-iuDAqfZg.js} +3 -4
  45. package/dist/{task-runner-BMl2aM2v.js → task-runner-S3QtH38c.js} +381 -12
  46. package/dist/{thread-state-CAR6gdAm.js → thread-state-C_arVuaN.js} +5 -5
  47. package/dist/{turn-failure-response-CZyoUMkD.js → turn-failure-response-DarwGYgC.js} +9 -10
  48. package/dist/{turn-lifecycle-DPDHKi2U.js → turn-lifecycle-CdiIejXH.js} +1 -1
  49. package/dist/{turn-session-routing-Jkmmxpp-.js → turn-session-routing-DyLGd_wU.js} +1 -1
  50. package/dist/{validation-BcGGxOa6.js → validation-BMig74Zp.js} +1 -1
  51. package/dist/{version-DYcuOFQb.js → version-lhQSfLjx.js} +1 -1
  52. package/dist/version.js +1 -1
  53. package/dist/web-authorization-r2lKYKJ2.js +162 -0
  54. package/dist/web-input-BU-Fu7iq.js +7784 -0
  55. package/dist/{reporting-window-DbUqng0P.js → web-message-id-xPI_EFSC.js} +11 -1
  56. package/dist/{work-Bm90pR9P.js → work-B9koS8sW.js} +7 -8
  57. package/dist/{worker-BvHr6JhE.js → worker-C_iJSRgG.js} +5 -6
  58. package/package.json +5 -4
  59. package/dist/execution-stats-CR9CYpyj.js +0 -373
  60. package/dist/history-yaSjGMD_.d.ts +0 -65
  61. package/dist/job-queue-D2SiTrnt.js +0 -63
  62. package/dist/short-title-3wYH3ipl.js +0 -71
  63. package/dist/stats-DawHjcTw.js +0 -2758
  64. package/dist/web-authorization-B09B8A8R.js +0 -38
  65. package/dist/web-input-CGE58TpK.js +0 -899
@@ -1,28 +1,26 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-B4FMCO8f.js";
2
- import { C as resolveGatewayCredential, S as getGatewayApiKey, _ as findProviderError, b as isProviderRetryError, c as COMPACTION_SUMMARY_PREFIX, d as isCompactionSummaryText, f as renderCurrentInstruction, g as createProviderError, h as ProviderError, i as completeText, l as MODEL_HANDOFF_SUMMARY_PREFIX, m as escapeXml, n as GEN_AI_SERVER_ADDRESS, o as resolveGatewayModel, p as unwrapCurrentInstruction, r as completeObject, s as ACTIVE_TURN_COMPACTION_SUMMARY_PREFIX, t as GEN_AI_PROVIDER_NAME, u as hasCompactedConversationContext, v as getProviderErrorAttributes, x as MISSING_GATEWAY_CREDENTIALS_ERROR } from "./client-ihmkx60d.js";
3
- import { C as withLogContext, F as normalizeIdentityEmail, I as isRecord$2, 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-CcTvYQ8A.js";
2
+ import { C as resolveGatewayCredential, S as getGatewayApiKey, _ as findProviderError, b as isProviderRetryError, c as COMPACTION_SUMMARY_PREFIX, d as isCompactionSummaryText, f as renderCurrentInstruction, g as createProviderError, h as ProviderError, i as completeText, l as MODEL_HANDOFF_SUMMARY_PREFIX, m as escapeXml, n as GEN_AI_SERVER_ADDRESS, o as resolveGatewayModel, p as unwrapCurrentInstruction, r as completeObject, s as ACTIVE_TURN_COMPACTION_SUMMARY_PREFIX, t as GEN_AI_PROVIDER_NAME, u as hasCompactedConversationContext, v as getProviderErrorAttributes, x as MISSING_GATEWAY_CREDENTIALS_ERROR } from "./client-Pcxo3lGn.js";
3
+ 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-BPBx6WF-.js";
4
4
  import { m as startInactiveSpan, n as captureMessage, v as withActiveSpan } from "./sentry-CgWZM_kV.js";
5
- 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-ZYy05Y3q.js";
6
- import { h as normalizeSlackEmojiName, n as botConfig } from "./config-DMg679BW.js";
5
+ 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-hQmyZbOC.js";
6
+ import { a as getChatConfig, h as normalizeSlackEmojiName, n as botConfig } from "./config-CYvEow1F.js";
7
7
  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";
8
- import { _ as parseSlackMessageTs, g as parseSlackUserId, h as parseSlackTeamId, i as credentialUserSubjectId, l as isUserActor, m as parseSlackChannelReferenceId, n as credentialContextSchema, p as parseSlackChannelId, t as credentialContextForActor } from "./context-Bplcgypl.js";
9
- import { A as tryWorkspaceRepoCheckoutPath, B as downloadPrivateSlackFile, C as invalidateReadySnapshot, D as SANDBOX_RUNTIME, E as setWorkspaceSnapshotBuild, Ft as saveObjectAnnotations, G as isDmChannel$1, Gt as getConversationMessageSearchStore, It as createPluginAnnotations, J as withSlackRetries, Jt as getSqlExecutor, K as normalizeSlackConversationId, Kt as getConversationStore, M as resolveChannelCapabilities, O as deleteWorkspaceSnapshotBuilders, Q as readActorIdentity, S as WorkspaceValidationError, T as setWorkspaceSnapshot, U as getSlackClient, V as getFilePermalink, W as isConversationScopedChannel, Wt as getConversationEventStore, _ as deleteWorkspace, at as STOP_WATCHING_TOOL_NAME, b as listWorkspaces, bt as createPluginLogger, c as getPluginTools, dt as findMatchingWatches, fn as parseDestination, ft as listWatches, g as createWorkspace, ht as getDashboardTaskLink, it as RESOURCE_WATCH_TOOL_SOURCE, j as workspaceRepoCheckoutPath, k as getWorkspaceSnapshotBuilder, l as getPluginUserPromptContributions, lt as createWatch, mt as getDashboardConversationLink, n as applyPluginFormatMarkdown, nn as addAgentTurnUsage, nt as RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS, on as hasAgentTurnUsage, ot as cancelSubscriptions, pt as JUNIOR_THREAD_STATE_TTL_MS, qt as getDb, r as createPluginHookRunner, s as getPluginSystemPromptContributions, st as cancelWatch, tt as getSlackToolContext, u as getPlugins, ut as deliverWatch, v as getWorkspace, w as loadSnapshotsForProfile, x as updateWorkspace, y as getWorkspaceByName, z as SlackActionError } from "./agent-hooks-CRuRcOg9.js";
8
+ import { b as parseSlackMessageTs, d as parseStoredSlackActor, g as parseSlackUserId, h as parseSlackTeamId, i as credentialUserSubjectId, l as isUserActor, m as parseSlackChannelReferenceId, n as credentialContextSchema, p as parseSlackChannelId, t as credentialContextForActor } from "./context-0Et9iVSB.js";
9
+ import { $t as getDb, A as tryWorkspaceRepoCheckoutPath, B as downloadPrivateSlackFile, C as invalidateReadySnapshot, D as SANDBOX_RUNTIME, E as setWorkspaceSnapshotBuild, G as isDmChannel$1, Ht as createPluginAnnotations, J as withSlackRetries, K as normalizeSlackConversationId, Lt as matchSlackAttachments, M as resolveChannelCapabilities, O as deleteWorkspaceSnapshotBuilders, Q as readActorIdentity, Qt as getConversationStore, S as WorkspaceValidationError, St as createPluginLogger, T as setWorkspaceSnapshot, U as getSlackClient, V as getFilePermalink, Vt as saveObjectAnnotations, W as isConversationScopedChannel, Xt as getConversationEventStore, Zt as getConversationMessageSearchStore, _ as deleteWorkspace, _n as parseDestination, _t as getDashboardTaskLink, at as STOP_WATCHING_TOOL_NAME, b as listWorkspaces, c as getPluginTools, dn as hasAgentTurnUsage, dt as deliverWatch, en as getSqlExecutor, ft as findMatchingWatches, g as createWorkspace, gt as getDashboardConversationLink, ht as JUNIOR_THREAD_STATE_TTL_MS, it as RESOURCE_WATCH_TOOL_SOURCE, j as workspaceRepoCheckoutPath, k as getWorkspaceSnapshotBuilder, l as getPluginUserPromptContributions, lt as createTimerWatch, n as applyPluginFormatMarkdown, nt as RESOURCE_SUBSCRIPTION_DEFAULT_TTL_MS, ot as cancelSubscriptions, pt as listWatches, r as createPluginHookRunner, rt as RESOURCE_SUBSCRIPTION_MAX_TTL_MS, s as getPluginSystemPromptContributions, sn as addAgentTurnUsage, st as cancelWatch, tt as getSlackToolContext, u as getPlugins, ut as createWatch, v as getWorkspace, w as loadSnapshotsForProfile, x as updateWorkspace, y as getWorkspaceByName, yn as sameDestination$1, z as SlackActionError } from "./agent-hooks-bEwEJhlg.js";
10
10
  import { n as juniorDestinations } from "./destinations-BeBiJeEM.js";
11
- import { D as juniorIdentities, E as juniorConversations, a as juniorSchedulerRuns, d as AGENT_INVOCATION_MAILBOX_STATUSES, g as juniorConversationBriefs, k as juniorArtifacts, w as juniorAttachments, x as juniorConversationAnnotations } from "./executor-B9R4cHte.js";
12
- import { F as briefLinkSchema, I as briefOutcomeStatusSchema, J as instructionActors, L as conversationBriefSchema, X as sameActorIdentity, Y as instructionProvenanceFor, j as messageCardRefSchema, q as contextProvenance, z as KNOWN_CONVERSATION_EVENT_TYPES } from "./conversation-Bygi39Xj.js";
11
+ import { D as juniorIdentities, E as juniorConversations, a as juniorSchedulerRuns, c as juniorStats, d as AGENT_INVOCATION_MAILBOX_STATUSES, g as juniorConversationBriefs, k as juniorArtifacts, l as juniorEventAutomations, o as juniorSchedulerTasks, w as juniorAttachments, x as juniorConversationAnnotations } from "./executor-BXMlFrV6.js";
12
+ import { $ as contextProvenance, B as briefLinkSchema, H as conversationBriefSchema, R as isVisionImageMediaType, V as briefOutcomeStatusSchema, W as KNOWN_CONVERSATION_EVENT_TYPES, et as instructionActors, j as messageCardRefSchema, nt as sameActorIdentity, tt as instructionProvenanceFor } from "./conversation-BnhOMfEB.js";
13
13
  import { a as listReferenceFiles, c as soulPathCandidates, l as worldPathCandidates } from "./package-discovery-7a9YqLhE.js";
14
- import { a as resolveAuthTokenPlaceholder, c as fetchWithBoundedOAuthErrorBodies, d as CredentialUnavailableError, i as resolvePluginHeaderEnvRefs, l as getOAuthProviderErrorAttributes, o as resolvePluginCommandEnv, s as OAuthProviderError } from "./registry-C8I4IejN.js";
15
- import { n as pluginCatalogRuntime } from "./catalog-runtime-pv4grNNb.js";
16
- import { a as parseSkillInvocation, n as findSkillByName, r as loadSkillsByName, t as discoverSkills } from "./skills-DUcDEgDI.js";
17
- import { C as isToolResultError, D as stripRuntimeTurnContext, E as retainRuntimeTurnContext, F as loadTurnRoute, G as recordToolExecutionStarted, H as recordGuardianActionReviewed, I as openConversationProjection, K as recordTurnRoute, L as recordAgentsInstructionsUpdated, M as loadConnectedMcpProviders, N as loadConversationProjection, O as trimTrailingAssistantMessages, R as recordAttachmentsDelivered, S as isContinuablePiBoundary, T as normalizeToolNameFromResult, U as recordMcpProviderConnected, V as recordAuthorizationRequested, _ as getTerminalAssistantMessages, d as TurnSliceLimitExceededError, dt as getAgentContextCompactionTriggerTokens, f as assertTurnToolCallLimit, ft as getAgentContextInputLimitTokens, g as getPiMessageRole, h as extractAssistantText, i as abandonTurnRecord, k as TURN_CONTEXT_TAG, m as isTurnExecutionLimitExceededError, mt as piMessageFromHistoryItem, n as loadTurnCheckpoint, pt as historyItemFromPiMessage, r as saveTurnCheckpoint, t as continuableMessages, ut as estimateTextTokens, v as getUserMessageInstructionText, w as isToolResultMessage, x as isAssistantMessage, y as hasRuntimeTurnContext } from "./checkpoint-6fkAdB3g.js";
18
- import { n as generateShortTitle, r as resolveTaskTitle, t as fallbackShortTitle } from "./short-title-3wYH3ipl.js";
19
- import { A as SANDBOX_DATA_ROOT, 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$1, 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-CV97YuiL.js";
20
- import { $ as workspaceSnapshotFinishedEvent, B as createEventAutomation, C as SCHEDULED_AUTOMATION_SYSTEM_ACTOR, D as eventAutomationMatchesDestination, E as eventAutomationListToolResultSchema, F as effectiveTaskOutcomes, Gt as makeStructuredToolOutput, I as moveTaskOutcomes, Jt as ToolInputError, L as resolveTaskOutcomes, M as requireEventAutomationSlackContext, N as requireSupportedEventAutomationTrigger, O as eventAutomationToolResult, P as writableEventAutomation, Q as WORKSPACE_SNAPSHOT_READY_EVENT, R as taskOutcomeInputSchema, T as compactEventAutomation, Ut as injectContinuationToolName, V as deleteEventAutomation, W as getEventAutomation, Wt as juniorToolOutputSchema, X as getEventCatalog, Y as saveEventAutomation, Z as WORKSPACE_SNAPSHOT_FAILED_EVENT, at as normalizeCatalogEventIdentifier, c as parseSlackThreadId, ct as registeredEventTypeSchema, et as workspaceSnapshotWatch, f as createScheduledAutomation, g as listScheduledAutomationsForTeam, i as readLiveAttachment, it as hasPluginEventCatalogEntries, j as registeredEventAutomationTriggerSchema, k as eventAutomationToolResultSchema, lt as registeredResourceTypeSchema, nt as eventNamespaceSchema, ot as pluginSupportsEvent, pt as appendAndEnqueueInboundMessage, q as listEventAutomationsForTeam, qt as buildDeterministicTurnId, rt as hasEventCatalogEntries, s as storeAttachments, st as registeredEventMatchSchema, t as incrementStat, tt as eventGuidance, ut as requireSupportedEventMatch, v as readScheduledAutomation, y as saveScheduledAutomation } from "./stats-DawHjcTw.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-BS4vPJmu.js";
22
- import { r as EVENT_AUTHOR_ID } from "./prompt-CL8zxsmm.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-DXVAzYa3.js";
24
- import { t as getVercelConversationWorkQueue } from "./vercel-queue-DIkVcfK4.js";
25
- import { a as getTurnRequestDeadline, n as sendNextWorkspaceSnapshotJob, r as sendWorkspaceSnapshotJob } from "./job-queue-D2SiTrnt.js";
14
+ import { a as resolveAuthTokenPlaceholder, c as fetchWithBoundedOAuthErrorBodies, d as CredentialUnavailableError, i as resolvePluginHeaderEnvRefs, l as getOAuthProviderErrorAttributes, o as resolvePluginCommandEnv, s as OAuthProviderError } from "./registry-DM5aMhjm.js";
15
+ import { n as pluginCatalogRuntime } from "./catalog-runtime-Cw9V2jpn.js";
16
+ import { a as parseSkillInvocation, n as findSkillByName, r as loadSkillsByName, t as discoverSkills } from "./skills-CAI5s___.js";
17
+ import { $ as fallbackShortTitle, C as isToolResultError, D as stripRuntimeTurnContext, E as retainRuntimeTurnContext, F as loadTurnRoute, G as recordToolExecutionStarted, H as recordGuardianActionReviewed, I as openConversationProjection, K as recordTurnRoute, L as recordAgentsInstructionsUpdated, M as loadConnectedMcpProviders, N as loadConversationProjection, O as trimTrailingAssistantMessages, R as recordAttachmentsDelivered, S as isContinuablePiBoundary, T as normalizeToolNameFromResult, U as recordMcpProviderConnected, V as recordAuthorizationRequested, _ as getTerminalAssistantMessages, _t as piMessageFromHistoryItem, d as TurnSliceLimitExceededError, et as generateShortTitle, f as assertTurnToolCallLimit, g as getPiMessageRole, gt as historyItemFromPiMessage, h as extractAssistantText, ht as getAgentContextInputLimitTokens, k as TURN_CONTEXT_TAG, m as isTurnExecutionLimitExceededError, mt as getAgentContextCompactionTriggerTokens, n as loadTurnCheckpoint, pt as estimateTextTokens, r as saveTurnCheckpoint, t as continuableMessages, tt as resolveTaskTitle, v as getUserMessageInstructionText, w as isToolResultMessage, x as isAssistantMessage, y as hasRuntimeTurnContext } from "./checkpoint-oZYD3-Ar.js";
18
+ 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-upzC-wN1.js";
19
+ import { n as canReusePendingAuthLink, t as abandonReplacedPendingAuth } from "./pending-auth-CHUjr_6d.js";
20
+ 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-Cq1GaLIP.js";
21
+ import { r as EVENT_AUTHOR_ID } from "./prompt-CZIQcPHy.js";
22
+ 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-sF6Lg7Ay.js";
23
+ import { a as getVercelConversationWorkQueue, f as getTurnRequestDeadline, n as sendNextWorkspaceSnapshotJob, r as sendWorkspaceSnapshotJob } from "./job-queue-AI0_QAgc.js";
26
24
  import { n as privateTraceResultAttributes } from "./private-trace-result-BLvBjy_e.js";
27
25
  import { streamSimple } from "@earendil-works/pi-ai/compat";
28
26
  import { createGatewayProvider } from "@ai-sdk/gateway";
@@ -32,8 +30,8 @@ import { isDeepStrictEqual } from "node:util";
32
30
  import { AsyncLocalStorage } from "node:async_hooks";
33
31
  import { z } from "zod";
34
32
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
35
- import { and, desc, eq, gt, gte, inArray, isNull, lt, ne, notExists, or, sql } from "drizzle-orm";
36
- import { EgressAuthRequired, EgressPolicyDenied, PluginToolInputError, actorSchema, destinationSchema, eventSchema, missingToolAnnotationKeys, ownedObjectAnnotationSchema, pluginAuthorizationSchema, pluginCredentialHeaderTransformSchema, pluginCredentialResultSchema, pluginGrantSchema, pluginProviderAccountSchema, pluginStoredTokensSchema, sourceSchema, stableEventMatchKey, watchResultSchema } from "@sentry/junior-plugin-api";
33
+ import { and, asc, desc, eq, gt, gte, inArray, isNotNull, isNull, lt, lte, ne, notExists, notInArray, or, sql } from "drizzle-orm";
34
+ import { EgressAuthRequired, EgressPolicyDenied, PluginToolInputError, actorSchema, actorUserIdSchema, destinationSchema, destinationVisibilitySchema, eventMatchSchema, eventMatches, eventSchema, eventTypeSchema, missingToolAnnotationKeys, normalizeEventIdentifier, ownedObjectAnnotationSchema, pluginAuthorizationSchema, pluginCredentialHeaderTransformSchema, pluginCredentialResultSchema, pluginGrantSchema, pluginProviderAccountSchema, pluginStoredTokensSchema, slackDestinationSchema, sourceSchema, stableEventMatchKey, taskOutcomeSchema, watchResultSchema } from "@sentry/junior-plugin-api";
37
35
  import { alias } from "drizzle-orm/pg-core";
38
36
  import { Agent, estimateContextTokens } from "@earendil-works/pi-agent-core";
39
37
  import fs from "node:fs/promises";
@@ -219,6 +217,15 @@ var SkillSandbox = class {
219
217
  }
220
218
  };
221
219
  //#endregion
220
+ //#region src/chat/tools/execution/tool-input-error.ts
221
+ /** Thrown when a tool fails due to invalid model/user input, not a system error. */
222
+ var ToolInputError = class extends Error {
223
+ constructor(message, options) {
224
+ super(message, options);
225
+ this.name = "ToolInputError";
226
+ }
227
+ };
228
+ //#endregion
222
229
  //#region src/chat/tools/sandbox/file-utils.ts
223
230
  const MAX_TEXT_CHARS = 6e4;
224
231
  const RIPGREP_EXCLUDED_GLOBS = ["!**/.git/**", "!**/node_modules/**"];
@@ -255,8 +262,7 @@ function isMissingPathError(error) {
255
262
  }
256
263
  /** Reject an expected missing search/list path through the tool-error channel. */
257
264
  function missingPathSearchResult(params) {
258
- const textPath = params.displayPath ?? params.missingPath ?? params.path;
259
- throw new ToolInputError(`Path not found: ${textPath}`);
265
+ throw new ToolInputError(`Path not found: ${params.displayPath ?? params.missingPath ?? params.path}`);
260
266
  }
261
267
  function escapeRegExp(value) {
262
268
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -839,6 +845,49 @@ function getDiscardedRetryUsage(messages, retryMessages) {
839
845
  return hasAgentTurnUsage(usage) ? usage : void 0;
840
846
  }
841
847
  //#endregion
848
+ //#region src/chat/tool-support/structured-result.ts
849
+ const juniorToolContinuationSchema = z.object({
850
+ arguments: z.record(z.string(), z.unknown()),
851
+ reason: z.string().min(1).optional()
852
+ }).strict();
853
+ const juniorToolBoundContinuationSchema = juniorToolContinuationSchema.extend({ tool_name: z.string().min(1) });
854
+ /** Shared optional fields for canonical Junior tool outputs. */
855
+ const juniorToolOutputSchema = z.object({
856
+ target: z.string().min(1).optional(),
857
+ truncated: z.boolean().optional(),
858
+ /** True when this attempt did not finish before its time budget. */
859
+ timed_out: z.boolean().optional(),
860
+ continuation: juniorToolContinuationSchema.optional()
861
+ }).passthrough();
862
+ function sortJsonValue(value) {
863
+ if (Array.isArray(value)) return value.map(sortJsonValue);
864
+ if (!value || typeof value !== "object") return value;
865
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, sortJsonValue(item)]));
866
+ }
867
+ function isRecord$2(value) {
868
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
869
+ }
870
+ /** Bind continuation metadata to the exposed runtime tool name. */
871
+ function injectContinuationToolName(details, toolName) {
872
+ if (!isRecord$2(details) || !isRecord$2(details.continuation)) return details;
873
+ return {
874
+ ...details,
875
+ continuation: juniorToolBoundContinuationSchema.parse({
876
+ ...details.continuation,
877
+ tool_name: toolName
878
+ })
879
+ };
880
+ }
881
+ function makeStructuredToolOutput(details, options = {}) {
882
+ return {
883
+ content: [{
884
+ type: "text",
885
+ text: JSON.stringify(sortJsonValue(details))
886
+ }, ...options.content ?? []],
887
+ details
888
+ };
889
+ }
890
+ //#endregion
842
891
  //#region src/chat/tool-support/timed-out-tool-result.ts
843
892
  /**
844
893
  * Project a host-preempted tool attempt onto the normal tool-result shape.
@@ -1796,19 +1845,6 @@ function buildTurnContextPrompt(params) {
1796
1845
  ].filter((section) => Boolean(section)).join("\n\n");
1797
1846
  }
1798
1847
  //#endregion
1799
- //#region src/chat/attachments/media.ts
1800
- const VISION_IMAGE_MEDIA_TYPES = /* @__PURE__ */ new Set([
1801
- "image/gif",
1802
- "image/jpeg",
1803
- "image/png",
1804
- "image/webp"
1805
- ]);
1806
- /** Report whether a media type is a raster image accepted by vision models. */
1807
- function isVisionImageMediaType(mediaType) {
1808
- const normalized = mediaType.split(";", 1)[0]?.trim().toLowerCase();
1809
- return normalized ? VISION_IMAGE_MEDIA_TYPES.has(normalized) : false;
1810
- }
1811
- //#endregion
1812
1848
  //#region src/chat/agent/prompt.ts
1813
1849
  /**
1814
1850
  * Per-run prompt assembly.
@@ -1941,6 +1977,14 @@ function buildUserTurnInput(args) {
1941
1977
  });
1942
1978
  continue;
1943
1979
  }
1980
+ if (attachment.attachmentId) userContentParts.push({
1981
+ type: "text",
1982
+ text: `Stored attachment: ${JSON.stringify({
1983
+ attachment_id: attachment.attachmentId,
1984
+ filename: attachment.filename,
1985
+ media_type: attachment.mediaType
1986
+ })}. Use loadAttachment to access the original file.`
1987
+ });
1944
1988
  if (isVisionImageMediaType(attachment.mediaType)) {
1945
1989
  if (!attachment.data) throw new Error("Image attachment is missing image data");
1946
1990
  userContentParts.push({
@@ -2208,7 +2252,7 @@ var StateAdapterTokenStore = class {
2208
2252
  await this.state.releaseLock(lock);
2209
2253
  }
2210
2254
  if (Date.now() >= deadline) throw new Error(`Could not acquire OAuth token refresh lock`);
2211
- await sleep$1(REFRESH_LOCK_RETRY_MS);
2255
+ await sleep$2(REFRESH_LOCK_RETRY_MS);
2212
2256
  }
2213
2257
  }
2214
2258
  };
@@ -2641,7 +2685,8 @@ const payload = z.strictObject({
2641
2685
  display_type: z.string().optional(),
2642
2686
  product_name: z.string().optional()
2643
2687
  }),
2644
- custom_fields: z.array(customField).optional()
2688
+ custom_fields: z.array(customField).optional(),
2689
+ display_order: z.array(z.string()).optional()
2645
2690
  });
2646
2691
  /** Limit IDs to Junior's safe subset; Slack silently rejects JSON punctuation. */
2647
2692
  const slackExternalRefIdSchema = z.string().min(1).regex(/^[A-Za-z0-9_-]+$/, "Unsupported Slack external reference ID");
@@ -2800,11 +2845,11 @@ async function addReactionToMessage(input) {
2800
2845
  const emoji = normalizeSlackEmojiName(input.emoji);
2801
2846
  if (!emoji) throw new Error("Slack reaction requires a valid emoji alias name");
2802
2847
  try {
2803
- await withSlackRetries(() => getSlackClient().reactions.add({
2848
+ await withSlackRetries(() => getSlackClient(input.timeoutMs === void 0 ? void 0 : { timeoutMs: input.timeoutMs }).reactions.add({
2804
2849
  channel: channelId,
2805
2850
  timestamp,
2806
2851
  name: emoji
2807
- }), 3, {
2852
+ }), input.timeoutMs === void 0 ? 3 : 1, {
2808
2853
  action: "reactions.add",
2809
2854
  idempotent: true,
2810
2855
  spanAttributes: {
@@ -3077,7 +3122,7 @@ function optionalString(value) {
3077
3122
  }
3078
3123
  /** Parse OAuth callback state that was persisted before a provider redirect. */
3079
3124
  function parseOAuthStatePayload(value) {
3080
- if (!isRecord$2(value)) return;
3125
+ if (!isRecord$3(value)) return;
3081
3126
  if (typeof value.userId !== "string" || typeof value.provider !== "string") return;
3082
3127
  const actor = value.actor === void 0 ? void 0 : actorSchema.safeParse(value.actor);
3083
3128
  if (value.actor !== void 0 && (!actor || !actor.success)) return;
@@ -3699,12 +3744,12 @@ const automatedTurnLimitStateSchema = z.object({
3699
3744
  paused: z.boolean(),
3700
3745
  updatedAtMs: z.number().finite().nonnegative()
3701
3746
  }).strict();
3702
- function sleep(ms) {
3747
+ function sleep$1(ms) {
3703
3748
  return new Promise((resolve) => {
3704
3749
  setTimeout(resolve, ms).unref?.();
3705
3750
  });
3706
3751
  }
3707
- function conversationKey(conversationId) {
3752
+ function conversationKey$1(conversationId) {
3708
3753
  return `${AUTOMATED_TURN_LIMIT_PREFIX}:conversation:${conversationId}`;
3709
3754
  }
3710
3755
  function lockKey(key) {
@@ -3719,7 +3764,7 @@ async function acquireConversationLock(state, key, waitDeadlineMs = Date.now() +
3719
3764
  const lock = await state.acquireLock(lockKey(key), AUTOMATED_TURN_LIMIT_LOCK_TTL_MS);
3720
3765
  if (lock) return lock;
3721
3766
  if (Date.now() >= waitDeadlineMs) throw new Error(`Could not acquire automated turn limit lock for ${key}`);
3722
- await sleep(AUTOMATED_TURN_LIMIT_LOCK_RETRY_MS);
3767
+ await sleep$1(AUTOMATED_TURN_LIMIT_LOCK_RETRY_MS);
3723
3768
  }
3724
3769
  }
3725
3770
  async function withConversationLock(state, key, callback) {
@@ -3756,7 +3801,7 @@ async function admitAutomatedTurn(args) {
3756
3801
  const state = args.state ?? getStateAdapter();
3757
3802
  await state.connect();
3758
3803
  const nowMs = args.nowMs ?? Date.now();
3759
- const key = conversationKey(args.conversationId);
3804
+ const key = conversationKey$1(args.conversationId);
3760
3805
  return await withConversationLock(state, key, async () => {
3761
3806
  const current = parseState(await state.get(key)) ?? emptyState(nowMs);
3762
3807
  if (current.paused || current.consecutiveAutomatedTurns >= args.maxTurns) {
@@ -3786,7 +3831,7 @@ async function countAutomatedTurn(args) {
3786
3831
  const state = args.state ?? getStateAdapter();
3787
3832
  await state.connect();
3788
3833
  const nowMs = args.nowMs ?? Date.now();
3789
- const key = conversationKey(args.conversationId);
3834
+ const key = conversationKey$1(args.conversationId);
3790
3835
  return await withConversationLock(state, key, async () => {
3791
3836
  const current = parseState(await state.get(key)) ?? emptyState(nowMs);
3792
3837
  const consecutiveAutomatedTurns = current.consecutiveAutomatedTurns + 1;
@@ -3814,7 +3859,7 @@ async function clearAutomatedTurnLimitNoticeClaim(args) {
3814
3859
  const state = args.state ?? getStateAdapter();
3815
3860
  await state.connect();
3816
3861
  const nowMs = args.nowMs ?? Date.now();
3817
- const key = conversationKey(args.conversationId);
3862
+ const key = conversationKey$1(args.conversationId);
3818
3863
  await withConversationLock(state, key, async () => {
3819
3864
  const current = parseState(await state.get(key));
3820
3865
  if (!current || current.noticePostedAtMs === void 0) return;
@@ -3829,7 +3874,7 @@ async function clearAutomatedTurnLimitNoticeClaim(args) {
3829
3874
  async function resetAutomatedTurnLimit(args) {
3830
3875
  const state = args.state ?? getStateAdapter();
3831
3876
  await state.connect();
3832
- const key = conversationKey(args.conversationId);
3877
+ const key = conversationKey$1(args.conversationId);
3833
3878
  await withConversationLock(state, key, async () => {
3834
3879
  await state.delete(key);
3835
3880
  });
@@ -3952,6 +3997,1577 @@ function renderTaskInput(args) {
3952
3997
  return lines.join("\n");
3953
3998
  }
3954
3999
  //#endregion
4000
+ //#region src/chat/task-execution/state.ts
4001
+ /**
4002
+ * Expiring conversation mailbox and execution index store.
4003
+ *
4004
+ * The conversation record owns pending inbound work, execution status, and the
4005
+ * lease. `conversation:active` feeds heartbeat recovery and contains every
4006
+ * non-idle conversation until the lease/status path makes it idle.
4007
+ * `conversation:by-activity` supports legacy SQL import only; SQL owns durable
4008
+ * conversation metadata in configured runtime.
4009
+ */
4010
+ const CONVERSATION_PREFIX = "junior:conversation:v2";
4011
+ const CONVERSATION_SCHEMA_VERSION = 2;
4012
+ const CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH = 1e4;
4013
+ const CONVERSATION_INDEX_LOCK_TTL_MS = 1e4;
4014
+ const CONVERSATION_INDEX_LOCK_WAIT_MS = 2e3;
4015
+ const CONVERSATION_INDEX_LOCK_RETRY_MS = 25;
4016
+ const CONVERSATION_MUTATION_LOCK_TTL_MS = 1e4;
4017
+ const CONVERSATION_MUTATION_WAIT_MS = 1e4;
4018
+ const CONVERSATION_MUTATION_RETRY_MS = 25;
4019
+ var InvalidConversationRecordError = class extends Error {
4020
+ constructor(conversationId) {
4021
+ super(`Conversation record is invalid for ${conversationId}`);
4022
+ this.name = "InvalidConversationRecordError";
4023
+ }
4024
+ };
4025
+ /** Return whether an error means a stored conversation record failed validation. */
4026
+ function isInvalidConversationRecordError(error) {
4027
+ return error instanceof InvalidConversationRecordError;
4028
+ }
4029
+ var ConversationMutationFencedError = class extends Error {
4030
+ constructor(conversationId) {
4031
+ super(`Conversation mutation lock was lost before write for ${conversationId}`);
4032
+ this.name = "ConversationMutationFencedError";
4033
+ }
4034
+ };
4035
+ const CONVERSATION_BY_ACTIVITY_INDEX_KEY = `${CONVERSATION_PREFIX}:by-activity`;
4036
+ const CONVERSATION_ACTIVE_INDEX_KEY = `${CONVERSATION_PREFIX}:active`;
4037
+ const CONVERSATION_WORK_LEASE_TTL_MS = 9e4;
4038
+ const CONVERSATION_WORK_CHECK_IN_INTERVAL_MS = 15e3;
4039
+ const CONVERSATION_WORK_STALE_ENQUEUE_MS = 6e4;
4040
+ const inboundMessageSourceSchema = z.enum([
4041
+ "api",
4042
+ "internal",
4043
+ "local",
4044
+ "plugin",
4045
+ "event",
4046
+ "scheduler",
4047
+ "slack",
4048
+ "web"
4049
+ ]);
4050
+ const agentInputSchema = z.object({
4051
+ attachments: z.array(z.unknown()).optional(),
4052
+ authorId: z.string().min(1).optional(),
4053
+ metadata: z.record(z.string(), z.unknown()).optional(),
4054
+ text: z.string()
4055
+ }).strict().refine((input) => input.text.trim() || input.attachments?.length, { message: "agent input requires text or attachments" });
4056
+ /** Durable delivery modes for pending inbound mailbox work. */
4057
+ const inboundMessageDeliverySchema = z.enum(["defer", "interrupt"]);
4058
+ /** Redis mailbox shape kept for deployed workers that still require this field. */
4059
+ const storedInboundMessageSchema = z.object({
4060
+ attemptCount: z.number().finite().optional(),
4061
+ conversationId: z.string().refine((value) => value.trim().length > 0),
4062
+ createdAtMs: z.number().finite(),
4063
+ delivery: inboundMessageDeliverySchema,
4064
+ destination: destinationSchema.optional(),
4065
+ inboundMessageId: z.string().refine((value) => value.trim().length > 0),
4066
+ injectedAtMs: z.number().finite().optional(),
4067
+ input: agentInputSchema,
4068
+ receivedAtMs: z.number().finite(),
4069
+ source: inboundMessageSourceSchema
4070
+ }).strict().extend({ publishExternally: z.boolean() });
4071
+ function conversationKey(conversationId) {
4072
+ return `${CONVERSATION_PREFIX}:${conversationId}`;
4073
+ }
4074
+ function conversationStopKey(conversationId) {
4075
+ return `${CONVERSATION_PREFIX}:stop:${conversationId}`;
4076
+ }
4077
+ function indexLockKey(indexKey) {
4078
+ return `${indexKey}:lock`;
4079
+ }
4080
+ function mutationLockKey(conversationId) {
4081
+ return `${CONVERSATION_PREFIX}:mutation:${conversationId}`;
4082
+ }
4083
+ function now$1() {
4084
+ return Date.now();
4085
+ }
4086
+ function compareMessages(left, right) {
4087
+ return left.createdAtMs - right.createdAtMs || left.receivedAtMs - right.receivedAtMs || left.inboundMessageId.localeCompare(right.inboundMessageId);
4088
+ }
4089
+ function inputHasAttachments(input) {
4090
+ return Array.isArray(input.attachments) && input.attachments.length > 0;
4091
+ }
4092
+ /**
4093
+ * Upgrade a still-pending payload when a duplicate source event carries file
4094
+ * attachments the stored copy lacks (Slack `app_mention`/`message` twins),
4095
+ * keeping the stored ordering timestamps and delivery attempts.
4096
+ */
4097
+ function upgradedPendingMessage(stored, duplicate) {
4098
+ const promotesInterrupt = stored.delivery === "defer" && duplicate.delivery === "interrupt";
4099
+ const delivery = promotesInterrupt ? "interrupt" : stored.delivery;
4100
+ const input = promotesInterrupt ? {
4101
+ ...duplicate.input,
4102
+ ...inputHasAttachments(duplicate.input) || !inputHasAttachments(stored.input) ? void 0 : { attachments: stored.input.attachments }
4103
+ } : inputHasAttachments(duplicate.input) && !inputHasAttachments(stored.input) ? duplicate.input : stored.input;
4104
+ if (input === stored.input && delivery === stored.delivery) return stored;
4105
+ return {
4106
+ ...stored,
4107
+ delivery,
4108
+ input
4109
+ };
4110
+ }
4111
+ function compareIndexDescending(left, right) {
4112
+ return right.score - left.score || right.conversationId.localeCompare(left.conversationId);
4113
+ }
4114
+ function compareIndexAscending(left, right) {
4115
+ return left.score - right.score || left.conversationId.localeCompare(right.conversationId);
4116
+ }
4117
+ function uniqueStrings(values) {
4118
+ return [...new Set(values)];
4119
+ }
4120
+ function normalizeSource(value) {
4121
+ const parsed = inboundMessageSourceSchema.safeParse(value);
4122
+ return parsed.success ? parsed.data : void 0;
4123
+ }
4124
+ function normalizeExecutionStatus(value) {
4125
+ if (value === "paused") return value;
4126
+ if (value === "failed" || value === "idle" || value === "pending" || value === "running") return value;
4127
+ }
4128
+ function normalizeMessage(value) {
4129
+ const parsed = storedInboundMessageSchema.safeParse(value);
4130
+ if (!parsed.success) return;
4131
+ const { publishExternally: _publishExternally, ...message } = parsed.data;
4132
+ return message;
4133
+ }
4134
+ function storeMessage(message, publishExternally) {
4135
+ return {
4136
+ ...message,
4137
+ publishExternally
4138
+ };
4139
+ }
4140
+ /** Whether this is the final attempt before an unacked message is dead-lettered. */
4141
+ function isFinalAttempt(message) {
4142
+ return (message.attemptCount ?? 0) >= 4;
4143
+ }
4144
+ function normalizeActor(value) {
4145
+ return parseStoredSlackActor(value);
4146
+ }
4147
+ function normalizeLease(value) {
4148
+ if (!isRecord$3(value)) return;
4149
+ const token = toOptionalString(value.token);
4150
+ const acquiredAtMs = toOptionalNumber(value.acquiredAtMs);
4151
+ const lastCheckInAtMs = toOptionalNumber(value.lastCheckInAtMs);
4152
+ const expiresAtMs = toOptionalNumber(value.expiresAtMs);
4153
+ if (!token || typeof acquiredAtMs !== "number" || typeof lastCheckInAtMs !== "number" || typeof expiresAtMs !== "number") return;
4154
+ return {
4155
+ token,
4156
+ acquiredAtMs,
4157
+ lastCheckInAtMs,
4158
+ expiresAtMs
4159
+ };
4160
+ }
4161
+ function normalizeStop(value) {
4162
+ if (!isRecord$3(value)) return;
4163
+ const inboundMessageIds = Array.isArray(value.inboundMessageIds) ? uniqueStrings(value.inboundMessageIds.filter((id) => typeof id === "string")) : [];
4164
+ const runId = toOptionalString(value.runId);
4165
+ return runId ? {
4166
+ inboundMessageIds,
4167
+ runId
4168
+ } : void 0;
4169
+ }
4170
+ /** Decode execution state and repair idle records that still own work. */
4171
+ function normalizeExecution(conversationId, value) {
4172
+ if (!isRecord$3(value)) return;
4173
+ const status = normalizeExecutionStatus(value.status);
4174
+ if (!status) return;
4175
+ const pendingMessages = [];
4176
+ if (Array.isArray(value.pendingMessages)) {
4177
+ for (const rawMessage of value.pendingMessages) {
4178
+ const message = normalizeMessage(rawMessage);
4179
+ if (!message || message.conversationId !== conversationId) return;
4180
+ if (message.injectedAtMs !== void 0) continue;
4181
+ pendingMessages.push(message);
4182
+ }
4183
+ pendingMessages.sort(compareMessages);
4184
+ }
4185
+ const inboundMessageIds = Array.isArray(value.inboundMessageIds) ? uniqueStrings(value.inboundMessageIds.map((id) => typeof id === "string" ? id : void 0).filter((id) => Boolean(id))) : [];
4186
+ const lease = normalizeLease(value.lease);
4187
+ const runId = toOptionalString(value.runId);
4188
+ return {
4189
+ status: status === "idle" && lease ? "running" : status === "idle" && pendingMessages.length > 0 ? "pending" : status,
4190
+ inboundMessageIds: uniqueStrings([...inboundMessageIds, ...pendingMessages.map((message) => message.inboundMessageId)]),
4191
+ pendingCount: pendingMessages.length,
4192
+ pendingMessages,
4193
+ lease,
4194
+ lastCheckpointAtMs: toOptionalNumber(value.lastCheckpointAtMs),
4195
+ lastEnqueuedAtMs: toOptionalNumber(value.lastEnqueuedAtMs),
4196
+ lastProgressAtMs: toOptionalNumber(value.lastProgressAtMs),
4197
+ retryCount: toOptionalNumber(value.retryCount),
4198
+ runId,
4199
+ updatedAtMs: toOptionalNumber(value.updatedAtMs)
4200
+ };
4201
+ }
4202
+ /** Decode current conversation records. */
4203
+ function normalizeConversation(conversationId, value) {
4204
+ if (!isRecord$3(value) || value.schemaVersion !== CONVERSATION_SCHEMA_VERSION) return;
4205
+ const storedConversationId = toOptionalString(value.conversationId);
4206
+ const createdAtMs = toOptionalNumber(value.createdAtMs);
4207
+ const lastActivityAtMs = toOptionalNumber(value.lastActivityAtMs);
4208
+ const updatedAtMs = toOptionalNumber(value.updatedAtMs);
4209
+ const execution = normalizeExecution(conversationId, value.execution);
4210
+ const destination = value.destination === void 0 ? void 0 : parseDestination(value.destination);
4211
+ if (storedConversationId !== conversationId || typeof createdAtMs !== "number" || typeof lastActivityAtMs !== "number" || typeof updatedAtMs !== "number" || !execution || value.destination !== void 0 && !destination) return;
4212
+ if (execution.pendingMessages.length > 0 && execution.pendingMessages.some((message) => {
4213
+ if (!message.destination) return false;
4214
+ return !destination || !sameDestination$1(message.destination, destination);
4215
+ })) return;
4216
+ return {
4217
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
4218
+ conversationId,
4219
+ createdAtMs,
4220
+ lastActivityAtMs,
4221
+ updatedAtMs,
4222
+ execution,
4223
+ ...destination ? { destination } : void 0,
4224
+ ...toOptionalString(value.title) ? { title: toOptionalString(value.title) } : void 0,
4225
+ ...toOptionalString(value.channelName) ? { channelName: toOptionalString(value.channelName) } : void 0,
4226
+ ...normalizeActor(value.actor) ? { actor: normalizeActor(value.actor) } : void 0,
4227
+ ...normalizeSource(value.source) ? { source: normalizeSource(value.source) } : void 0
4228
+ };
4229
+ }
4230
+ function emptyConversation(args) {
4231
+ return {
4232
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
4233
+ conversationId: args.conversationId,
4234
+ createdAtMs: args.nowMs,
4235
+ lastActivityAtMs: args.nowMs,
4236
+ updatedAtMs: args.nowMs,
4237
+ ...args.destination ? { destination: args.destination } : void 0,
4238
+ ...args.source ? { source: args.source } : void 0,
4239
+ execution: {
4240
+ status: "idle",
4241
+ inboundMessageIds: [],
4242
+ pendingCount: 0,
4243
+ pendingMessages: [],
4244
+ updatedAtMs: args.nowMs
4245
+ }
4246
+ };
4247
+ }
4248
+ function isLeaseActive(lease, nowMs) {
4249
+ return Boolean(lease && lease.expiresAtMs > nowMs);
4250
+ }
4251
+ function pendingMessages(conversation) {
4252
+ return [...conversation.execution.pendingMessages].sort(compareMessages);
4253
+ }
4254
+ function isRunnableStatus(status) {
4255
+ return status !== "failed" && status !== "idle";
4256
+ }
4257
+ function hasRunnableWork(conversation) {
4258
+ return isRunnableStatus(conversation.execution.status) || pendingMessages(conversation).length > 0;
4259
+ }
4260
+ function executionWithPendingMessages(execution, pending) {
4261
+ const pendingMessages = [...pending].sort(compareMessages);
4262
+ const status = execution.status === "idle" && execution.lease ? "running" : execution.status === "idle" && pendingMessages.length > 0 ? "pending" : execution.status;
4263
+ return {
4264
+ ...execution,
4265
+ status,
4266
+ inboundMessageIds: uniqueStrings([...execution.inboundMessageIds, ...pendingMessages.map((message) => message.inboundMessageId)]),
4267
+ pendingMessages,
4268
+ pendingCount: pendingMessages.length
4269
+ };
4270
+ }
4271
+ function withExecutionUpdate(conversation, execution, nowMs) {
4272
+ return {
4273
+ ...conversation,
4274
+ updatedAtMs: nowMs,
4275
+ execution: {
4276
+ ...executionWithPendingMessages(execution, execution.pendingMessages),
4277
+ updatedAtMs: nowMs
4278
+ }
4279
+ };
4280
+ }
4281
+ async function getConnectedState(stateAdapter) {
4282
+ const state = stateAdapter ?? getStateAdapter();
4283
+ await state.connect();
4284
+ return state;
4285
+ }
4286
+ async function sleep(ms) {
4287
+ await new Promise((resolve) => {
4288
+ setTimeout(resolve, ms).unref?.();
4289
+ });
4290
+ }
4291
+ async function withIndexLock(state, indexKey, callback) {
4292
+ const startedAtMs = now$1();
4293
+ let lock;
4294
+ while (true) {
4295
+ lock = await state.acquireLock(indexLockKey(indexKey), CONVERSATION_INDEX_LOCK_TTL_MS);
4296
+ if (lock) break;
4297
+ if (now$1() - startedAtMs >= CONVERSATION_INDEX_LOCK_WAIT_MS) throw new Error(`Could not acquire conversation index lock for ${indexKey}`);
4298
+ await sleep(CONVERSATION_INDEX_LOCK_RETRY_MS);
4299
+ }
4300
+ try {
4301
+ return await callback();
4302
+ } finally {
4303
+ await state.releaseLock(lock);
4304
+ }
4305
+ }
4306
+ function normalizeIndexEntry(value) {
4307
+ if (!isRecord$3(value)) return;
4308
+ const conversationId = toOptionalString(value.conversationId);
4309
+ const score = toOptionalNumber(value.score);
4310
+ if (!conversationId || typeof score !== "number") return;
4311
+ return {
4312
+ conversationId,
4313
+ score
4314
+ };
4315
+ }
4316
+ function uniqueIndexEntries(value) {
4317
+ if (!Array.isArray(value)) return [];
4318
+ const entries = /* @__PURE__ */ new Map();
4319
+ for (const item of value) {
4320
+ const entry = normalizeIndexEntry(item);
4321
+ if (!entry) continue;
4322
+ const existing = entries.get(entry.conversationId);
4323
+ if (!existing || entry.score > existing.score) entries.set(entry.conversationId, entry);
4324
+ }
4325
+ return [...entries.values()];
4326
+ }
4327
+ function retainedIndexEntries(indexKey, entries) {
4328
+ if (indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) return entries.sort(compareIndexDescending).slice(0, CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH);
4329
+ if (indexKey === CONVERSATION_ACTIVE_INDEX_KEY) return entries.sort(compareIndexAscending);
4330
+ throw new Error(`Unknown conversation index ${indexKey}`);
4331
+ }
4332
+ function redisIndexKey(indexKey) {
4333
+ const prefix = getChatConfig().state.keyPrefix;
4334
+ return [...prefix ? [prefix] : [], indexKey].join(":");
4335
+ }
4336
+ function parseRedisIndexEntries(values) {
4337
+ if (!Array.isArray(values)) return [];
4338
+ const entries = [];
4339
+ for (let index = 0; index < values.length; index += 2) {
4340
+ const conversationId = toOptionalString(values[index]);
4341
+ const score = typeof values[index + 1] === "number" ? values[index + 1] : Number(values[index + 1]);
4342
+ if (!conversationId || !Number.isFinite(score)) continue;
4343
+ entries.push({
4344
+ conversationId,
4345
+ score
4346
+ });
4347
+ }
4348
+ return entries;
4349
+ }
4350
+ function redisConversationIndexStore(client) {
4351
+ const upsertBoundedActivityScript = `
4352
+ redis.call("ZADD", KEYS[1], ARGV[1], ARGV[2])
4353
+ redis.call("PEXPIRE", KEYS[1], ARGV[3])
4354
+ local extra = redis.call("ZCARD", KEYS[1]) - tonumber(ARGV[4])
4355
+ if extra > 0 then
4356
+ redis.call("ZREMRANGEBYRANK", KEYS[1], 0, extra - 1)
4357
+ end
4358
+ return 1
4359
+ `;
4360
+ return {
4361
+ async list(args) {
4362
+ const key = redisIndexKey(args.indexKey);
4363
+ const limit = args.limit;
4364
+ const offset = Math.max(0, args.offset ?? 0);
4365
+ if (limit === 0) return [];
4366
+ return parseRedisIndexEntries(args.scoreMax !== void 0 ? await client.sendCommand([
4367
+ "ZRANGEBYSCORE",
4368
+ key,
4369
+ "-inf",
4370
+ String(args.scoreMax),
4371
+ "WITHSCORES",
4372
+ ...limit !== void 0 || offset > 0 ? [
4373
+ "LIMIT",
4374
+ String(offset),
4375
+ String(limit ?? 1e9)
4376
+ ] : []
4377
+ ]) : await client.sendCommand([
4378
+ args.order === "asc" ? "ZRANGE" : "ZREVRANGE",
4379
+ key,
4380
+ String(offset),
4381
+ String(limit === void 0 ? -1 : offset + Math.max(0, limit - 1)),
4382
+ "WITHSCORES"
4383
+ ]));
4384
+ },
4385
+ async remove(args) {
4386
+ await client.sendCommand([
4387
+ "ZREM",
4388
+ redisIndexKey(args.indexKey),
4389
+ args.conversationId
4390
+ ]);
4391
+ },
4392
+ async upsert(args) {
4393
+ const key = redisIndexKey(args.indexKey);
4394
+ if (args.indexKey === CONVERSATION_BY_ACTIVITY_INDEX_KEY) {
4395
+ await client.sendCommand([
4396
+ "EVAL",
4397
+ upsertBoundedActivityScript,
4398
+ "1",
4399
+ key,
4400
+ String(args.score),
4401
+ args.conversationId,
4402
+ String(JUNIOR_THREAD_STATE_TTL_MS),
4403
+ String(CONVERSATION_ACTIVITY_INDEX_MAX_LENGTH)
4404
+ ]);
4405
+ return;
4406
+ }
4407
+ if (args.indexKey === CONVERSATION_ACTIVE_INDEX_KEY) {
4408
+ await client.sendCommand([
4409
+ "ZADD",
4410
+ key,
4411
+ String(args.score),
4412
+ args.conversationId
4413
+ ]);
4414
+ await client.sendCommand([
4415
+ "PEXPIRE",
4416
+ key,
4417
+ String(JUNIOR_THREAD_STATE_TTL_MS)
4418
+ ]);
4419
+ return;
4420
+ }
4421
+ throw new Error(`Unknown conversation index ${args.indexKey}`);
4422
+ }
4423
+ };
4424
+ }
4425
+ function emulatedConversationIndexStore(state) {
4426
+ const readIndex = async (indexKey) => uniqueIndexEntries(await state.get(indexKey));
4427
+ const writeIndex = async (indexKey, entries) => {
4428
+ await state.set(indexKey, entries, JUNIOR_THREAD_STATE_TTL_MS);
4429
+ };
4430
+ return {
4431
+ async list(args) {
4432
+ const entries = (await readIndex(args.indexKey)).filter((entry) => args.scoreMax === void 0 ? true : entry.score <= args.scoreMax).sort(args.order === "asc" ? compareIndexAscending : compareIndexDescending);
4433
+ const offset = Math.max(0, args.offset ?? 0);
4434
+ return entries.slice(offset, args.limit === void 0 ? entries.length : offset + args.limit);
4435
+ },
4436
+ async remove(args) {
4437
+ await withIndexLock(state, args.indexKey, async () => {
4438
+ const entries = await readIndex(args.indexKey);
4439
+ const next = entries.filter((entry) => entry.conversationId !== args.conversationId);
4440
+ if (next.length === entries.length) return;
4441
+ await writeIndex(args.indexKey, next);
4442
+ });
4443
+ },
4444
+ async upsert(args) {
4445
+ await withIndexLock(state, args.indexKey, async () => {
4446
+ const withoutCurrent = (await readIndex(args.indexKey)).filter((entry) => entry.conversationId !== args.conversationId);
4447
+ const next = retainedIndexEntries(args.indexKey, [...withoutCurrent, {
4448
+ conversationId: args.conversationId,
4449
+ score: args.score
4450
+ }]);
4451
+ await writeIndex(args.indexKey, next);
4452
+ });
4453
+ }
4454
+ };
4455
+ }
4456
+ async function getConversationIndexStore(state) {
4457
+ const redisStateAdapter = await getDefaultRedisStateAdapterFor(state);
4458
+ if (redisStateAdapter) return redisConversationIndexStore(redisStateAdapter.getClient());
4459
+ return emulatedConversationIndexStore(state);
4460
+ }
4461
+ async function upsertIndexEntry(args) {
4462
+ await (await getConversationIndexStore(args.state)).upsert({
4463
+ conversationId: args.conversationId,
4464
+ indexKey: args.indexKey,
4465
+ score: args.score
4466
+ });
4467
+ }
4468
+ async function removeIndexEntry(args) {
4469
+ await (await getConversationIndexStore(args.state)).remove({
4470
+ conversationId: args.conversationId,
4471
+ indexKey: args.indexKey
4472
+ });
4473
+ }
4474
+ async function acquireMutationLock(state, conversationId) {
4475
+ const startedAtMs = now$1();
4476
+ while (true) {
4477
+ const lock = await state.acquireLock(mutationLockKey(conversationId), CONVERSATION_MUTATION_LOCK_TTL_MS);
4478
+ if (lock) return lock;
4479
+ if (now$1() - startedAtMs >= CONVERSATION_MUTATION_WAIT_MS) throw new Error(`Could not acquire conversation mutation lock for ${conversationId}`);
4480
+ await sleep(CONVERSATION_MUTATION_RETRY_MS);
4481
+ }
4482
+ }
4483
+ async function withConversationMutation(args, callback) {
4484
+ const state = await getConnectedState(args.state);
4485
+ const lock = await acquireMutationLock(state, args.conversationId);
4486
+ try {
4487
+ return await callback(state, lock);
4488
+ } finally {
4489
+ await state.releaseLock(lock);
4490
+ }
4491
+ }
4492
+ async function readConversation(state, conversationId) {
4493
+ const raw = await state.get(conversationKey(conversationId));
4494
+ if (raw == null) return;
4495
+ const conversation = normalizeConversation(conversationId, raw);
4496
+ if (!conversation) throw new InvalidConversationRecordError(conversationId);
4497
+ return conversation;
4498
+ }
4499
+ /**
4500
+ * Persist a conversation and refresh its reporting and active-recovery indexes.
4501
+ *
4502
+ * Conversation writes are lock-serialized blind whole-record writes, so the
4503
+ * mutation lock is re-validated (and extended) immediately before the write:
4504
+ * a stalled mutator that lost its lock must fail loudly instead of clobbering
4505
+ * a newer record with a stale copy.
4506
+ */
4507
+ async function writeConversation(state, lock, conversation) {
4508
+ const execution = executionWithPendingMessages(conversation.execution, conversation.execution.pendingMessages);
4509
+ const next = {
4510
+ ...conversation,
4511
+ execution
4512
+ };
4513
+ await fenceConversationMutation(state, lock, next.conversationId);
4514
+ const hasSlackDestination = next.destination?.platform === "slack";
4515
+ const stored = {
4516
+ ...next,
4517
+ execution: {
4518
+ ...next.execution,
4519
+ pendingMessages: next.execution.pendingMessages.map((message) => storeMessage(message, hasSlackDestination && (message.source === "slack" || message.source === "event" || message.source === "plugin" || message.source === "scheduler")))
4520
+ }
4521
+ };
4522
+ await state.set(conversationKey(next.conversationId), stored, JUNIOR_THREAD_STATE_TTL_MS);
4523
+ await upsertIndexEntry({
4524
+ state,
4525
+ indexKey: CONVERSATION_BY_ACTIVITY_INDEX_KEY,
4526
+ conversationId: next.conversationId,
4527
+ score: next.lastActivityAtMs
4528
+ });
4529
+ if (!hasRunnableWork(next)) {
4530
+ await removeIndexEntry({
4531
+ state,
4532
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
4533
+ conversationId: next.conversationId
4534
+ });
4535
+ return;
4536
+ }
4537
+ await upsertIndexEntry({
4538
+ state,
4539
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
4540
+ conversationId: next.conversationId,
4541
+ score: next.execution.updatedAtMs ?? next.updatedAtMs
4542
+ });
4543
+ }
4544
+ function assertSameOptionalConversationDestination(args) {
4545
+ if (!args.next || !args.current) return;
4546
+ if (sameDestination$1(args.current, args.next)) return;
4547
+ throw new Error(`Conversation destination changed for ${args.conversationId}`);
4548
+ }
4549
+ function conversationWorkState(conversation) {
4550
+ const lease = conversation.execution.lease;
4551
+ return {
4552
+ ...conversation,
4553
+ lastEnqueuedAtMs: conversation.execution.lastEnqueuedAtMs,
4554
+ ...lease ? { lease: {
4555
+ acquiredAtMs: lease.acquiredAtMs,
4556
+ lastCheckInAtMs: lease.lastCheckInAtMs,
4557
+ leaseExpiresAtMs: lease.expiresAtMs,
4558
+ leaseToken: lease.token
4559
+ } } : void 0,
4560
+ messages: pendingMessages(conversation),
4561
+ needsRun: hasRunnableWork(conversation)
4562
+ };
4563
+ }
4564
+ /** Return a persisted conversation record, if one exists. */
4565
+ async function getConversation$1(args) {
4566
+ return await readConversation(await getConnectedState(args.state), args.conversationId);
4567
+ }
4568
+ /** Return a persisted conversation work record, if one exists. */
4569
+ async function getConversationWorkState$1(args) {
4570
+ const conversation = await getConversation$1(args);
4571
+ return conversation ? conversationWorkState(conversation) : void 0;
4572
+ }
4573
+ /** Return whether a conversation has pending or resumable execution work. */
4574
+ function hasRunnableConversationWork$1(conversation) {
4575
+ return hasRunnableWork(conversation);
4576
+ }
4577
+ async function appendInboundMessageWithAdmission(args) {
4578
+ const nowMs = args.nowMs ?? now$1();
4579
+ return await withConversationMutation({
4580
+ conversationId: args.message.conversationId,
4581
+ state: args.state
4582
+ }, async (state, lock) => {
4583
+ const existing = await readConversation(state, args.message.conversationId);
4584
+ const current = existing ?? emptyConversation({
4585
+ conversationId: args.message.conversationId,
4586
+ destination: args.message.destination,
4587
+ nowMs,
4588
+ source: args.message.source
4589
+ });
4590
+ if (existing) assertSameOptionalConversationDestination({
4591
+ conversationId: args.message.conversationId,
4592
+ current: current.destination,
4593
+ next: args.message.destination
4594
+ });
4595
+ const existingPending = current.execution.pendingMessages.some((message) => message.inboundMessageId === args.message.inboundMessageId);
4596
+ if (current.execution.inboundMessageIds.includes(args.message.inboundMessageId)) {
4597
+ if (!existingPending) return { status: "duplicate" };
4598
+ const nextStatus = current.execution.status === "idle" ? "pending" : current.execution.status;
4599
+ await writeConversation(state, lock, withExecutionUpdate(current, {
4600
+ ...current.execution,
4601
+ status: nextStatus,
4602
+ pendingMessages: current.execution.pendingMessages.map((message) => message.inboundMessageId === args.message.inboundMessageId ? upgradedPendingMessage(message, args.message) : message)
4603
+ }, nowMs));
4604
+ return { status: "duplicate" };
4605
+ }
4606
+ if (args.exclusive && hasRunnableWork(current)) return { status: "active" };
4607
+ let status;
4608
+ if (current.execution.lease) status = current.execution.status === "running" ? "running" : "paused";
4609
+ else if (current.execution.status === "paused" && args.message.delivery === "defer") status = "paused";
4610
+ else status = "pending";
4611
+ await writeConversation(state, lock, withExecutionUpdate({
4612
+ ...current,
4613
+ destination: current.destination ?? args.message.destination,
4614
+ source: current.source ?? args.message.source,
4615
+ lastActivityAtMs: nowMs
4616
+ }, {
4617
+ ...current.execution,
4618
+ status,
4619
+ inboundMessageIds: [...current.execution.inboundMessageIds, args.message.inboundMessageId],
4620
+ pendingMessages: [...current.execution.pendingMessages, args.message].sort(compareMessages)
4621
+ }, nowMs));
4622
+ return { status: "appended" };
4623
+ });
4624
+ }
4625
+ /** Persist one inbound message idempotently in its conversation mailbox. */
4626
+ async function appendInboundMessage(args) {
4627
+ const result = await appendInboundMessageWithAdmission({
4628
+ ...args,
4629
+ exclusive: false
4630
+ });
4631
+ if (result.status === "active") throw new Error("Non-exclusive mailbox append returned active");
4632
+ return result;
4633
+ }
4634
+ /** Append only when the Conversation has no other runnable work. */
4635
+ async function appendExclusiveInboundMessage(args) {
4636
+ return await appendInboundMessageWithAdmission({
4637
+ ...args,
4638
+ exclusive: true
4639
+ });
4640
+ }
4641
+ /** Mark a conversation runnable when there is no new mailbox message. */
4642
+ async function requestConversationWork$1(args) {
4643
+ const nowMs = args.nowMs ?? now$1();
4644
+ return await withConversationMutation(args, async (state, lock) => {
4645
+ const existing = await readConversation(state, args.conversationId);
4646
+ if (existing) assertSameOptionalConversationDestination({
4647
+ conversationId: args.conversationId,
4648
+ current: existing.destination,
4649
+ next: args.destination
4650
+ });
4651
+ const current = existing ?? emptyConversation({
4652
+ conversationId: args.conversationId,
4653
+ destination: args.destination,
4654
+ nowMs
4655
+ });
4656
+ const status = current.execution.lease ? "paused" : "pending";
4657
+ await writeConversation(state, lock, withExecutionUpdate({
4658
+ ...current,
4659
+ destination: current.destination ?? args.destination
4660
+ }, {
4661
+ ...current.execution,
4662
+ status
4663
+ }, nowMs));
4664
+ return { status: existing === void 0 ? "created" : "updated" };
4665
+ });
4666
+ }
4667
+ /** Record that a wake-up nudge was accepted for the conversation. */
4668
+ async function markConversationWorkEnqueued$1(args) {
4669
+ const nowMs = args.nowMs ?? now$1();
4670
+ await withConversationMutation(args, async (state, lock) => {
4671
+ const current = await readConversation(state, args.conversationId);
4672
+ if (!current) return;
4673
+ await writeConversation(state, lock, withExecutionUpdate(current, {
4674
+ ...current.execution,
4675
+ lastEnqueuedAtMs: nowMs
4676
+ }, nowMs));
4677
+ });
4678
+ }
4679
+ /** Clear a wake marker after its queue delivery finds no runnable work. */
4680
+ async function clearConsumedConversationWake$1(args) {
4681
+ const nowMs = args.nowMs ?? now$1();
4682
+ return await withConversationMutation(args, async (state, lock) => {
4683
+ const current = await readConversation(state, args.conversationId);
4684
+ if (!current || hasRunnableWork(current) || current.execution.lastEnqueuedAtMs === void 0) return false;
4685
+ await writeConversation(state, lock, withExecutionUpdate(current, {
4686
+ ...current.execution,
4687
+ lastEnqueuedAtMs: void 0
4688
+ }, nowMs));
4689
+ return true;
4690
+ });
4691
+ }
4692
+ /** Try to acquire the durable execution lease for one conversation. */
4693
+ async function startConversationWork$1(args) {
4694
+ const nowMs = args.nowMs ?? now$1();
4695
+ return await withConversationMutation(args, async (state, lock) => {
4696
+ const current = await readConversation(state, args.conversationId);
4697
+ if (!current) return { status: "no_work" };
4698
+ if (isLeaseActive(current.execution.lease, nowMs)) return {
4699
+ status: "active",
4700
+ leaseExpiresAtMs: current.execution.lease.expiresAtMs
4701
+ };
4702
+ if (!hasRunnableWork(current)) return { status: "no_work" };
4703
+ const lease = {
4704
+ token: randomUUID(),
4705
+ acquiredAtMs: nowMs,
4706
+ lastCheckInAtMs: nowMs,
4707
+ expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
4708
+ };
4709
+ const startsNewRun = current.execution.runId === void 0;
4710
+ await writeConversation(state, lock, withExecutionUpdate(current, {
4711
+ ...current.execution,
4712
+ lease,
4713
+ status: current.execution.status === "paused" ? "paused" : "running",
4714
+ runId: current.execution.runId ?? randomUUID(),
4715
+ lastEnqueuedAtMs: void 0,
4716
+ retryCount: startsNewRun ? 0 : current.execution.retryCount
4717
+ }, nowMs));
4718
+ return {
4719
+ status: "acquired",
4720
+ leaseToken: lease.token,
4721
+ leaseExpiresAtMs: lease.expiresAtMs
4722
+ };
4723
+ });
4724
+ }
4725
+ /** Extend the durable execution lease when the worker checks in. */
4726
+ async function checkInConversationWork$1(args) {
4727
+ const nowMs = args.nowMs ?? now$1();
4728
+ return await withConversationMutation(args, async (state, lock) => {
4729
+ const current = await readConversation(state, args.conversationId);
4730
+ if (!current || current.execution.lease?.token !== args.leaseToken) return false;
4731
+ await writeConversation(state, lock, withExecutionUpdate(current, {
4732
+ ...current.execution,
4733
+ lease: {
4734
+ ...current.execution.lease,
4735
+ lastCheckInAtMs: nowMs,
4736
+ expiresAtMs: nowMs + CONVERSATION_WORK_LEASE_TTL_MS
4737
+ }
4738
+ }, nowMs));
4739
+ return true;
4740
+ });
4741
+ }
4742
+ /**
4743
+ * Resolve pending mailbox entries after the caller accepts responsibility.
4744
+ *
4745
+ * Returning ids acknowledges only that subset; returning nothing acknowledges
4746
+ * every pending entry passed to the handler.
4747
+ */
4748
+ async function drainConversationMailbox$1(args) {
4749
+ const nowMs = args.nowMs ?? now$1();
4750
+ const pending = await withConversationMutation(args, async (state) => {
4751
+ const current = await readConversation(state, args.conversationId);
4752
+ if (!current || current.execution.lease?.token !== args.leaseToken) throw new Error(`Conversation lease is not held for ${args.conversationId}`);
4753
+ return pendingMessages(current);
4754
+ });
4755
+ if (pending.length === 0) return {
4756
+ changed: false,
4757
+ messages: []
4758
+ };
4759
+ const result = await args.handle(pending);
4760
+ const pendingIds = new Set(pending.map((message) => message.inboundMessageId));
4761
+ const acknowledgedIds = new Set(result ?? pending.map((message) => message.inboundMessageId));
4762
+ for (const inboundMessageId of acknowledgedIds) if (!pendingIds.has(inboundMessageId)) throw new Error(`Conversation mailbox drain result is not pending for ${args.conversationId}`);
4763
+ if (acknowledgedIds.size === 0) return {
4764
+ changed: false,
4765
+ messages: []
4766
+ };
4767
+ await withConversationMutation(args, async (state, lock) => {
4768
+ const current = await readConversation(state, args.conversationId);
4769
+ if (!current || current.execution.lease?.token !== args.leaseToken) throw new Error(`Conversation lease is not held for ${args.conversationId}`);
4770
+ const pendingMessages = current.execution.pendingMessages.filter((message) => !acknowledgedIds.has(message.inboundMessageId));
4771
+ await writeConversation(state, lock, withExecutionUpdate(current, {
4772
+ ...current.execution,
4773
+ status: current.execution.status === "pending" && pendingMessages.length === 0 ? "running" : current.execution.status,
4774
+ lastProgressAtMs: nowMs,
4775
+ retryCount: 0,
4776
+ pendingMessages
4777
+ }, nowMs));
4778
+ });
4779
+ return {
4780
+ changed: true,
4781
+ messages: pending.filter((message) => acknowledgedIds.has(message.inboundMessageId))
4782
+ };
4783
+ }
4784
+ /** Acknowledge leased mailbox entries after the handler accepts responsibility. */
4785
+ async function ackMessages$1(args) {
4786
+ const nowMs = args.nowMs ?? now$1();
4787
+ const inboundMessageIds = new Set(args.inboundMessageIds);
4788
+ return await withConversationMutation(args, async (state, lock) => {
4789
+ const current = await readConversation(state, args.conversationId);
4790
+ if (!current || current.execution.lease?.token !== args.leaseToken) return false;
4791
+ if (inboundMessageIds.size === 0) return true;
4792
+ const pendingMessages = current.execution.pendingMessages.filter((message) => !inboundMessageIds.has(message.inboundMessageId));
4793
+ if (pendingMessages.length === current.execution.pendingMessages.length) return true;
4794
+ await writeConversation(state, lock, withExecutionUpdate(current, {
4795
+ ...current.execution,
4796
+ lastProgressAtMs: nowMs,
4797
+ retryCount: 0,
4798
+ pendingMessages
4799
+ }, nowMs));
4800
+ return true;
4801
+ });
4802
+ }
4803
+ function isHumanFacingMessage(message) {
4804
+ return message.source === "web" || message.source === "slack";
4805
+ }
4806
+ async function readConversationStop(state, conversationId) {
4807
+ return normalizeStop(await state.get(conversationStopKey(conversationId)));
4808
+ }
4809
+ async function fenceConversationMutation(state, lock, conversationId) {
4810
+ if (!await state.extendLock(lock, CONVERSATION_MUTATION_LOCK_TTL_MS)) throw new ConversationMutationFencedError(conversationId);
4811
+ }
4812
+ /** Return whether one Conversation run has a durable stop request. */
4813
+ async function hasConversationStop(args) {
4814
+ return (await readConversationStop(await getConnectedState(args.state), args.conversationId))?.runId === args.runId;
4815
+ }
4816
+ /** Persist a stop request for the current run without process affinity. */
4817
+ async function stopConversationWork$1(args) {
4818
+ const nowMs = args.nowMs ?? now$1();
4819
+ return await withConversationMutation(args, async (state, lock) => {
4820
+ const current = await readConversation(state, args.conversationId);
4821
+ if (!current || !hasRunnableWork(current)) return { status: "no_work" };
4822
+ const runId = current.execution.runId ?? randomUUID();
4823
+ const pendingMessages = current.execution.pendingMessages.filter(isHumanFacingMessage);
4824
+ const inboundMessageIds = pendingMessages.map((message) => message.inboundMessageId);
4825
+ if (current.execution.runId === void 0) await writeConversation(state, lock, withExecutionUpdate(current, {
4826
+ ...current.execution,
4827
+ runId
4828
+ }, nowMs));
4829
+ await fenceConversationMutation(state, lock, args.conversationId);
4830
+ await state.set(conversationStopKey(args.conversationId), {
4831
+ inboundMessageIds,
4832
+ runId
4833
+ }, JUNIOR_THREAD_STATE_TTL_MS);
4834
+ return {
4835
+ runId,
4836
+ status: "requested",
4837
+ pendingMessages
4838
+ };
4839
+ });
4840
+ }
4841
+ /** Clear one observed stop request and discard older human-facing mailbox work. */
4842
+ async function completeConversationStop$1(args) {
4843
+ const nowMs = args.nowMs ?? now$1();
4844
+ return await withConversationMutation(args, async (state, lock) => {
4845
+ const current = await readConversation(state, args.conversationId);
4846
+ if (!current || current.execution.lease?.token !== args.leaseToken) return {
4847
+ status: "lost_lease",
4848
+ removedInboundMessageIds: []
4849
+ };
4850
+ const stop = await readConversationStop(state, args.conversationId);
4851
+ if (!stop || stop.runId !== args.runId) return {
4852
+ status: "none",
4853
+ removedInboundMessageIds: []
4854
+ };
4855
+ const stoppedInboundMessageIds = new Set(stop.inboundMessageIds);
4856
+ const removedInboundMessageIds = [];
4857
+ const pendingMessages = current.execution.pendingMessages.filter((message) => {
4858
+ const shouldStop = stoppedInboundMessageIds.has(message.inboundMessageId);
4859
+ if (shouldStop) removedInboundMessageIds.push(message.inboundMessageId);
4860
+ return !shouldStop;
4861
+ });
4862
+ await writeConversation(state, lock, withExecutionUpdate(current, {
4863
+ ...current.execution,
4864
+ lastEnqueuedAtMs: pendingMessages.length === 0 ? void 0 : current.execution.lastEnqueuedAtMs,
4865
+ pendingMessages
4866
+ }, nowMs));
4867
+ await fenceConversationMutation(state, lock, args.conversationId);
4868
+ await state.delete(conversationStopKey(args.conversationId));
4869
+ return {
4870
+ status: "cleared",
4871
+ removedInboundMessageIds
4872
+ };
4873
+ });
4874
+ }
4875
+ /** Cancel human-facing pending mailbox rows without requiring a worker lease. */
4876
+ async function cancelHumanFacingPendingMessages$1(args) {
4877
+ const nowMs = args.nowMs ?? now$1();
4878
+ const requestedIds = args.inboundMessageIds === void 0 ? void 0 : new Set(args.inboundMessageIds);
4879
+ return await withConversationMutation(args, async (state, lock) => {
4880
+ const current = await readConversation(state, args.conversationId);
4881
+ if (!current) return {
4882
+ cancelledInboundMessageIds: [],
4883
+ cancelledMessages: []
4884
+ };
4885
+ const cancelledInboundMessageIds = [];
4886
+ const cancelledMessages = [];
4887
+ const pendingMessages = [];
4888
+ for (const message of current.execution.pendingMessages) {
4889
+ const isRequested = requestedIds === void 0 || requestedIds.has(message.inboundMessageId);
4890
+ const isInSnapshot = args.receivedBeforeMs === void 0 || message.receivedAtMs <= args.receivedBeforeMs;
4891
+ if (isHumanFacingMessage(message) && isRequested && isInSnapshot) {
4892
+ cancelledInboundMessageIds.push(message.inboundMessageId);
4893
+ cancelledMessages.push(message);
4894
+ continue;
4895
+ }
4896
+ pendingMessages.push(message);
4897
+ }
4898
+ if (cancelledInboundMessageIds.length === 0) return {
4899
+ cancelledInboundMessageIds,
4900
+ cancelledMessages
4901
+ };
4902
+ const becomesIdle = current.execution.status === "pending" && pendingMessages.length === 0;
4903
+ await writeConversation(state, lock, withExecutionUpdate(current, {
4904
+ ...current.execution,
4905
+ lastEnqueuedAtMs: pendingMessages.length === 0 ? void 0 : current.execution.lastEnqueuedAtMs,
4906
+ pendingMessages,
4907
+ retryCount: becomesIdle ? 0 : current.execution.retryCount,
4908
+ runId: becomesIdle ? void 0 : current.execution.runId,
4909
+ status: becomesIdle ? "idle" : current.execution.status
4910
+ }, nowMs));
4911
+ return {
4912
+ cancelledInboundMessageIds,
4913
+ cancelledMessages
4914
+ };
4915
+ });
4916
+ }
4917
+ /** Mark the leased conversation as needing another queue-delivered slice. */
4918
+ async function requestAnotherSlice$1(args) {
4919
+ const nowMs = args.nowMs ?? now$1();
4920
+ return await withConversationMutation(args, async (state, lock) => {
4921
+ const current = await readConversation(state, args.conversationId);
4922
+ if (!current || current.execution.lease?.token !== args.leaseToken) return false;
4923
+ assertSameOptionalConversationDestination({
4924
+ conversationId: args.conversationId,
4925
+ current: current.destination,
4926
+ next: args.destination
4927
+ });
4928
+ await writeConversation(state, lock, withExecutionUpdate(current, {
4929
+ ...current.execution,
4930
+ status: "paused"
4931
+ }, nowMs));
4932
+ return true;
4933
+ });
4934
+ }
4935
+ /** Begin a requested turn resume under the worker's existing lease. */
4936
+ async function beginConversationResume$1(args) {
4937
+ const nowMs = args.nowMs ?? now$1();
4938
+ return await withConversationMutation(args, async (state, lock) => {
4939
+ const current = await readConversation(state, args.conversationId);
4940
+ if (!current || current.execution.lease?.token !== args.leaseToken || current.execution.status !== "paused") return false;
4941
+ await writeConversation(state, lock, withExecutionUpdate(current, {
4942
+ ...current.execution,
4943
+ status: "running"
4944
+ }, nowMs));
4945
+ return true;
4946
+ });
4947
+ }
4948
+ /** Release the durable execution lease without changing completion state. */
4949
+ async function releaseConversationWork$1(args) {
4950
+ const nowMs = args.nowMs ?? now$1();
4951
+ return await withConversationMutation(args, async (state, lock) => {
4952
+ const current = await readConversation(state, args.conversationId);
4953
+ if (!current || current.execution.lease?.token !== args.leaseToken) return false;
4954
+ await writeConversation(state, lock, withExecutionUpdate(current, {
4955
+ ...current.execution,
4956
+ lease: void 0,
4957
+ status: current.execution.status === "running" ? "pending" : current.execution.status
4958
+ }, nowMs));
4959
+ return true;
4960
+ });
4961
+ }
4962
+ /** Count one failed execution attempt and stop the conversation when retries are exhausted. */
4963
+ async function recordConversationRetry$1(args) {
4964
+ const nowMs = args.nowMs ?? now$1();
4965
+ return await withConversationMutation(args, async (state, lock) => {
4966
+ const current = await readConversation(state, args.conversationId);
4967
+ if (!current || current.execution.lease?.token !== args.leaseToken) return "lost_lease";
4968
+ const count = (current.execution.retryCount ?? 0) + 1;
4969
+ const stopped = count >= 5;
4970
+ await writeConversation(state, lock, withExecutionUpdate(current, {
4971
+ ...current.execution,
4972
+ lastEnqueuedAtMs: void 0,
4973
+ lease: stopped ? void 0 : current.execution.lease,
4974
+ retryCount: count,
4975
+ pendingMessages: stopped ? [] : current.execution.pendingMessages,
4976
+ runId: stopped ? void 0 : current.execution.runId,
4977
+ status: stopped ? "failed" : "paused"
4978
+ }, nowMs));
4979
+ return stopped ? "stopped" : "recorded";
4980
+ });
4981
+ }
4982
+ /** Finish a leased conversation and report whether runnable work remains. */
4983
+ async function completeConversationWork$1(args) {
4984
+ const nowMs = args.nowMs ?? now$1();
4985
+ return await withConversationMutation(args, async (state, lock) => {
4986
+ const current = await readConversation(state, args.conversationId);
4987
+ if (!current || current.execution.lease?.token !== args.leaseToken) return "lost_lease";
4988
+ const hasPending = pendingMessages(current).length > 0;
4989
+ const stopped = args.resumeIfStopped === true && (await readConversationStop(state, args.conversationId))?.runId === current.execution.runId;
4990
+ const needsRun = current.execution.status === "paused" || stopped;
4991
+ const runnable = needsRun || hasPending;
4992
+ await writeConversation(state, lock, withExecutionUpdate(current, {
4993
+ ...current.execution,
4994
+ lease: void 0,
4995
+ status: needsRun ? "paused" : hasPending ? "pending" : "idle",
4996
+ lastProgressAtMs: args.madeProgress === false ? current.execution.lastProgressAtMs : nowMs,
4997
+ retryCount: runnable || args.madeProgress === false ? current.execution.retryCount : 0,
4998
+ runId: runnable ? current.execution.runId : void 0
4999
+ }, nowMs));
5000
+ return runnable ? "pending" : "completed";
5001
+ });
5002
+ }
5003
+ /**
5004
+ * Record one failed execution attempt for pending messages, dead-lettering
5005
+ * the conversation when its retry budget or the legacy message delivery limit
5006
+ * is exhausted.
5007
+ *
5008
+ * Retries are counted only when the run made no durable progress: if any
5009
+ * attempted message left the mailbox, the remaining pending entries may be
5010
+ * deliberate deferrals and are left untouched. Consumed message ids stay in
5011
+ * `inboundMessageIds` so source retries remain duplicates.
5012
+ */
5013
+ async function recordAttemptFailure$1(args) {
5014
+ const nowMs = args.nowMs ?? now$1();
5015
+ return await withConversationMutation(args, async (state, lock) => {
5016
+ const current = await readConversation(state, args.conversationId);
5017
+ if (!current || current.execution.lease?.token !== args.leaseToken) return {
5018
+ status: "lost_lease",
5019
+ pendingCount: 0,
5020
+ deadLetteredMessages: [],
5021
+ retryCount: 0
5022
+ };
5023
+ const pendingIds = new Set(current.execution.pendingMessages.map((message) => message.inboundMessageId));
5024
+ if (args.inboundMessageIds.length === 0 || args.inboundMessageIds.some((id) => !pendingIds.has(id))) return {
5025
+ status: "skipped",
5026
+ pendingCount: current.execution.pendingMessages.length,
5027
+ deadLetteredMessages: [],
5028
+ retryCount: current.execution.retryCount ?? 0
5029
+ };
5030
+ const retryCount = (current.execution.retryCount ?? 0) + 1;
5031
+ const retriesExhausted = retryCount >= 5;
5032
+ const attemptedIds = new Set(args.inboundMessageIds);
5033
+ const deadLetteredMessages = [];
5034
+ const pendingMessages = [];
5035
+ for (const message of current.execution.pendingMessages) {
5036
+ if (!attemptedIds.has(message.inboundMessageId)) {
5037
+ if (retriesExhausted) deadLetteredMessages.push(message);
5038
+ else pendingMessages.push(message);
5039
+ continue;
5040
+ }
5041
+ const attempted = {
5042
+ ...message,
5043
+ attemptCount: (message.attemptCount ?? 0) + 1
5044
+ };
5045
+ if (retriesExhausted || attempted.attemptCount >= 5) {
5046
+ deadLetteredMessages.push(attempted);
5047
+ continue;
5048
+ }
5049
+ pendingMessages.push(attempted);
5050
+ }
5051
+ await writeConversation(state, lock, withExecutionUpdate(current, {
5052
+ ...current.execution,
5053
+ retryCount,
5054
+ pendingMessages
5055
+ }, nowMs));
5056
+ return {
5057
+ status: "recorded",
5058
+ pendingCount: pendingMessages.length,
5059
+ deadLetteredMessages,
5060
+ retryCount
5061
+ };
5062
+ });
5063
+ }
5064
+ /** Record a terminal failure completion for a leased conversation. */
5065
+ async function deadLetterAttempt$1(args) {
5066
+ const nowMs = args.nowMs ?? now$1();
5067
+ return await withConversationMutation(args, async (state, lock) => {
5068
+ const current = await readConversation(state, args.conversationId);
5069
+ if (!current || current.execution.lease?.token !== args.leaseToken) return "lost_lease";
5070
+ const runnable = pendingMessages(current).length > 0;
5071
+ await writeConversation(state, lock, withExecutionUpdate(current, {
5072
+ ...current.execution,
5073
+ lease: void 0,
5074
+ status: runnable ? "pending" : "failed",
5075
+ runId: runnable ? current.execution.runId : void 0
5076
+ }, nowMs));
5077
+ return runnable ? "pending" : "failed";
5078
+ });
5079
+ }
5080
+ /** Recover an expired lease, consuming the retry budget under the mutation lock. */
5081
+ async function clearExpiredConversationLease$1(args) {
5082
+ const nowMs = args.nowMs ?? now$1();
5083
+ return await withConversationMutation(args, async (state, lock) => {
5084
+ const current = await readConversation(state, args.conversationId);
5085
+ if (!current?.execution.lease || current.execution.lease.expiresAtMs > nowMs) return "not_expired";
5086
+ const retryCount = (current.execution.retryCount ?? 0) + 1;
5087
+ const stopped = retryCount >= 5;
5088
+ await writeConversation(state, lock, withExecutionUpdate(current, {
5089
+ ...current.execution,
5090
+ lastEnqueuedAtMs: void 0,
5091
+ lease: void 0,
5092
+ retryCount,
5093
+ pendingMessages: stopped ? [] : current.execution.pendingMessages,
5094
+ runId: stopped ? void 0 : current.execution.runId,
5095
+ status: stopped ? "failed" : "paused"
5096
+ }, nowMs));
5097
+ return stopped ? "stopped" : "requeued";
5098
+ });
5099
+ }
5100
+ /** Remove one conversation from the active index after it is missing or idle. */
5101
+ async function removeActiveConversation$1(args) {
5102
+ await removeIndexEntry({
5103
+ state: await getConnectedState(args.state),
5104
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
5105
+ conversationId: args.conversationId
5106
+ });
5107
+ }
5108
+ /** List active conversation ids by oldest execution update first. */
5109
+ async function listActiveConversationIds$1(args = {}) {
5110
+ return (await (await getConversationIndexStore(await getConnectedState(args.state))).list({
5111
+ indexKey: CONVERSATION_ACTIVE_INDEX_KEY,
5112
+ limit: args.limit,
5113
+ order: "asc",
5114
+ scoreMax: args.staleBeforeMs
5115
+ })).map((entry) => entry.conversationId);
5116
+ }
5117
+ //#endregion
5118
+ //#region src/chat/task-execution/store.ts
5119
+ function metadataStore(options) {
5120
+ return options.conversationStore ?? getConversationStore();
5121
+ }
5122
+ function duplicateInboundNudgeIdempotencyKey(message, nowMs) {
5123
+ return `duplicate:${message.conversationId}:${message.inboundMessageId}:${nowMs}`;
5124
+ }
5125
+ function hasRecentEnqueueMarker(conversation, nowMs) {
5126
+ const lastEnqueuedAtMs = conversation.execution.lastEnqueuedAtMs;
5127
+ return typeof lastEnqueuedAtMs === "number" && lastEnqueuedAtMs + 6e4 > nowMs;
5128
+ }
5129
+ function now() {
5130
+ return Date.now();
5131
+ }
5132
+ async function recordExecutionMetadata(args) {
5133
+ const conversation = await getConversation$1({
5134
+ conversationId: args.conversationId,
5135
+ state: args.state
5136
+ });
5137
+ if (!conversation) return;
5138
+ await metadataStore(args).recordExecution({
5139
+ channelName: conversation.channelName,
5140
+ conversationId: conversation.conversationId,
5141
+ createdAtMs: conversation.createdAtMs,
5142
+ destination: conversation.destination,
5143
+ execution: {
5144
+ lastCheckpointAtMs: conversation.execution.lastCheckpointAtMs,
5145
+ lastEnqueuedAtMs: conversation.execution.lastEnqueuedAtMs,
5146
+ runId: conversation.execution.runId,
5147
+ status: conversation.execution.status,
5148
+ updatedAtMs: conversation.execution.updatedAtMs
5149
+ },
5150
+ lastActivityAtMs: conversation.lastActivityAtMs,
5151
+ metrics: null,
5152
+ actor: conversation.actor,
5153
+ source: conversation.source,
5154
+ title: conversation.title,
5155
+ updatedAtMs: conversation.updatedAtMs
5156
+ });
5157
+ }
5158
+ /** Return a persisted conversation record, if one exists. */
5159
+ async function getConversation(args) {
5160
+ return await getConversation$1(args);
5161
+ }
5162
+ /** Return a persisted conversation work record, if one exists. */
5163
+ async function getConversationWorkState(args) {
5164
+ return await getConversationWorkState$1(args);
5165
+ }
5166
+ /** Count one failed conversation execution and persist its terminal retry state. */
5167
+ async function recordConversationRetry(args) {
5168
+ const result = await recordConversationRetry$1(args);
5169
+ await recordExecutionMetadata(args);
5170
+ return result;
5171
+ }
5172
+ /** Count mailbox messages that have not yet reached the conversation event log. */
5173
+ function countPendingConversationMessages(conversation) {
5174
+ return conversation.execution.pendingMessages.length;
5175
+ }
5176
+ /** Return whether a conversation has pending or resumable execution work. */
5177
+ function hasRunnableConversationWork(conversation) {
5178
+ return conversation.execution.status !== "idle" || countPendingConversationMessages(conversation) > 0;
5179
+ }
5180
+ /**
5181
+ * Ensure runnable conversation work has one accepted queue wake-up nudge.
5182
+ *
5183
+ * Ordinary wakes coalesce on a recent accepted marker. A caller can ignore the
5184
+ * marker without replacing an active lease. Replacement is only for consumed
5185
+ * or known-stale deliveries where another queue nudge must exist.
5186
+ */
5187
+ async function ensureConversationWake(args) {
5188
+ const nowMs = args.nowMs ?? now();
5189
+ const conversation = await getConversation$1({
5190
+ conversationId: args.conversationId,
5191
+ state: args.state
5192
+ });
5193
+ if (!conversation || !hasRunnableConversationWork(conversation)) return { status: "no_work" };
5194
+ if (args.replaceExistingWake !== true && conversation.execution.lease && conversation.execution.lease.expiresAtMs > nowMs) return { status: "lease_active" };
5195
+ if (args.ignoreEnqueueMarker !== true && args.replaceExistingWake !== true && hasRecentEnqueueMarker(conversation, nowMs)) return { status: "already_enqueued" };
5196
+ const queueResult = await args.queue.send({
5197
+ schemaVersion: 2,
5198
+ conversationId: args.conversationId
5199
+ }, {
5200
+ delayMs: args.delayMs,
5201
+ idempotencyKey: args.idempotencyKey
5202
+ });
5203
+ await markConversationWorkEnqueued({
5204
+ conversationId: args.conversationId,
5205
+ conversationStore: args.conversationStore,
5206
+ nowMs,
5207
+ state: args.state
5208
+ });
5209
+ return {
5210
+ status: "enqueued",
5211
+ queueMessageId: queueResult?.messageId
5212
+ };
5213
+ }
5214
+ async function enqueueAfterAppend(args) {
5215
+ const nowMs = args.nowMs ?? now();
5216
+ const appendResult = args.appendResult;
5217
+ if (appendResult.status === "active") return appendResult;
5218
+ let idempotencyKey = args.message.inboundMessageId;
5219
+ if (appendResult.status === "duplicate") {
5220
+ const conversation = await getConversation$1({
5221
+ conversationId: args.message.conversationId,
5222
+ state: args.state
5223
+ });
5224
+ if (!conversation) return appendResult;
5225
+ if (hasRecentEnqueueMarker(conversation, nowMs)) {
5226
+ await recordExecutionMetadata({
5227
+ conversationId: args.message.conversationId,
5228
+ conversationStore: args.conversationStore,
5229
+ state: args.state
5230
+ });
5231
+ return appendResult;
5232
+ }
5233
+ if (!conversation.execution.pendingMessages.some((message) => message.inboundMessageId === args.message.inboundMessageId)) {
5234
+ await recordExecutionMetadata({
5235
+ conversationId: args.message.conversationId,
5236
+ conversationStore: args.conversationStore,
5237
+ state: args.state
5238
+ });
5239
+ return appendResult;
5240
+ }
5241
+ idempotencyKey = duplicateInboundNudgeIdempotencyKey(args.message, nowMs);
5242
+ }
5243
+ const wake = await ensureConversationWake({
5244
+ conversationId: args.message.conversationId,
5245
+ conversationStore: args.conversationStore,
5246
+ delayMs: args.queueDelayMs,
5247
+ idempotencyKey,
5248
+ ignoreEnqueueMarker: args.message.source === "event" ? void 0 : true,
5249
+ nowMs,
5250
+ queue: args.queue,
5251
+ replaceExistingWake: args.replaceExistingWake,
5252
+ state: args.state
5253
+ });
5254
+ if (wake.status !== "enqueued") await recordExecutionMetadata({
5255
+ conversationId: args.message.conversationId,
5256
+ conversationStore: args.conversationStore,
5257
+ state: args.state
5258
+ });
5259
+ return {
5260
+ ...appendResult,
5261
+ ...wake.status === "enqueued" ? { queueMessageId: wake.queueMessageId } : void 0
5262
+ };
5263
+ }
5264
+ /** Persist inbound work and ensure a worker wake-up. */
5265
+ async function appendAndEnqueueInboundMessage(args) {
5266
+ const nowMs = args.nowMs ?? now();
5267
+ const result = await enqueueAfterAppend({
5268
+ ...args,
5269
+ appendResult: await appendInboundMessage({
5270
+ message: args.message,
5271
+ nowMs,
5272
+ state: args.state
5273
+ }),
5274
+ nowMs
5275
+ });
5276
+ if (result.status === "active") throw new Error("Non-exclusive mailbox enqueue returned active");
5277
+ return result;
5278
+ }
5279
+ /** Persist exclusive inbound work and wake its worker when admitted. */
5280
+ async function appendAndEnqueueExclusiveInboundMessage(args) {
5281
+ const nowMs = args.nowMs ?? now();
5282
+ return await enqueueAfterAppend({
5283
+ ...args,
5284
+ appendResult: await appendExclusiveInboundMessage({
5285
+ message: args.message,
5286
+ nowMs,
5287
+ state: args.state
5288
+ }),
5289
+ nowMs
5290
+ });
5291
+ }
5292
+ /** Persist a stop request for the current Conversation run. */
5293
+ async function stopConversationWork(args) {
5294
+ const result = await stopConversationWork$1(args);
5295
+ if (result.status === "requested") await recordExecutionMetadata(args);
5296
+ return result;
5297
+ }
5298
+ /** Finish an observed stop request under the current worker lease. */
5299
+ async function completeConversationStop(args) {
5300
+ const result = await completeConversationStop$1(args);
5301
+ if (result.status === "cleared") await recordExecutionMetadata(args);
5302
+ return result;
5303
+ }
5304
+ /** Clear an accepted wake marker after its delivery finds no runnable work. */
5305
+ async function clearConsumedConversationWake(args) {
5306
+ const result = await clearConsumedConversationWake$1(args);
5307
+ if (result) await recordExecutionMetadata(args);
5308
+ return result;
5309
+ }
5310
+ /** Mark a conversation runnable when there is no new mailbox message. */
5311
+ async function requestConversationWork(args) {
5312
+ const result = await requestConversationWork$1(args);
5313
+ await recordExecutionMetadata({
5314
+ conversationId: args.conversationId,
5315
+ conversationStore: args.conversationStore,
5316
+ state: args.state
5317
+ });
5318
+ return result;
5319
+ }
5320
+ /** Record that a wake-up nudge was accepted for the conversation. */
5321
+ async function markConversationWorkEnqueued(args) {
5322
+ await markConversationWorkEnqueued$1(args);
5323
+ await recordExecutionMetadata(args);
5324
+ }
5325
+ /** Try to acquire the durable execution lease for one conversation. */
5326
+ async function startConversationWork(args) {
5327
+ const result = await startConversationWork$1(args);
5328
+ await recordExecutionMetadata(args);
5329
+ return result;
5330
+ }
5331
+ /** Extend the durable execution lease when the worker checks in. */
5332
+ async function checkInConversationWork(args) {
5333
+ const result = await checkInConversationWork$1(args);
5334
+ if (result) await recordExecutionMetadata(args);
5335
+ return result;
5336
+ }
5337
+ /** Drain pending mailbox entries after the caller accepts responsibility. */
5338
+ async function drainConversationMailbox(args) {
5339
+ const result = await drainConversationMailbox$1(args);
5340
+ if (result.changed) await recordExecutionMetadata(args);
5341
+ return result.messages;
5342
+ }
5343
+ /** Acknowledge leased mailbox entries after the handler accepts responsibility. */
5344
+ async function ackMessages(args) {
5345
+ const result = await ackMessages$1(args);
5346
+ await recordExecutionMetadata(args);
5347
+ return result;
5348
+ }
5349
+ /** Cancel human-facing pending mailbox rows without requiring a worker lease. */
5350
+ async function cancelHumanFacingPendingMessages(args) {
5351
+ const result = await cancelHumanFacingPendingMessages$1(args);
5352
+ if (result.cancelledInboundMessageIds.length > 0) await recordExecutionMetadata(args);
5353
+ return result;
5354
+ }
5355
+ /** Mark the leased conversation as needing another queue-delivered slice. */
5356
+ async function requestAnotherSlice(args) {
5357
+ const result = await requestAnotherSlice$1(args);
5358
+ await recordExecutionMetadata(args);
5359
+ return result;
5360
+ }
5361
+ /** Begin a requested turn resume without releasing the current lease. */
5362
+ async function beginConversationResume(args) {
5363
+ const result = await beginConversationResume$1(args);
5364
+ if (result) await recordExecutionMetadata(args);
5365
+ return result;
5366
+ }
5367
+ /** Release the durable execution lease without changing completion state. */
5368
+ async function releaseConversationWork(args) {
5369
+ const result = await releaseConversationWork$1(args);
5370
+ await recordExecutionMetadata(args);
5371
+ return result;
5372
+ }
5373
+ /** Finish a leased conversation and report whether runnable work remains. */
5374
+ async function completeConversationWork(args) {
5375
+ const result = await completeConversationWork$1(args);
5376
+ await recordExecutionMetadata(args);
5377
+ return result;
5378
+ }
5379
+ /** Record one failed delivery attempt and dead-letter messages at their limit. */
5380
+ async function recordAttemptFailure(args) {
5381
+ const result = await recordAttemptFailure$1(args);
5382
+ if (result.status === "recorded") await recordExecutionMetadata(args);
5383
+ return result;
5384
+ }
5385
+ /** Record a terminal failure completion for a leased conversation. */
5386
+ async function deadLetterAttempt(args) {
5387
+ const result = await deadLetterAttempt$1(args);
5388
+ await recordExecutionMetadata(args);
5389
+ return result;
5390
+ }
5391
+ /** Recover an expired lease and persist the resulting retry state. */
5392
+ async function clearExpiredConversationLease(args) {
5393
+ const result = await clearExpiredConversationLease$1(args);
5394
+ await recordExecutionMetadata(args);
5395
+ return result;
5396
+ }
5397
+ /** Remove one conversation from the active index after it is missing or idle. */
5398
+ async function removeActiveConversation(args) {
5399
+ return await removeActiveConversation$1(args);
5400
+ }
5401
+ /** List active conversation ids by oldest execution update first. */
5402
+ async function listActiveConversationIds(args = {}) {
5403
+ return await listActiveConversationIds$1(args);
5404
+ }
5405
+ //#endregion
5406
+ //#region src/chat/events/catalog.ts
5407
+ /** Core-owned event namespace. Not a plugin publisher. */
5408
+ const CORE_EVENT_NAMESPACE = "junior";
5409
+ /** True when the catalog has any enabled registration. */
5410
+ function hasEventCatalogEntries(catalog) {
5411
+ return Object.keys(catalog).length > 0;
5412
+ }
5413
+ /**
5414
+ * True when at least one plugin namespace is enabled.
5415
+ *
5416
+ * Core Workspace snapshot events stay in the catalog for search and temporary
5417
+ * watches. Durable event automations still require a plugin publisher.
5418
+ */
5419
+ function hasPluginEventCatalogEntries(catalog) {
5420
+ return Object.keys(pluginEventCatalog(catalog)).length > 0;
5421
+ }
5422
+ /**
5423
+ * Catalog entries that durable event automations may select.
5424
+ *
5425
+ * Core Workspace snapshot events only wake temporary watches. Snapshot finish
5426
+ * does not run event-automation ingest, so those namespaces stay out of create/update
5427
+ * schemas and trigger availability.
5428
+ */
5429
+ function pluginEventCatalog(catalog) {
5430
+ return Object.fromEntries(Object.entries(catalog).filter(([namespace]) => namespace !== CORE_EVENT_NAMESPACE));
5431
+ }
5432
+ function enumSchema(values, unavailableMessage) {
5433
+ if (values.length === 0) return z.string().refine(() => false, unavailableMessage);
5434
+ return z.enum(values);
5435
+ }
5436
+ /** Build the model-facing namespace enum from enabled event plugins. */
5437
+ function eventNamespaceSchema(catalog) {
5438
+ return enumSchema(Object.keys(catalog).sort(), "No event namespaces are enabled.");
5439
+ }
5440
+ /** Build the model-facing event enum from enabled plugin registrations. */
5441
+ function registeredEventTypeSchema(catalog) {
5442
+ return enumSchema([...new Set(Object.values(catalog).flatMap((registration) => registration.resourceTypes.flatMap((resourceType) => resourceType.supportedEvents)))].sort(), "No event types are enabled.");
5443
+ }
5444
+ /** Build the model-facing resource type enum from enabled plugin registrations. */
5445
+ function registeredResourceTypeSchema(catalog) {
5446
+ return enumSchema([...new Set(Object.values(catalog).flatMap((registration) => registration.resourceTypes.map((resourceType) => resourceType.type)))].sort(), "No resource types are enabled.");
5447
+ }
5448
+ /** Return whether one enabled plugin declared an event type. */
5449
+ function pluginSupportsEvent(catalog, namespace, resourceType, eventType) {
5450
+ return catalog[namespace]?.resourceTypes.find((candidate) => candidate.type === resourceType)?.supportedEvents.includes(eventType) ?? false;
5451
+ }
5452
+ /** Return app guidance for one registered resource type and event type. */
5453
+ function eventGuidance(catalog, namespace, resourceType, eventType) {
5454
+ return catalog[namespace]?.resourceTypes.find((candidate) => candidate.type === resourceType)?.guidance?.[eventType];
5455
+ }
5456
+ /** Return declared match keys for one registered resource type. */
5457
+ function eventMatchFields(catalog, namespace, resourceType) {
5458
+ return catalog[namespace]?.resourceTypes.find((candidate) => candidate.type === resourceType)?.matchFields;
5459
+ }
5460
+ /** Build the match object the tools accept for one resource type. */
5461
+ function registeredEventMatchSchema() {
5462
+ return eventMatchSchema.optional().describe("Optional exact values from the resource type matchFields. A single value must equal the event data. A list means the event value may be any one entry. Omit a key to ignore it. Unmatched events are dropped with no agent turn.");
5463
+ }
5464
+ /** Reject match keys the resource type does not list. */
5465
+ function requireSupportedEventMatch(catalog, input) {
5466
+ const match = input.match;
5467
+ if (!match || Object.keys(match).length === 0) return void 0;
5468
+ const fields = eventMatchFields(catalog, input.namespace, input.resourceType);
5469
+ if (!fields || Object.keys(fields).length === 0) throw new ToolInputError(`Resource type "${input.namespace}:${input.resourceType}" does not support match.`);
5470
+ const normalized = {};
5471
+ for (const [key, value] of Object.entries(match)) {
5472
+ const field = fields[key];
5473
+ if (!field) throw new ToolInputError(`Resource type "${input.namespace}:${input.resourceType}" does not support match key "${key}".`);
5474
+ if (Array.isArray(value)) {
5475
+ if (field.kind === "boolean") throw new ToolInputError(`Match key "${key}" is boolean and cannot use a list.`);
5476
+ for (const entry of value) {
5477
+ if (typeof entry !== field.kind) throw new ToolInputError(`Match key "${key}" expects ${field.kind} values.`);
5478
+ if (field.enum && typeof entry === "string" && !field.enum.includes(entry)) throw new ToolInputError(`Match key "${key}" does not allow value "${entry}".`);
5479
+ }
5480
+ normalized[key] = [...new Set(value)];
5481
+ continue;
5482
+ }
5483
+ if (typeof value !== field.kind) throw new ToolInputError(`Match key "${key}" expects a ${field.kind} value.`);
5484
+ if (field.enum && typeof value === "string" && !field.enum.includes(value)) throw new ToolInputError(`Match key "${key}" does not allow value "${value}".`);
5485
+ normalized[key] = value;
5486
+ }
5487
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
5488
+ }
5489
+ /** Normalize one selector with the convention declared by its plugin. */
5490
+ function normalizeCatalogEventIdentifier(catalog, namespace, identifier) {
5491
+ return normalizeEventIdentifier(catalog[namespace], identifier);
5492
+ }
5493
+ //#endregion
5494
+ //#region src/chat/sandbox/snapshot/events.ts
5495
+ const WORKSPACE_SNAPSHOT_NAMESPACE = CORE_EVENT_NAMESPACE;
5496
+ const WORKSPACE_SNAPSHOT_RESOURCE_TYPE = "workspace_snapshot";
5497
+ const WORKSPACE_SNAPSHOT_READY_EVENT = "workspace_snapshot.ready";
5498
+ const WORKSPACE_SNAPSHOT_FAILED_EVENT = "workspace_snapshot.failed";
5499
+ /**
5500
+ * Core catalog entry for Workspace snapshot ready and failed events.
5501
+ *
5502
+ * Uses the same registration shape as plugins so search, guidance, and tool
5503
+ * schemas treat these events like any other resource type.
5504
+ */
5505
+ function workspaceSnapshotEvents() {
5506
+ return { resourceTypes: [{
5507
+ type: WORKSPACE_SNAPSHOT_RESOURCE_TYPE,
5508
+ supportedEvents: [WORKSPACE_SNAPSHOT_READY_EVENT, WORKSPACE_SNAPSHOT_FAILED_EVENT],
5509
+ suggestedEvents: [],
5510
+ guidance: {
5511
+ [WORKSPACE_SNAPSHOT_READY_EVENT]: "Call switchWorkspace again with the same Workspace name. Do not create another watch when switchWorkspace already returned a subscription.",
5512
+ [WORKSPACE_SNAPSHOT_FAILED_EVENT]: "Report the snapshot failure. Do not keep waiting for this Workspace unless the user asks to retry."
5513
+ }
5514
+ }] };
5515
+ }
5516
+ /** Describe the events that report when a snapshot build ends. */
5517
+ function workspaceSnapshotWatch(input) {
5518
+ return {
5519
+ identifier: input.workspaceId,
5520
+ label: `Workspace ${input.workspaceName} snapshot`,
5521
+ namespace: WORKSPACE_SNAPSHOT_NAMESPACE,
5522
+ supportedEvents: [WORKSPACE_SNAPSHOT_READY_EVENT, WORKSPACE_SNAPSHOT_FAILED_EVENT],
5523
+ suggestedEvents: [],
5524
+ type: WORKSPACE_SNAPSHOT_RESOURCE_TYPE
5525
+ };
5526
+ }
5527
+ /** Report that a Workspace snapshot build is ready or failed. */
5528
+ function workspaceSnapshotFinishedEvent(input) {
5529
+ const occurredAtMs = input.occurredAtMs ?? Date.now();
5530
+ const eventType = input.status === "ready" ? WORKSPACE_SNAPSHOT_READY_EVENT : WORKSPACE_SNAPSHOT_FAILED_EVENT;
5531
+ const trustedSummary = input.status === "ready" ? "Workspace snapshot is ready." : "Workspace snapshot build failed.";
5532
+ const event = {
5533
+ eventKey: `${WORKSPACE_SNAPSHOT_NAMESPACE}:${input.workspaceId}:${input.resultId}:${input.status}`,
5534
+ eventType,
5535
+ identifier: input.workspaceId,
5536
+ namespace: WORKSPACE_SNAPSHOT_NAMESPACE,
5537
+ occurredAtMs,
5538
+ terminal: true,
5539
+ trustedSummary,
5540
+ data: {
5541
+ workspaceId: input.workspaceId,
5542
+ status: input.status
5543
+ }
5544
+ };
5545
+ if (input.status === "failed" && input.error) event.untrustedText = input.error;
5546
+ return event;
5547
+ }
5548
+ //#endregion
5549
+ //#region src/chat/events/runtime-catalog.ts
5550
+ /**
5551
+ * Enabled event registrations for search, guidance, and tool schemas.
5552
+ *
5553
+ * Includes core Workspace snapshot events under the `junior` namespace, plus
5554
+ * every enabled plugin registration. Plugins may not use the `junior` namespace.
5555
+ */
5556
+ function getEventCatalog() {
5557
+ const catalog = { [CORE_EVENT_NAMESPACE]: { resourceTypes: [...workspaceSnapshotEvents().resourceTypes, {
5558
+ type: "timer",
5559
+ supportedEvents: ["timer.fired"],
5560
+ suggestedEvents: []
5561
+ }] } };
5562
+ for (const plugin of getPlugins()) {
5563
+ const registration = plugin.events;
5564
+ if (!registration || registration.isEnabled?.() === false) continue;
5565
+ if (plugin.manifest.name === "junior") throw new Error(`Plugin "${CORE_EVENT_NAMESPACE}" cannot register events; that namespace is reserved for core events`);
5566
+ catalog[plugin.manifest.name] = registration;
5567
+ }
5568
+ return catalog;
5569
+ }
5570
+ //#endregion
3955
5571
  //#region src/chat/events/notification.ts
3956
5572
  const EVENT_WAIT_MS = 3e4;
3957
5573
  /**
@@ -4470,13 +6086,13 @@ async function resolveWorkspaceSnapshot(params) {
4470
6086
  if (!isSandboxApiTransientError(error)) throw error;
4471
6087
  advanced = true;
4472
6088
  if (shouldStopForTimeLimit(current.shouldStop)) throw new WorkspaceSnapshotNotReadyError(workspace.name);
4473
- await sleep$1(TRANSIENT_API_RETRY_MS, params.signal);
6089
+ await sleep$2(TRANSIENT_API_RETRY_MS, params.signal);
4474
6090
  continue;
4475
6091
  }
4476
6092
  advanced = true;
4477
6093
  if (snapshot) return snapshot;
4478
6094
  if (shouldStopForTimeLimit(current.shouldStop)) throw new WorkspaceSnapshotNotReadyError(workspace.name);
4479
- await sleep$1(WAIT_POLL_MS, params.signal);
6095
+ await sleep$2(WAIT_POLL_MS, params.signal);
4480
6096
  }
4481
6097
  }
4482
6098
  //#endregion
@@ -4894,7 +6510,7 @@ function createSandboxRuntime(options) {
4894
6510
  }));
4895
6511
  } catch (error) {
4896
6512
  if (!isSnapshottingError(error) || attempt === 2) throw error;
4897
- await sleep$1(SNAPSHOT_BOOT_RETRY_DELAY_MS, signal);
6513
+ await sleep$2(SNAPSHOT_BOOT_RETRY_DELAY_MS, signal);
4898
6514
  }
4899
6515
  }
4900
6516
  throw new Error(`Failed to boot sandbox from snapshot ${snapshotId}`);
@@ -8328,7 +9944,8 @@ const listedResourceWatchSchema = z.object({
8328
9944
  events: z.array(z.string().min(1)).min(1),
8329
9945
  match: z.record(z.string(), z.unknown()).optional(),
8330
9946
  intent: z.string().min(1),
8331
- expiresAtMs: z.number().finite()
9947
+ expiresAtMs: z.number().finite(),
9948
+ firesAtMs: z.number().finite().optional()
8332
9949
  }).strict();
8333
9950
  const resultDataSchema$2 = z.object({ subscriptions: z.array(listedResourceWatchSchema) }).strict();
8334
9951
  const outputSchema$5 = juniorToolOutputSchema.merge(resultDataSchema$2);
@@ -8356,7 +9973,8 @@ function createListWatchesTool(context) {
8356
9973
  events: subscription.events,
8357
9974
  ...subscription.match ? { match: subscription.match } : void 0,
8358
9975
  intent: subscription.intent,
8359
- expiresAtMs: subscription.expiresAtMs
9976
+ expiresAtMs: subscription.expiresAtMs,
9977
+ firesAtMs: subscription.firesAtMs
8360
9978
  })) };
8361
9979
  }
8362
9980
  });
@@ -8466,6 +10084,7 @@ function createStopWatchingResourcesTool(context) {
8466
10084
  id
8467
10085
  });
8468
10086
  if (!stopped) throw new ToolInputError("Watch was not found in the current conversation.");
10087
+ if (stopped.status === "completed") throw new ToolInputError("Watch already completed; delivered input cannot be recalled.");
8469
10088
  stoppedIds = [stopped.id];
8470
10089
  } else {
8471
10090
  const subscriptions = await listWatches({ conversationId: context.conversationId });
@@ -8480,6 +10099,40 @@ function createStopWatchingResourcesTool(context) {
8480
10099
  });
8481
10100
  }
8482
10101
  //#endregion
10102
+ //#region src/chat/tools/watch-timer.ts
10103
+ /** Create one heartbeat-backed Watch in the current Conversation. */
10104
+ function createWatchTimerTool(context) {
10105
+ return zodTool({
10106
+ annotations: {
10107
+ destructiveHint: false,
10108
+ idempotentHint: true,
10109
+ openWorldHint: false,
10110
+ readOnlyHint: false
10111
+ },
10112
+ description: "Wake this conversation once after a delay so you can check back later. Returns immediately. Timing is approximate. Use watchEvents instead when you can watch for the event.",
10113
+ inputSchema: z.object({
10114
+ afterMs: z.number().int().positive().max(RESOURCE_SUBSCRIPTION_MAX_TTL_MS - 864e5).describe("Delay in milliseconds, up to 29 days."),
10115
+ intent: z.string().trim().min(1).max(1e3).describe("What to do when the timer fires.")
10116
+ }).strict(),
10117
+ outputSchema: z.object({
10118
+ id: z.string(),
10119
+ firesAtMs: z.number()
10120
+ }).strict(),
10121
+ async execute(input, options) {
10122
+ if (!options.toolCallId) throw new Error("Timer creation requires a tool-call identity");
10123
+ const watch = await createTimerWatch({
10124
+ ...input,
10125
+ conversationId: context.conversationId,
10126
+ toolCallId: options.toolCallId
10127
+ });
10128
+ return {
10129
+ id: watch.id,
10130
+ firesAtMs: watch.firesAtMs
10131
+ };
10132
+ }
10133
+ });
10134
+ }
10135
+ //#endregion
8483
10136
  //#region src/chat/tools/watch-events.ts
8484
10137
  function inputSchema(catalog) {
8485
10138
  return z.object({
@@ -8544,6 +10197,7 @@ function createWatchEventsTool(context, catalog) {
8544
10197
  inputSchema: inputSchema(catalog),
8545
10198
  outputSchema: outputSchema$2,
8546
10199
  async execute(input) {
10200
+ if (input.namespace === "junior" && input.resourceType === "timer") throw new ToolInputError("Use watchTimer to create a timer Watch.");
8547
10201
  const events = cleanStrings(input.events);
8548
10202
  for (const eventType of events) if (!pluginSupportsEvent(catalog, input.namespace, input.resourceType, eventType)) throw new ToolInputError(`Resource type "${input.namespace}:${input.resourceType}" does not support event "${eventType}".`);
8549
10203
  const intent = input.intent.trim();
@@ -8583,17 +10237,445 @@ function createWatchEventsTool(context, catalog) {
8583
10237
  }
8584
10238
  });
8585
10239
  }
8586
- //#endregion
8587
- //#region src/chat/tools/events.ts
8588
- /** Build the complete resource-watch tool set for this runtime context. */
8589
- function createEventTools(context, catalog) {
10240
+ //#endregion
10241
+ //#region src/chat/tools/events.ts
10242
+ /** Build the complete resource-watch tool set for this runtime context. */
10243
+ function createEventTools(context, catalog) {
10244
+ const enabled = hasEventCatalogEntries(catalog);
10245
+ return {
10246
+ watchTimer: createWatchTimerTool(context),
10247
+ ...enabled ? {
10248
+ searchEventTypes: createSearchEventTypesTool(catalog),
10249
+ watchEvents: createWatchEventsTool(context, catalog)
10250
+ } : void 0,
10251
+ listWatches: createListWatchesTool(context),
10252
+ stopWatchingResources: createStopWatchingResourcesTool(context)
10253
+ };
10254
+ }
10255
+ /** Validate the persisted Slack creator identity for an event automation. */
10256
+ const eventAutomationPrincipalSchema = z.object({
10257
+ slackUserId: actorUserIdSchema,
10258
+ fullName: z.string().optional(),
10259
+ userName: z.string().optional()
10260
+ }).strict();
10261
+ /** Validate one persisted event-automation selector and its presentation metadata. */
10262
+ const eventAutomationTriggerSchema = z.object({
10263
+ events: z.array(eventTypeSchema).min(1),
10264
+ label: z.string().min(1),
10265
+ match: eventMatchSchema.optional(),
10266
+ namespace: z.string().min(1),
10267
+ identifier: z.string().min(1).max(300),
10268
+ resourceType: z.string().min(1)
10269
+ }).strict();
10270
+ /** Event automations target a Slack channel or DM, never a message thread. */
10271
+ const eventAutomationDestinationSchema = slackDestinationSchema.omit({ threadTs: true });
10272
+ const eventAutomationOutcomeSchema = taskOutcomeSchema.extend({ destination: eventAutomationDestinationSchema });
10273
+ /** Validate one persisted event automation. */
10274
+ const eventAutomationSchema = z.object({
10275
+ id: z.string().min(1),
10276
+ createdAtMs: z.number().finite(),
10277
+ createdBy: eventAutomationPrincipalSchema,
10278
+ credentialMode: z.enum(["system", "creator"]),
10279
+ destination: eventAutomationDestinationSchema,
10280
+ destinationVisibility: destinationVisibilitySchema,
10281
+ /** Explicit visible effects after successful work. An empty list is silent. */
10282
+ outcomes: z.array(eventAutomationOutcomeSchema).max(5),
10283
+ task: z.object({ text: z.string().min(1) }).strict(),
10284
+ trigger: eventAutomationTriggerSchema
10285
+ }).strict();
10286
+ //#endregion
10287
+ //#region src/chat/event-automations/store.ts
10288
+ const retainedEventAutomationSchema = eventAutomationSchema.extend({
10289
+ destination: slackDestinationSchema,
10290
+ outcomes: z.array(taskOutcomeSchema).max(5)
10291
+ });
10292
+ /** JSON task payload must not carry SQL-backed columns. */
10293
+ function eventAutomationJsonPayload(task) {
10294
+ const { status: _status, title: _title, ...payload } = task;
10295
+ return payload;
10296
+ }
10297
+ function parseTask(row) {
10298
+ const raw = row.task && typeof row.task === "object" ? { ...row.task } : row.task;
10299
+ if (raw && typeof raw === "object" && "title" in raw) delete raw.title;
10300
+ if (raw && typeof raw === "object" && !("outcomes" in raw) && "destination" in raw) raw.outcomes = [{
10301
+ action: "send_message",
10302
+ destination: raw.destination
10303
+ }];
10304
+ const retained = retainedEventAutomationSchema.parse(raw);
10305
+ const { threadTs: _threadTs, ...destination } = retained.destination;
10306
+ const title = row.title?.trim();
10307
+ return {
10308
+ ...retained,
10309
+ destination,
10310
+ outcomes: retained.outcomes.map((outcome) => {
10311
+ const { threadTs: _threadTs, ...destination } = outcome.destination;
10312
+ return {
10313
+ ...outcome,
10314
+ destination
10315
+ };
10316
+ }),
10317
+ status: row.status === "deleted" ? "deleted" : "active",
10318
+ ...title ? { title } : void 0
10319
+ };
10320
+ }
10321
+ function activeEventAutomationWhere() {
10322
+ return ne(juniorEventAutomations.status, "deleted");
10323
+ }
10324
+ /** Read one event automation by id, including deleted rows when present. */
10325
+ async function getEventAutomation(db, id) {
10326
+ const rows = await db.select({
10327
+ status: juniorEventAutomations.status,
10328
+ task: juniorEventAutomations.task,
10329
+ title: juniorEventAutomations.title
10330
+ }).from(juniorEventAutomations).where(eq(juniorEventAutomations.id, id)).limit(1);
10331
+ return rows[0] ? parseTask(rows[0]) : void 0;
10332
+ }
10333
+ /** Create one retry-stable event automation, or revive a deleted row with the new payload. */
10334
+ async function createEventAutomation(db, task) {
10335
+ const parsed = eventAutomationSchema.parse(eventAutomationJsonPayload(task));
10336
+ const title = task.title?.trim() || null;
10337
+ await db.insert(juniorEventAutomations).values({
10338
+ id: parsed.id,
10339
+ teamId: parsed.destination.teamId,
10340
+ namespace: parsed.trigger.namespace,
10341
+ identifier: parsed.trigger.identifier,
10342
+ createdAtMs: parsed.createdAtMs,
10343
+ status: "active",
10344
+ title,
10345
+ task: parsed
10346
+ }).onConflictDoNothing();
10347
+ const existing = await getEventAutomation(db, parsed.id);
10348
+ if (!existing) return {
10349
+ ...parsed,
10350
+ status: "active",
10351
+ ...title ? { title } : void 0
10352
+ };
10353
+ if (existing.status !== "deleted") return existing;
10354
+ const rows = await db.update(juniorEventAutomations).set({
10355
+ teamId: parsed.destination.teamId,
10356
+ namespace: parsed.trigger.namespace,
10357
+ identifier: parsed.trigger.identifier,
10358
+ status: "active",
10359
+ title,
10360
+ task: parsed
10361
+ }).where(and(eq(juniorEventAutomations.id, parsed.id), eq(juniorEventAutomations.status, "deleted"))).returning({
10362
+ status: juniorEventAutomations.status,
10363
+ task: juniorEventAutomations.task,
10364
+ title: juniorEventAutomations.title
10365
+ });
10366
+ return rows[0] ? parseTask(rows[0]) : await getEventAutomation(db, parsed.id) ?? {
10367
+ ...parsed,
10368
+ status: "active",
10369
+ ...title ? { title } : void 0
10370
+ };
10371
+ }
10372
+ /** Replace an existing non-deleted event automation. */
10373
+ async function saveEventAutomation(db, task) {
10374
+ const parsed = eventAutomationSchema.parse(eventAutomationJsonPayload(task));
10375
+ const title = task.title?.trim() || null;
10376
+ const rows = await db.update(juniorEventAutomations).set({
10377
+ namespace: parsed.trigger.namespace,
10378
+ identifier: parsed.trigger.identifier,
10379
+ title,
10380
+ task: parsed
10381
+ }).where(and(eq(juniorEventAutomations.id, parsed.id), activeEventAutomationWhere())).returning({
10382
+ status: juniorEventAutomations.status,
10383
+ task: juniorEventAutomations.task,
10384
+ title: juniorEventAutomations.title
10385
+ });
10386
+ return rows[0] ? parseTask(rows[0]) : void 0;
10387
+ }
10388
+ /** Mark one existing event automation deleted while retaining the row for history. */
10389
+ async function deleteEventAutomation(db, id) {
10390
+ const rows = await db.update(juniorEventAutomations).set({ status: "deleted" }).where(and(eq(juniorEventAutomations.id, id), activeEventAutomationWhere())).returning({
10391
+ status: juniorEventAutomations.status,
10392
+ task: juniorEventAutomations.task,
10393
+ title: juniorEventAutomations.title
10394
+ });
10395
+ return rows[0] ? parseTask(rows[0]) : void 0;
10396
+ }
10397
+ /** List live event automations in one Slack workspace. */
10398
+ async function listEventAutomationsForTeam(db, teamId) {
10399
+ return (await db.select({
10400
+ status: juniorEventAutomations.status,
10401
+ task: juniorEventAutomations.task,
10402
+ title: juniorEventAutomations.title
10403
+ }).from(juniorEventAutomations).where(and(eq(juniorEventAutomations.teamId, teamId), activeEventAutomationWhere())).orderBy(asc(juniorEventAutomations.createdAtMs), asc(juniorEventAutomations.id))).map(parseTask);
10404
+ }
10405
+ function viewerSlackIdentities(user) {
10406
+ return user.identities.filter((identity) => identity.provider === "slack" && Boolean(identity.providerTenantId) && Boolean(identity.providerSubjectId));
10407
+ }
10408
+ /** Return whether one event automation was created by a viewer-linked Slack identity. */
10409
+ function eventAutomationBelongsToUser(task, user) {
10410
+ return viewerSlackIdentities(user).some((identity) => identity.providerTenantId === task.destination.teamId && identity.providerSubjectId === task.createdBy.slackUserId);
10411
+ }
10412
+ /** List a bounded newest-first page of live event automations created by one user. */
10413
+ async function listEventAutomationsCreatedBy(db, user, limit, query) {
10414
+ const identities = viewerSlackIdentities(user);
10415
+ const ownership = or(...identities.map((identity) => and(eq(juniorEventAutomations.teamId, identity.providerTenantId), sql`${juniorEventAutomations.task}->'createdBy'->>'slackUserId' = ${identity.providerSubjectId}`)));
10416
+ if (!ownership) return [];
10417
+ return (await db.select({
10418
+ status: juniorEventAutomations.status,
10419
+ task: juniorEventAutomations.task,
10420
+ title: juniorEventAutomations.title
10421
+ }).from(juniorEventAutomations).where(and(ownership, activeEventAutomationWhere(), query ? sql`strpos(lower(coalesce(${juniorEventAutomations.title}, ${juniorEventAutomations.task}->'task'->>'text')), ${query}) > 0` : void 0)).orderBy(desc(juniorEventAutomations.createdAtMs), desc(juniorEventAutomations.id)).limit(limit)).map(parseTask);
10422
+ }
10423
+ /**
10424
+ * List deleted event automations created by one user, newest first.
10425
+ * Used only to keep historical runs after the live automation list drops the row.
10426
+ */
10427
+ async function listDeletedEventAutomationsCreatedBy(db, user, limit) {
10428
+ const identities = viewerSlackIdentities(user);
10429
+ const ownership = or(...identities.map((identity) => and(eq(juniorEventAutomations.teamId, identity.providerTenantId), sql`${juniorEventAutomations.task}->'createdBy'->>'slackUserId' = ${identity.providerSubjectId}`)));
10430
+ if (!ownership) return [];
10431
+ return (await db.select({
10432
+ status: juniorEventAutomations.status,
10433
+ task: juniorEventAutomations.task,
10434
+ title: juniorEventAutomations.title
10435
+ }).from(juniorEventAutomations).where(and(ownership, eq(juniorEventAutomations.status, "deleted"))).orderBy(desc(juniorEventAutomations.createdAtMs), desc(juniorEventAutomations.id)).limit(limit)).map(parseTask);
10436
+ }
10437
+ /** List live event automations whose current Slack destination is public. */
10438
+ async function listPublicEventAutomationsForTeams(db, teamIds, limit, query) {
10439
+ if (teamIds.length === 0) return [];
10440
+ return (await db.select({
10441
+ status: juniorEventAutomations.status,
10442
+ task: juniorEventAutomations.task,
10443
+ title: juniorEventAutomations.title
10444
+ }).from(juniorEventAutomations).innerJoin(juniorDestinations, and(eq(juniorDestinations.provider, "slack"), eq(juniorDestinations.providerTenantId, juniorEventAutomations.teamId), sql`${juniorDestinations.providerDestinationId} = ${juniorEventAutomations.task}->'destination'->>'channelId'`)).where(and(inArray(juniorEventAutomations.teamId, teamIds), activeEventAutomationWhere(), query ? sql`strpos(lower(coalesce(${juniorEventAutomations.title}, ${juniorEventAutomations.task}->'task'->>'text')), ${query}) > 0` : void 0, eq(juniorDestinations.visibility, "public"))).orderBy(desc(juniorEventAutomations.createdAtMs), desc(juniorEventAutomations.id)).limit(limit)).map(parseTask);
10445
+ }
10446
+ /**
10447
+ * Collect match keys from live event automations for these identifiers and event types.
10448
+ * Does not evaluate match values — only reports keys that filters use.
10449
+ */
10450
+ async function collectEventAutomationMatchKeys(db, input) {
10451
+ const eventTypes = new Set(input.eventTypes.map((eventType) => eventType.trim()).filter(Boolean));
10452
+ const identifiers = [...new Set(input.identifiers.map((value) => value.trim()).filter(Boolean))];
10453
+ if (eventTypes.size === 0 || identifiers.length === 0) return [];
10454
+ const rows = await db.select({
10455
+ status: juniorEventAutomations.status,
10456
+ task: juniorEventAutomations.task,
10457
+ title: juniorEventAutomations.title
10458
+ }).from(juniorEventAutomations).where(and(eq(juniorEventAutomations.teamId, input.teamId), eq(juniorEventAutomations.namespace, input.namespace), inArray(juniorEventAutomations.identifier, identifiers), activeEventAutomationWhere())).orderBy(asc(juniorEventAutomations.createdAtMs), asc(juniorEventAutomations.id));
10459
+ const keys = /* @__PURE__ */ new Set();
10460
+ for (const row of rows) {
10461
+ const task = parseTask(row);
10462
+ if (!task.trigger.events.some((eventType) => eventTypes.has(eventType))) continue;
10463
+ for (const key of Object.keys(task.trigger.match ?? {})) keys.add(key);
10464
+ }
10465
+ return [...keys].sort();
10466
+ }
10467
+ /** Find every live task matching one normalized event. */
10468
+ async function findMatchingEventAutomations(db, event, teamId) {
10469
+ return (await db.select({
10470
+ status: juniorEventAutomations.status,
10471
+ task: juniorEventAutomations.task,
10472
+ title: juniorEventAutomations.title
10473
+ }).from(juniorEventAutomations).where(and(eq(juniorEventAutomations.teamId, teamId), eq(juniorEventAutomations.namespace, event.namespace), eq(juniorEventAutomations.identifier, event.identifier), activeEventAutomationWhere())).orderBy(asc(juniorEventAutomations.createdAtMs), asc(juniorEventAutomations.id))).map(parseTask).filter((task) => task.trigger.events.includes(event.eventType) && eventMatches(task.trigger.match, event.data));
10474
+ }
10475
+ //#endregion
10476
+ //#region src/chat/task-outcomes.ts
10477
+ const taskMessageDestinationInputSchema = z.enum(["current_conversation", "task_creator"]).describe("Where to send the message. Use current_conversation for posts, digests, summaries, and channel reminders. Use task_creator only when the user asks for a direct reminder or notification.");
10478
+ /** Input accepted by task authoring tools before a user becomes a DM Destination. */
10479
+ const taskOutcomeInputSchema = z.object({
10480
+ action: z.literal("send_message"),
10481
+ destination: taskMessageDestinationInputSchema
10482
+ }).strict();
10483
+ /** Resolve explicit message outcomes to the Slack Destinations stored on an Automation. */
10484
+ async function resolveTaskOutcomes(outcomes, currentDestination, creatorSlackUserId) {
10485
+ if (outcomes === void 0) return [];
10486
+ const resolved = [];
10487
+ for (const outcome of outcomes) {
10488
+ if (outcome.destination === "current_conversation") {
10489
+ resolved.push({
10490
+ action: "send_message",
10491
+ destination: currentDestination
10492
+ });
10493
+ continue;
10494
+ }
10495
+ const userId = creatorSlackUserId;
10496
+ const channelId = (await withSlackRetries(() => getSlackClient().conversations.open({ users: userId }), 3, { action: "conversations.open" })).channel?.id;
10497
+ if (!channelId) throw new Error("Slack did not return a direct message destination.");
10498
+ resolved.push({
10499
+ action: "send_message",
10500
+ destination: {
10501
+ platform: "slack",
10502
+ teamId: currentDestination.teamId,
10503
+ channelId
10504
+ }
10505
+ });
10506
+ }
10507
+ return resolved;
10508
+ }
10509
+ function outcomeTargetsDestination(outcome, destination) {
10510
+ return outcome.destination.teamId === destination.teamId && outcome.destination.channelId === destination.channelId && (!outcome.destination.threadTs || outcome.destination.threadTs === destination.threadTs);
10511
+ }
10512
+ /** Bind same-channel message outcomes to the Automation Destination. */
10513
+ function effectiveTaskOutcomes(outcomes, destination) {
10514
+ return outcomes.map((outcome) => destination.threadTs && outcomeTargetsDestination(outcome, destination) ? {
10515
+ ...outcome,
10516
+ destination
10517
+ } : outcome);
10518
+ }
10519
+ /** Move outcomes that target the task Destination and keep other outcomes unchanged. */
10520
+ function moveTaskOutcomes(outcomes, currentDestination, nextDestination) {
10521
+ return effectiveTaskOutcomes(outcomes, currentDestination).map((outcome) => outcomeTargetsDestination(outcome, currentDestination) ? {
10522
+ ...outcome,
10523
+ destination: nextDestination
10524
+ } : outcome);
10525
+ }
10526
+ //#endregion
10527
+ //#region src/chat/event-automations/tool-support.ts
10528
+ const compactEventAutomationResultSchema = z.object({
10529
+ id: z.string().min(1),
10530
+ title: z.string().min(1).nullable(),
10531
+ dashboardUrl: z.string().url().nullable(),
10532
+ instruction: z.string().min(1),
10533
+ trigger: z.object({
10534
+ namespace: z.string().min(1),
10535
+ identifier: z.string().min(1),
10536
+ resourceType: z.string().min(1),
10537
+ label: z.string().min(1),
10538
+ events: z.array(z.string().min(1)).min(1),
10539
+ match: z.record(z.string(), z.unknown()).optional(),
10540
+ available: z.boolean()
10541
+ }).strict(),
10542
+ credentialMode: z.enum(["system", "creator"]),
10543
+ isCreator: z.boolean(),
10544
+ outcomes: z.array(z.object({
10545
+ action: z.literal("send_message"),
10546
+ destination: z.object({
10547
+ channel: z.string().min(1),
10548
+ thread: z.string().min(1).nullable()
10549
+ }).strict()
10550
+ }).strict()).max(5),
10551
+ createdBy: z.object({
10552
+ name: z.string().min(1).nullable(),
10553
+ username: z.string().min(1).nullable()
10554
+ }).strict()
10555
+ }).strict();
10556
+ /** Validate one successful event-automation mutation result. */
10557
+ const eventAutomationToolResultSchema = juniorToolOutputSchema.extend({
10558
+ automation: compactEventAutomationResultSchema,
10559
+ objectCards: z.array(ownedObjectAnnotationSchema)
10560
+ }).strict();
10561
+ /** Validate one successful event-automation list result. */
10562
+ const eventAutomationListToolResultSchema = juniorToolOutputSchema.extend({
10563
+ automations: z.array(compactEventAutomationResultSchema),
10564
+ truncated: z.boolean()
10565
+ }).strict();
10566
+ /** Build the validated event trigger accepted by event-automation tools. */
10567
+ function registeredEventAutomationTriggerSchema(catalog) {
10568
+ const plugins = pluginEventCatalog(catalog);
10569
+ return z.object({
10570
+ namespace: eventNamespaceSchema(plugins),
10571
+ identifier: z.string().trim().min(1).max(300),
10572
+ resourceType: registeredResourceTypeSchema(plugins),
10573
+ label: z.string().trim().min(1).max(500),
10574
+ events: z.array(registeredEventTypeSchema(plugins)).min(1),
10575
+ match: registeredEventMatchSchema()
10576
+ }).strict();
10577
+ }
10578
+ /** Reject an event-automation trigger that is no longer supported by the catalog. */
10579
+ function requireSupportedEventAutomationTrigger(catalog, trigger) {
10580
+ const plugins = pluginEventCatalog(catalog);
10581
+ for (const eventType of trigger.events) if (!pluginSupportsEvent(plugins, trigger.namespace, trigger.resourceType, eventType)) throw new ToolInputError(`Resource type "${trigger.namespace}:${trigger.resourceType}" does not support event "${eventType}".`);
10582
+ return requireSupportedEventMatch(plugins, {
10583
+ match: trigger.match,
10584
+ namespace: trigger.namespace,
10585
+ resourceType: trigger.resourceType
10586
+ });
10587
+ }
10588
+ /** Require the active Slack authority used for event-automation management. */
10589
+ function requireEventAutomationSlackContext(context) {
10590
+ if (context.source.kind !== "slack" || context.destination.platform !== "slack" || context.actor?.platform !== "slack" || context.actor.teamId !== context.source.teamId) throw new ToolInputError("Event automations require an active Slack channel or DM and actor.");
10591
+ return {
10592
+ actor: context.actor,
10593
+ destination: {
10594
+ platform: "slack",
10595
+ teamId: context.source.teamId,
10596
+ channelId: context.source.channelId
10597
+ },
10598
+ source: context.source
10599
+ };
10600
+ }
10601
+ /** Return whether an event automation belongs to the active Slack destination. */
10602
+ function eventAutomationMatchesDestination(task, destination) {
10603
+ return task.destination.teamId === destination.teamId && task.destination.channelId === destination.channelId;
10604
+ }
10605
+ /**
10606
+ * Return whether the active destination may update or delete this automation.
10607
+ * Same-destination automations stay local. Public automations may be managed by id from
10608
+ * another destination in the same workspace.
10609
+ */
10610
+ function eventAutomationIsWritableFrom(task, destination) {
10611
+ if (task.destination.teamId !== destination.teamId) return false;
10612
+ return task.destination.channelId === destination.channelId || task.destinationVisibility === "public";
10613
+ }
10614
+ /** Load one automation the active destination may update or delete. */
10615
+ async function writableEventAutomation(context, id) {
10616
+ const { destination } = requireEventAutomationSlackContext(context);
10617
+ const task = await getEventAutomation(getDb(), id);
10618
+ if (!task || task.status === "deleted" || !eventAutomationIsWritableFrom(task, destination)) throw new ToolInputError("Event automation was not found.");
10619
+ return task;
10620
+ }
10621
+ function eventAutomationTriggerAvailable(task, catalog) {
10622
+ const registration = pluginEventCatalog(catalog)[task.trigger.namespace];
10623
+ return Boolean(registration && task.trigger.events.every((eventType) => registration.resourceTypes.some((resourceType) => resourceType.supportedEvents.includes(eventType))));
10624
+ }
10625
+ /** Project an event automation into the bounded tool-result shape. */
10626
+ function compactEventAutomation(task, catalog, requesterSlackUserId) {
10627
+ return compactEventAutomationResultSchema.parse({
10628
+ id: task.id,
10629
+ title: task.title?.trim() || null,
10630
+ dashboardUrl: getDashboardTaskLink(task.id) ?? null,
10631
+ instruction: task.task.text,
10632
+ trigger: {
10633
+ namespace: task.trigger.namespace,
10634
+ identifier: task.trigger.identifier,
10635
+ resourceType: task.trigger.resourceType,
10636
+ label: task.trigger.label,
10637
+ events: task.trigger.events,
10638
+ ...task.trigger.match ? { match: task.trigger.match } : void 0,
10639
+ available: eventAutomationTriggerAvailable(task, catalog)
10640
+ },
10641
+ credentialMode: task.credentialMode,
10642
+ isCreator: task.createdBy.slackUserId === requesterSlackUserId,
10643
+ outcomes: effectiveTaskOutcomes(task.outcomes, task.destination).map((outcome) => ({
10644
+ action: outcome.action,
10645
+ destination: {
10646
+ channel: outcome.destination.channelId,
10647
+ thread: outcome.destination.threadTs ?? null
10648
+ }
10649
+ })),
10650
+ createdBy: {
10651
+ name: task.createdBy.fullName ?? null,
10652
+ username: task.createdBy.userName ?? null
10653
+ }
10654
+ });
10655
+ }
10656
+ /** Return the standard successful event-automation tool result. */
10657
+ async function eventAutomationToolResult(conversationId, task, catalog, requesterSlackUserId) {
10658
+ const automation = compactEventAutomation(task, catalog, requesterSlackUserId);
10659
+ const filters = Object.entries(task.trigger.match ?? {}).map(([key, value]) => `${key} = ${JSON.stringify(value)}`);
10660
+ const title = automation.title ?? fallbackShortTitle(task.task.text, "Event automation");
8590
10661
  return {
8591
- ...hasEventCatalogEntries(catalog) ? {
8592
- searchEventTypes: createSearchEventTypesTool(catalog),
8593
- watchEvents: createWatchEventsTool(context, catalog)
8594
- } : void 0,
8595
- listWatches: createListWatchesTool(context),
8596
- stopWatchingResources: createStopWatchingResourcesTool(context)
10662
+ automation,
10663
+ objectCards: await saveObjectAnnotations(conversationId, "junior", [{
10664
+ kind: "object",
10665
+ objectType: "automation",
10666
+ label: title,
10667
+ key: task.id,
10668
+ title,
10669
+ url: automation.dashboardUrl,
10670
+ description: task.task.text,
10671
+ status: automation.trigger.available ? "ready" : "unavailable",
10672
+ trigger: [
10673
+ task.trigger.label,
10674
+ task.trigger.events.join(", "),
10675
+ ...filters
10676
+ ].join(" · "),
10677
+ warning: !automation.trigger.available ? "Trigger unavailable. This automation cannot receive events." : void 0
10678
+ }])
8597
10679
  };
8598
10680
  }
8599
10681
  //#endregion
@@ -8839,6 +10921,324 @@ function createEventAutomationTools(context, catalog) {
8839
10921
  };
8840
10922
  }
8841
10923
  //#endregion
10924
+ //#region src/chat/scheduled-automations/types.ts
10925
+ /** Durable scheduled-automation domain types owned by Junior core. */
10926
+ const scheduledAutomationStatusSchema = z.enum([
10927
+ "active",
10928
+ "blocked",
10929
+ "completed",
10930
+ "deleted"
10931
+ ]);
10932
+ const scheduledAutomationCredentialModeSchema = z.enum(["system", "creator"]);
10933
+ const scheduledRunStatusSchema = z.enum([
10934
+ "pending",
10935
+ "running",
10936
+ "completed",
10937
+ "failed",
10938
+ "blocked",
10939
+ "skipped"
10940
+ ]);
10941
+ const scheduledAutomationPrincipalSchema = z.object({
10942
+ slackUserId: actorUserIdSchema,
10943
+ fullName: z.string().optional(),
10944
+ userName: z.string().optional()
10945
+ }).strict();
10946
+ const scheduledAutomationRecurrenceSchema = z.object({
10947
+ dayOfMonth: z.number().optional(),
10948
+ frequency: z.enum([
10949
+ "daily",
10950
+ "weekly",
10951
+ "monthly",
10952
+ "yearly"
10953
+ ]),
10954
+ interval: z.number(),
10955
+ month: z.number().optional(),
10956
+ startDate: z.string(),
10957
+ time: z.object({
10958
+ hour: z.number(),
10959
+ minute: z.number()
10960
+ }).strict(),
10961
+ weekdays: z.array(z.number()).optional()
10962
+ }).strict();
10963
+ const scheduledAutomationScheduleSchema = z.object({
10964
+ description: z.string(),
10965
+ kind: z.enum(["one_off", "recurring"]),
10966
+ recurrence: scheduledAutomationRecurrenceSchema.optional(),
10967
+ timezone: z.string()
10968
+ }).strict();
10969
+ const scheduledAutomationExecutionActorSchema = z.object({
10970
+ platform: z.literal("system"),
10971
+ name: z.string()
10972
+ }).strict();
10973
+ /** Scheduled automations target a Slack conversation, never one message thread. */
10974
+ const scheduledAutomationDestinationSchema = slackDestinationSchema.omit({ threadTs: true });
10975
+ const scheduledAutomationOutcomeSchema = z.object({
10976
+ action: z.literal("send_message"),
10977
+ destination: scheduledAutomationDestinationSchema
10978
+ }).strict();
10979
+ /** Validate the current scheduled-automation domain shape. */
10980
+ const scheduledAutomationSchema = z.object({
10981
+ id: z.string(),
10982
+ conversationAccess: z.object({
10983
+ audience: z.enum([
10984
+ "direct",
10985
+ "group",
10986
+ "channel"
10987
+ ]),
10988
+ visibility: z.enum(["private", "public"])
10989
+ }).strict(),
10990
+ createdAtMs: z.number(),
10991
+ createdBy: scheduledAutomationPrincipalSchema,
10992
+ /** Authoritative provider identity that created the task. */
10993
+ creatorIdentityId: z.string(),
10994
+ /** Selects system credentials or task-bound creator credential delegation. */
10995
+ credentialMode: scheduledAutomationCredentialModeSchema,
10996
+ destination: scheduledAutomationDestinationSchema,
10997
+ executionActor: scheduledAutomationExecutionActorSchema.optional(),
10998
+ lastRunAtMs: z.number().optional(),
10999
+ nextRunAtMs: z.number().optional(),
11000
+ originalRequest: z.string().optional(),
11001
+ runNowAtMs: z.number().optional(),
11002
+ schedule: scheduledAutomationScheduleSchema,
11003
+ status: scheduledAutomationStatusSchema,
11004
+ statusReason: z.string().optional(),
11005
+ /** Explicit visible effects after successful work. An empty list is silent. */
11006
+ outcomes: z.array(scheduledAutomationOutcomeSchema).max(5),
11007
+ task: z.object({ text: z.string() }).strict(),
11008
+ /** SQL-backed short display title generated from the task instruction. */
11009
+ title: z.string().optional(),
11010
+ updatedAtMs: z.number()
11011
+ }).strict();
11012
+ /** Validate the current scheduled-run domain shape. */
11013
+ const scheduledRunSchema = z.object({
11014
+ id: z.string(),
11015
+ attempt: z.number(),
11016
+ claimedAtMs: z.number(),
11017
+ completedAtMs: z.number().optional(),
11018
+ dispatchId: z.string().optional(),
11019
+ errorMessage: z.string().optional(),
11020
+ resultMessageTs: z.string().optional(),
11021
+ scheduledForMs: z.number(),
11022
+ startedAtMs: z.number().optional(),
11023
+ status: scheduledRunStatusSchema,
11024
+ taskId: z.string()
11025
+ }).strict();
11026
+ const SCHEDULED_AUTOMATION_SYSTEM_ACTOR = Object.freeze({
11027
+ platform: "system",
11028
+ name: "scheduled-automation"
11029
+ });
11030
+ //#endregion
11031
+ //#region src/chat/scheduled-automations/tasks.ts
11032
+ const SCHEDULER_KEY_PREFIX = "junior:scheduler";
11033
+ const retainedScheduledAutomationSchema = scheduledAutomationSchema.omit({
11034
+ creatorIdentityId: true,
11035
+ title: true
11036
+ }).extend({
11037
+ destination: slackDestinationSchema,
11038
+ outcomes: z.array(taskOutcomeSchema).max(5),
11039
+ status: z.enum([
11040
+ "active",
11041
+ "blocked",
11042
+ "completed",
11043
+ "deleted",
11044
+ "paused"
11045
+ ]),
11046
+ version: z.number().optional()
11047
+ }).strict();
11048
+ /** Decode a retained scheduler task row and reject invalid routing context. */
11049
+ function parseScheduledAutomationRow(row) {
11050
+ const record = z.record(z.string(), z.unknown()).safeParse(row.record);
11051
+ if (!record.success) return void 0;
11052
+ const { creatorIdentityId: legacyCreatorIdentityId, title: legacyTitle, ...retained } = record.data;
11053
+ const current = retained.outcomes === void 0 && retained.destination !== void 0 ? {
11054
+ ...retained,
11055
+ outcomes: [{
11056
+ action: "send_message",
11057
+ destination: retained.destination
11058
+ }]
11059
+ } : retained;
11060
+ const parsed = retainedScheduledAutomationSchema.safeParse(current);
11061
+ if (!parsed.success) return void 0;
11062
+ const { status, version: _version, ...retainedTask } = parsed.data;
11063
+ const destination = {
11064
+ platform: "slack",
11065
+ teamId: retainedTask.destination.teamId,
11066
+ channelId: retainedTask.destination.channelId
11067
+ };
11068
+ const task = {
11069
+ ...retainedTask,
11070
+ destination,
11071
+ outcomes: retainedTask.outcomes.map((outcome) => ({
11072
+ ...outcome,
11073
+ destination: {
11074
+ platform: "slack",
11075
+ teamId: outcome.destination.teamId,
11076
+ channelId: outcome.destination.channelId
11077
+ }
11078
+ }))
11079
+ };
11080
+ const fallbackIdentity = row.creatorIdentityId === null ? z.string().safeParse(legacyCreatorIdentityId) : void 0;
11081
+ const creatorIdentityId = row.creatorIdentityId ?? (fallbackIdentity?.success ? fallbackIdentity.data : void 0);
11082
+ if (creatorIdentityId === void 0) return void 0;
11083
+ const fallbackTitle = row.title === null ? z.string().safeParse(legacyTitle) : void 0;
11084
+ const title = (row.title !== null ? row.title : fallbackTitle?.success ? fallbackTitle.data : void 0)?.trim() || void 0;
11085
+ if (status === "paused") {
11086
+ const { nextRunAtMs: _nextRunAtMs, runNowAtMs: _runNowAtMs, ...retained } = task;
11087
+ return {
11088
+ ...retained,
11089
+ creatorIdentityId,
11090
+ status: "deleted",
11091
+ ...title ? { title } : void 0
11092
+ };
11093
+ }
11094
+ return {
11095
+ ...task,
11096
+ creatorIdentityId,
11097
+ status,
11098
+ ...title ? { title } : void 0
11099
+ };
11100
+ }
11101
+ /** Keep scan paths from returning current or legacy tombstones. */
11102
+ function isListedScheduledAutomation(task) {
11103
+ return task !== void 0 && task.status !== "deleted";
11104
+ }
11105
+ /** Build the stable run id for one task occurrence. */
11106
+ function scheduledRunId(taskId, scheduledForMs) {
11107
+ return `${taskId}:${scheduledForMs}`;
11108
+ }
11109
+ /** Acquire the deployed advisory lock for one scheduled automation. */
11110
+ function withScheduledAutomationLock(db, taskId, callback) {
11111
+ return db.transaction(async (tx) => {
11112
+ await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtext(${`${SCHEDULER_KEY_PREFIX}:task:${taskId}:lock`}))`);
11113
+ return await callback(tx);
11114
+ });
11115
+ }
11116
+ function requireStoredTask(task) {
11117
+ const parsed = scheduledAutomationSchema.safeParse(task);
11118
+ if (!parsed.success) throw new Error("Scheduled automation routing context is invalid.");
11119
+ const { title, ...current } = parsed.data;
11120
+ const normalizedTitle = title?.trim() || void 0;
11121
+ return {
11122
+ ...current,
11123
+ ...normalizedTitle ? { title: normalizedTitle } : void 0
11124
+ };
11125
+ }
11126
+ function scheduledAutomationJsonRecord(task) {
11127
+ const { title: _title, ...record } = task;
11128
+ return record;
11129
+ }
11130
+ async function upsertScheduledAutomation(db, task) {
11131
+ const title = task.title?.trim() || null;
11132
+ await db.insert(juniorSchedulerTasks).values({
11133
+ createdAtMs: task.createdAtMs,
11134
+ creatorIdentityId: task.creatorIdentityId,
11135
+ creatorSlackUserId: task.createdBy.slackUserId,
11136
+ id: task.id,
11137
+ nextRunAtMs: task.nextRunAtMs,
11138
+ record: scheduledAutomationJsonRecord(task),
11139
+ runNowAtMs: task.runNowAtMs,
11140
+ status: task.status,
11141
+ teamId: task.destination.teamId,
11142
+ title
11143
+ }).onConflictDoUpdate({
11144
+ target: juniorSchedulerTasks.id,
11145
+ set: {
11146
+ createdAtMs: sql`excluded.created_at_ms`,
11147
+ creatorIdentityId: sql`excluded.creator_identity_id`,
11148
+ creatorSlackUserId: sql`excluded.creator_slack_user_id`,
11149
+ nextRunAtMs: sql`excluded.next_run_at_ms`,
11150
+ record: sql`excluded.record`,
11151
+ runNowAtMs: sql`excluded.run_now_at_ms`,
11152
+ status: sql`excluded.status`,
11153
+ teamId: sql`excluded.team_id`,
11154
+ title: sql`excluded.title`
11155
+ }
11156
+ });
11157
+ }
11158
+ /** Read and decode one scheduled automation. */
11159
+ async function readScheduledAutomation(db, taskId) {
11160
+ const rows = await db.select({
11161
+ creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
11162
+ record: juniorSchedulerTasks.record,
11163
+ title: juniorSchedulerTasks.title
11164
+ }).from(juniorSchedulerTasks).where(eq(juniorSchedulerTasks.id, taskId)).limit(1);
11165
+ return rows[0] ? parseScheduledAutomationRow(rows[0]) : void 0;
11166
+ }
11167
+ async function readListedScheduledAutomations(db, teamId) {
11168
+ return (await db.select({
11169
+ creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
11170
+ record: juniorSchedulerTasks.record,
11171
+ title: juniorSchedulerTasks.title
11172
+ }).from(juniorSchedulerTasks).where(and(notInArray(juniorSchedulerTasks.status, ["deleted", "paused"]), teamId === void 0 ? void 0 : eq(juniorSchedulerTasks.teamId, teamId))).orderBy(asc(juniorSchedulerTasks.createdAtMs), asc(juniorSchedulerTasks.id))).map(parseScheduledAutomationRow).filter(isListedScheduledAutomation);
11173
+ }
11174
+ /** List decoded scheduled automations in stable creation order. */
11175
+ async function listScheduledAutomations(db) {
11176
+ return await readListedScheduledAutomations(db);
11177
+ }
11178
+ /** List decoded scheduled automations for one team in stable creation order. */
11179
+ async function listScheduledAutomationsForTeam(db, teamId) {
11180
+ return await readListedScheduledAutomations(db, teamId);
11181
+ }
11182
+ async function writeScheduledAutomation(db, task, current) {
11183
+ 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")));
11184
+ if (task.status === "deleted" && current?.status !== "deleted") await skipPendingRunsForDeletedTask(db, task);
11185
+ await upsertScheduledAutomation(db, task);
11186
+ }
11187
+ async function skipPendingRunsForDeletedTask(db, task) {
11188
+ const errorMessage = `Scheduled automation ${task.id} was deleted before the run started.`;
11189
+ const rows = await db.select({ record: juniorSchedulerRuns.record }).from(juniorSchedulerRuns).where(and(eq(juniorSchedulerRuns.taskId, task.id), eq(juniorSchedulerRuns.status, "pending")));
11190
+ for (const row of rows) {
11191
+ const parsed = scheduledRunSchema.safeParse(row.record);
11192
+ if (!parsed.success || parsed.data.status !== "pending") continue;
11193
+ const next = scheduledRunSchema.parse({
11194
+ ...parsed.data,
11195
+ completedAtMs: task.updatedAtMs,
11196
+ errorMessage,
11197
+ status: "skipped"
11198
+ });
11199
+ await db.update(juniorSchedulerRuns).set({
11200
+ record: next,
11201
+ status: next.status
11202
+ }).where(and(eq(juniorSchedulerRuns.id, next.id), eq(juniorSchedulerRuns.status, "pending")));
11203
+ }
11204
+ }
11205
+ /** Create a scheduled automation once under its retry-stable task lock. */
11206
+ async function createScheduledAutomation(db, task) {
11207
+ const next = requireStoredTask(task);
11208
+ return await withScheduledAutomationLock(db, task.id, async (tx) => {
11209
+ const current = await readScheduledAutomation(tx, task.id);
11210
+ if (current) return current;
11211
+ await writeScheduledAutomation(tx, next, void 0);
11212
+ return next;
11213
+ });
11214
+ }
11215
+ /** Save a scheduled automation and clear its blocked occurrence on reactivation. */
11216
+ async function saveScheduledAutomation(db, task) {
11217
+ const next = requireStoredTask(task);
11218
+ await withScheduledAutomationLock(db, task.id, async (tx) => {
11219
+ const current = await readScheduledAutomation(tx, task.id);
11220
+ await writeScheduledAutomation(tx, next, current);
11221
+ });
11222
+ return next;
11223
+ }
11224
+ /** Save a scheduled automation inside a task lock already held by the caller. */
11225
+ async function saveScheduledAutomationInLock(db, task, current) {
11226
+ await writeScheduledAutomation(db, requireStoredTask(task), current);
11227
+ }
11228
+ /** List scheduled automations whose current Slack destination is public. */
11229
+ async function listPublicScheduledAutomationsForTeams(db, teamIds, input) {
11230
+ if (teamIds.length === 0) return [];
11231
+ return (await db.select({
11232
+ creatorIdentityId: juniorSchedulerTasks.creatorIdentityId,
11233
+ record: juniorSchedulerTasks.record,
11234
+ title: juniorSchedulerTasks.title
11235
+ }).from(juniorSchedulerTasks).innerJoin(juniorDestinations, and(eq(juniorDestinations.provider, "slack"), eq(juniorDestinations.providerTenantId, juniorSchedulerTasks.teamId), sql`${juniorDestinations.providerDestinationId} = ${juniorSchedulerTasks.record}->'destination'->>'channelId'`)).where(and(inArray(juniorSchedulerTasks.teamId, teamIds), notInArray(juniorSchedulerTasks.status, [
11236
+ "completed",
11237
+ "deleted",
11238
+ "paused"
11239
+ ]), input.query ? sql`strpos(lower(coalesce(${juniorSchedulerTasks.title}, ${juniorSchedulerTasks.record}->'task'->>'text')), ${input.query}) > 0` : void 0, eq(juniorDestinations.visibility, "public"))).orderBy(desc(juniorSchedulerTasks.createdAtMs), desc(juniorSchedulerTasks.id)).limit(input.limit)).map(parseScheduledAutomationRow).filter(isListedScheduledAutomation);
11240
+ }
11241
+ //#endregion
8842
11242
  //#region src/chat/scheduled-automations/cadence.ts
8843
11243
  const FORMATTERS = /* @__PURE__ */ new Map();
8844
11244
  function getFormatter(timezone) {
@@ -9537,6 +11937,7 @@ async function scheduleAutomationToolResult(conversationId, task, requesterSlack
9537
11937
  title,
9538
11938
  url: automation.dashboardUrl,
9539
11939
  description: automation.instruction,
11940
+ status: automation.status,
9540
11941
  trigger: task.schedule.kind === "one_off" && task.nextRunAtMs !== void 0 ? `${new Intl.DateTimeFormat("en-US", {
9541
11942
  dateStyle: "medium",
9542
11943
  timeStyle: "short",
@@ -9640,7 +12041,7 @@ function createSlackScheduleCreateAutomationTool(context) {
9640
12041
  instruction: input.instruction,
9641
12042
  title: input.title
9642
12043
  });
9643
- const task = {
12044
+ const committed = await createScheduledAutomation(db, {
9644
12045
  id,
9645
12046
  createdAtMs: nowMs,
9646
12047
  updatedAtMs: nowMs,
@@ -9657,8 +12058,7 @@ function createSlackScheduleCreateAutomationTool(context) {
9657
12058
  outcomes: await resolveTaskOutcomes(input.outcomes, destination, actor.slackUserId),
9658
12059
  task: { text: input.instruction },
9659
12060
  ...title ? { title } : void 0
9660
- };
9661
- const committed = await createScheduledAutomation(db, task);
12061
+ });
9662
12062
  if (!sameDestination(committed, destination) || committed.createdBy.slackUserId !== actor.slackUserId) throwToolInputError("Scheduled automation operation identity is invalid.");
9663
12063
  logInfo("scheduled_automation.create.completed", scheduledAutomationAttributes(committed));
9664
12064
  return scheduleAutomationToolResult(context.conversationId, committed, actor.slackUserId);
@@ -9954,6 +12354,7 @@ function createScheduledAutomationTools(context) {
9954
12354
  }
9955
12355
  //#endregion
9956
12356
  //#region src/chat/slack/channel.ts
12357
+ const replySubtypeSchema = z.object({ subtype: z.string().optional() });
9957
12358
  /** List channel history using Slack-native, pre-validated timestamp bounds. */
9958
12359
  async function listChannelMessages(input) {
9959
12360
  const client = getSlackClient();
@@ -10005,7 +12406,10 @@ async function listThreadReplies(input) {
10005
12406
  cursor
10006
12407
  }), 3, { action: "conversations.replies" });
10007
12408
  const batch = response.messages ?? [];
10008
- replies.push(...batch);
12409
+ replies.push(...batch.map((reply) => ({
12410
+ ...reply,
12411
+ ...replySubtypeSchema.parse(reply)
12412
+ })));
10009
12413
  for (const reply of batch) if (typeof reply.ts === "string" && pendingTargets.size > 0) pendingTargets.delete(reply.ts);
10010
12414
  cursor = response.response_metadata?.next_cursor || void 0;
10011
12415
  if (!cursor || hasTargetMessages && pendingTargets.size === 0) break;
@@ -10020,15 +12424,14 @@ async function getConversationInfo(channelId) {
10020
12424
  idempotent: true
10021
12425
  })).channel;
10022
12426
  if (!channel || typeof channel !== "object") throw new Error(`Slack conversations.info returned no channel for ${channelId}`);
10023
- const record = channel;
10024
12427
  return {
10025
12428
  id: channelId,
10026
- ...typeof record.name === "string" && record.name ? { name: record.name } : void 0,
10027
- isChannel: record.is_channel === true,
10028
- isPrivate: record.is_private === true,
10029
- isIm: record.is_im === true,
10030
- isMpim: record.is_mpim === true,
10031
- ...typeof record.is_member === "boolean" ? { isMember: record.is_member } : void 0
12429
+ ...typeof channel.name === "string" && channel.name ? { name: channel.name } : void 0,
12430
+ isChannel: channel.is_channel === true,
12431
+ isPrivate: channel.is_private === true,
12432
+ isIm: channel.is_im === true,
12433
+ isMpim: channel.is_mpim === true,
12434
+ ...typeof channel.is_member === "boolean" ? { isMember: channel.is_member } : void 0
10032
12435
  };
10033
12436
  }
10034
12437
  /** Join a public channel so the bot can read history and threads there. */
@@ -10214,6 +12617,73 @@ function createSlackChannelJoinTool(context) {
10214
12617
  });
10215
12618
  }
10216
12619
  //#endregion
12620
+ //#region src/chat/slack/context.ts
12621
+ function toTrimmedSlackString(value) {
12622
+ return toOptionalString(value)?.trim() || void 0;
12623
+ }
12624
+ const slackMessageEnvelopeSchema = z.object({ raw: z.unknown().optional() });
12625
+ const rawSlackMessageSchema = z.object({
12626
+ channel: z.unknown().optional(),
12627
+ message: z.unknown().optional(),
12628
+ team: z.unknown().optional(),
12629
+ team_id: z.unknown().optional(),
12630
+ thread_ts: z.unknown().optional(),
12631
+ ts: z.unknown().optional(),
12632
+ user_team: z.unknown().optional()
12633
+ });
12634
+ const nestedRawSlackMessageSchema = z.object({ ts: z.unknown().optional() });
12635
+ /** Project only the Slack raw fields consumed by runtime thread context. */
12636
+ function readSlackRawMessageContext(message) {
12637
+ const envelope = slackMessageEnvelopeSchema.safeParse(message);
12638
+ if (!envelope.success) return;
12639
+ const raw = rawSlackMessageSchema.safeParse(envelope.data.raw);
12640
+ if (!raw.success) return;
12641
+ const nestedMessage = nestedRawSlackMessageSchema.safeParse(raw.data.message);
12642
+ const channelId = parseSlackChannelId(raw.data.channel);
12643
+ const threadTs = parseSlackMessageTs(raw.data.thread_ts);
12644
+ const messageTs = parseSlackMessageTs(raw.data.ts);
12645
+ const nestedMessageTs = nestedMessage.success && parseSlackMessageTs(nestedMessage.data.ts);
12646
+ const teamId = parseSlackTeamId(raw.data.team_id) ?? parseSlackTeamId(raw.data.team);
12647
+ const authorTeamId = parseSlackTeamId(raw.data.user_team);
12648
+ return {
12649
+ ...channelId ? { channelId } : void 0,
12650
+ ...threadTs ? { threadTs } : void 0,
12651
+ ...messageTs ? { messageTs } : void 0,
12652
+ ...nestedMessageTs ? { nestedMessageTs } : void 0,
12653
+ ...teamId ? { teamId } : void 0,
12654
+ ...authorTeamId ? { authorTeamId } : void 0
12655
+ };
12656
+ }
12657
+ /** Extract a channel ID and validated Slack timestamp from `slack:<channel>:<ts>`. */
12658
+ function parseSlackThreadId(threadId) {
12659
+ const normalizedThreadId = toTrimmedSlackString(threadId);
12660
+ if (!normalizedThreadId) return;
12661
+ const parts = normalizedThreadId.split(":");
12662
+ if (parts.length !== 3 || parts[0] !== "slack") return;
12663
+ const channelId = parseSlackChannelId(parts[1]);
12664
+ const threadTs = parseSlackMessageTs(parts[2]);
12665
+ if (!channelId || !threadTs) return;
12666
+ return {
12667
+ channelId,
12668
+ threadTs
12669
+ };
12670
+ }
12671
+ /** Resolve the Slack channel ID from a `slack:<channel>:<ts>` thread identifier. */
12672
+ function resolveSlackChannelIdFromThreadId(threadId) {
12673
+ return parseSlackThreadId(threadId)?.channelId;
12674
+ }
12675
+ /** Best-effort channel ID extraction from a raw Slack message payload. */
12676
+ function resolveSlackChannelIdFromMessage(message) {
12677
+ const messageChannelId = parseSlackChannelId(message.channelId);
12678
+ if (messageChannelId) return messageChannelId;
12679
+ const raw = message.raw;
12680
+ if (raw && typeof raw === "object") {
12681
+ const rawChannel = parseSlackChannelId(raw.channel);
12682
+ if (rawChannel) return rawChannel;
12683
+ }
12684
+ return resolveSlackChannelIdFromThreadId(toTrimmedSlackString(message.threadId));
12685
+ }
12686
+ //#endregion
10217
12687
  //#region src/chat/slack/timestamp-param.ts
10218
12688
  /** Define a model-facing Slack timestamp, normalizing numeric model args to strings. */
10219
12689
  function slackTimestampParam(description) {
@@ -10869,6 +13339,298 @@ function createSlackMessageAddReactionTool(context, state) {
10869
13339
  });
10870
13340
  }
10871
13341
  //#endregion
13342
+ //#region src/chat/attachments/store.ts
13343
+ const DAY_MS = 864e5;
13344
+ const ATTACHMENT_GC_BATCH_LIMIT = 200;
13345
+ /** Time that Junior keeps attachment bytes after the file is stored. */
13346
+ const ATTACHMENT_RETENTION_MS = 30 * DAY_MS;
13347
+ function attachmentDigest(file) {
13348
+ return createHash("sha256").update(file.data).digest("hex");
13349
+ }
13350
+ /** Stable conversation-owned id for one file body and presentation metadata. */
13351
+ function attachmentId(args) {
13352
+ const identity = [
13353
+ args.conversationId,
13354
+ args.storageProvider,
13355
+ args.sha256,
13356
+ args.filename,
13357
+ args.contentType
13358
+ ];
13359
+ if (args.source) identity.push(args.source.provider, args.source.id);
13360
+ return createHash("sha256").update(identity.join("\0")).digest("hex");
13361
+ }
13362
+ /**
13363
+ * Object key for one write attempt.
13364
+ *
13365
+ * Attachment ids are content-stable. Object keys are not: each put gets a fresh
13366
+ * key so a failed-write cleanup or GC delete cannot remove another writer's
13367
+ * durable blob.
13368
+ */
13369
+ function attachmentKey(args) {
13370
+ return `conversations/${args.conversationId}/attachments/${args.attachmentId}/${args.writeId}/${args.filename}`;
13371
+ }
13372
+ async function bestEffortDelete(storage, key) {
13373
+ try {
13374
+ await storage.delete([key]);
13375
+ } catch {}
13376
+ }
13377
+ async function isConversationPurged(db, conversationId) {
13378
+ const [row] = await db.db().select({ transcriptPurgedAt: juniorConversations.transcriptPurgedAt }).from(juniorConversations).where(eq(juniorConversations.conversationId, conversationId));
13379
+ return row?.transcriptPurgedAt != null;
13380
+ }
13381
+ function purgedConversationError(conversationId) {
13382
+ return /* @__PURE__ */ new Error(`Cannot store attachment for purged conversation ${conversationId}`);
13383
+ }
13384
+ async function assertConversationNotPurged(db, conversationId) {
13385
+ if (await isConversationPurged(db, conversationId)) throw purgedConversationError(conversationId);
13386
+ }
13387
+ /** Mark a just-written row for deletion and drop its unused object. */
13388
+ async function rejectPurgedAttachmentWrite(args) {
13389
+ await args.db.db().update(juniorAttachments).set({ deleteRequestedAt: args.now }).where(eq(juniorAttachments.id, args.id));
13390
+ await bestEffortDelete(args.storage, args.storageKey);
13391
+ throw purgedConversationError(args.conversationId);
13392
+ }
13393
+ /**
13394
+ * Return another writer's live row only when the conversation is still active.
13395
+ *
13396
+ * The caller already wrote a unique object for this attempt. Delete that unused
13397
+ * object either way. If the conversation is purged, do not report success for a
13398
+ * row that purge/GC is about to remove.
13399
+ */
13400
+ async function reuseLiveAttachment(args) {
13401
+ await bestEffortDelete(args.storage, args.storageKey);
13402
+ await assertConversationNotPurged(args.db, args.conversationId);
13403
+ return { id: args.id };
13404
+ }
13405
+ /**
13406
+ * Persist one conversation-owned file.
13407
+ *
13408
+ * Identity is content-stable under the conversation. Object storage is written
13409
+ * first under a unique key; the SQL row is created only after that write
13410
+ * succeeds. A row means the attachment is durable. Retries reuse an existing
13411
+ * live row. A purge-marked row is revived by writing a new object and pointing
13412
+ * the row at it, unless the conversation is still purged. If the SQL write
13413
+ * fails after object storage accepts the bytes, this path deletes that unique
13414
+ * object. A process crash between those steps can leave an unreferenced unique
13415
+ * object with no SQL row.
13416
+ */
13417
+ async function storeAttachment(args) {
13418
+ const now = new Date(args.nowMs ?? Date.now());
13419
+ const sha256 = attachmentDigest(args.file);
13420
+ const id = attachmentId({
13421
+ conversationId: args.conversationId,
13422
+ contentType: args.file.mimeType,
13423
+ filename: args.file.filename,
13424
+ source: args.source,
13425
+ storageProvider: args.storage.provider,
13426
+ sha256
13427
+ });
13428
+ const [existing] = await args.db.db().select({
13429
+ contentType: juniorAttachments.contentType,
13430
+ conversationId: juniorAttachments.conversationId,
13431
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
13432
+ filename: juniorAttachments.filename,
13433
+ id: juniorAttachments.id,
13434
+ provider: juniorAttachments.provider,
13435
+ providerId: juniorAttachments.providerId,
13436
+ storageProvider: juniorAttachments.storageProvider,
13437
+ sha256: juniorAttachments.sha256,
13438
+ storageKey: juniorAttachments.storageKey
13439
+ }).from(juniorAttachments).where(eq(juniorAttachments.id, id));
13440
+ if (existing) {
13441
+ if (existing.contentType !== args.file.mimeType || existing.conversationId !== args.conversationId || existing.filename !== args.file.filename || existing.provider !== (args.source?.provider ?? null) || existing.providerId !== (args.source?.id ?? null) || existing.storageProvider !== args.storage.provider || existing.sha256 !== sha256) throw new Error(`Attachment write conflicts with ${existing.id}`);
13442
+ if (existing.deleteRequestedAt === null) {
13443
+ await assertConversationNotPurged(args.db, args.conversationId);
13444
+ return { id: existing.id };
13445
+ }
13446
+ }
13447
+ await assertConversationNotPurged(args.db, args.conversationId);
13448
+ const storageKey = attachmentKey({
13449
+ attachmentId: id,
13450
+ conversationId: args.conversationId,
13451
+ filename: args.file.filename,
13452
+ writeId: randomUUID()
13453
+ });
13454
+ await args.storage.put({
13455
+ body: args.file.data,
13456
+ contentType: args.file.mimeType,
13457
+ key: storageKey
13458
+ });
13459
+ if (existing?.deleteRequestedAt != null) {
13460
+ if (await isConversationPurged(args.db, args.conversationId)) {
13461
+ await bestEffortDelete(args.storage, storageKey);
13462
+ throw purgedConversationError(args.conversationId);
13463
+ }
13464
+ const oldStorageKey = existing.storageKey;
13465
+ if ((await args.db.db().update(juniorAttachments).set({
13466
+ deleteRequestedAt: null,
13467
+ storageKey
13468
+ }).where(and(eq(juniorAttachments.id, id), isNotNull(juniorAttachments.deleteRequestedAt))).returning({ id: juniorAttachments.id })).length > 0) {
13469
+ if (await isConversationPurged(args.db, args.conversationId)) await rejectPurgedAttachmentWrite({
13470
+ conversationId: args.conversationId,
13471
+ db: args.db,
13472
+ id,
13473
+ now,
13474
+ storage: args.storage,
13475
+ storageKey
13476
+ });
13477
+ if (oldStorageKey !== storageKey) await bestEffortDelete(args.storage, oldStorageKey);
13478
+ return { id };
13479
+ }
13480
+ const [afterRace] = await args.db.db().select({
13481
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
13482
+ id: juniorAttachments.id,
13483
+ storageKey: juniorAttachments.storageKey
13484
+ }).from(juniorAttachments).where(eq(juniorAttachments.id, id));
13485
+ if (afterRace?.deleteRequestedAt === null) return reuseLiveAttachment({
13486
+ conversationId: args.conversationId,
13487
+ db: args.db,
13488
+ id: afterRace.id,
13489
+ storage: args.storage,
13490
+ storageKey
13491
+ });
13492
+ }
13493
+ let inserted = [];
13494
+ try {
13495
+ inserted = await args.db.db().insert(juniorAttachments).values({
13496
+ id,
13497
+ conversationId: args.conversationId,
13498
+ storageProvider: args.storage.provider,
13499
+ storageKey,
13500
+ provider: args.source?.provider,
13501
+ providerId: args.source?.id,
13502
+ filename: args.file.filename,
13503
+ contentType: args.file.mimeType,
13504
+ bytes: args.file.bytes,
13505
+ sha256,
13506
+ createdAt: now
13507
+ }).onConflictDoNothing({ target: juniorAttachments.id }).returning({ id: juniorAttachments.id });
13508
+ } catch (error) {
13509
+ await bestEffortDelete(args.storage, storageKey);
13510
+ throw error;
13511
+ }
13512
+ if (inserted.length > 0) {
13513
+ if (await isConversationPurged(args.db, args.conversationId)) await rejectPurgedAttachmentWrite({
13514
+ conversationId: args.conversationId,
13515
+ db: args.db,
13516
+ id,
13517
+ now,
13518
+ storage: args.storage,
13519
+ storageKey
13520
+ });
13521
+ return { id };
13522
+ }
13523
+ const [row] = await args.db.db().select({
13524
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
13525
+ id: juniorAttachments.id,
13526
+ storageKey: juniorAttachments.storageKey
13527
+ }).from(juniorAttachments).where(eq(juniorAttachments.id, id));
13528
+ if (!row) {
13529
+ await bestEffortDelete(args.storage, storageKey);
13530
+ throw new Error(`Attachment row missing after write for ${id}`);
13531
+ }
13532
+ if (row.deleteRequestedAt === null) return reuseLiveAttachment({
13533
+ conversationId: args.conversationId,
13534
+ db: args.db,
13535
+ id: row.id,
13536
+ storage: args.storage,
13537
+ storageKey
13538
+ });
13539
+ if (await isConversationPurged(args.db, args.conversationId)) {
13540
+ await bestEffortDelete(args.storage, storageKey);
13541
+ throw purgedConversationError(args.conversationId);
13542
+ }
13543
+ if ((await args.db.db().update(juniorAttachments).set({
13544
+ deleteRequestedAt: null,
13545
+ storageKey
13546
+ }).where(and(eq(juniorAttachments.id, id), isNotNull(juniorAttachments.deleteRequestedAt))).returning({ id: juniorAttachments.id })).length > 0) {
13547
+ if (await isConversationPurged(args.db, args.conversationId)) await rejectPurgedAttachmentWrite({
13548
+ conversationId: args.conversationId,
13549
+ db: args.db,
13550
+ id,
13551
+ now,
13552
+ storage: args.storage,
13553
+ storageKey
13554
+ });
13555
+ if (row.storageKey !== storageKey) await bestEffortDelete(args.storage, row.storageKey);
13556
+ return { id };
13557
+ }
13558
+ const [afterRace] = await args.db.db().select({
13559
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
13560
+ id: juniorAttachments.id
13561
+ }).from(juniorAttachments).where(eq(juniorAttachments.id, id));
13562
+ if (afterRace?.deleteRequestedAt === null) return reuseLiveAttachment({
13563
+ conversationId: args.conversationId,
13564
+ db: args.db,
13565
+ id: afterRace.id,
13566
+ storage: args.storage,
13567
+ storageKey
13568
+ });
13569
+ await bestEffortDelete(args.storage, storageKey);
13570
+ throw new Error(`Attachment ${id} disappeared while reviving`);
13571
+ }
13572
+ /** Load one live attachment owned by a conversation. */
13573
+ async function readLiveAttachment(args) {
13574
+ const [row] = await args.db.db().select({
13575
+ bytes: juniorAttachments.bytes,
13576
+ contentType: juniorAttachments.contentType,
13577
+ conversationId: juniorAttachments.conversationId,
13578
+ deleteRequestedAt: juniorAttachments.deleteRequestedAt,
13579
+ filename: juniorAttachments.filename,
13580
+ id: juniorAttachments.id,
13581
+ storageProvider: juniorAttachments.storageProvider,
13582
+ storageKey: juniorAttachments.storageKey
13583
+ }).from(juniorAttachments).where(and(eq(juniorAttachments.id, args.attachmentId), eq(juniorAttachments.conversationId, args.conversationId), isNull(juniorAttachments.deleteRequestedAt)));
13584
+ if (!row) return null;
13585
+ if (await isConversationPurged(args.db, args.conversationId)) return null;
13586
+ return {
13587
+ bytes: row.bytes,
13588
+ contentType: row.contentType,
13589
+ conversationId: row.conversationId,
13590
+ filename: row.filename,
13591
+ id: row.id,
13592
+ storageKey: row.storageKey,
13593
+ storageProvider: row.storageProvider
13594
+ };
13595
+ }
13596
+ /** Persist many conversation-owned files. */
13597
+ async function storeAttachments(args) {
13598
+ const stored = [];
13599
+ for (const file of args.files) stored.push(await storeAttachment({
13600
+ conversationId: args.conversationId,
13601
+ db: args.db,
13602
+ file,
13603
+ ...args.nowMs !== void 0 ? { nowMs: args.nowMs } : void 0,
13604
+ storage: args.storage
13605
+ }));
13606
+ return stored;
13607
+ }
13608
+ /** Mark attachments unavailable when their conversation content is purged. */
13609
+ async function requestAttachmentDeletion(db, conversationIds, nowMs) {
13610
+ if (conversationIds.length === 0) return;
13611
+ await db.db().update(juniorAttachments).set({ deleteRequestedAt: new Date(nowMs) }).where(and(inArray(juniorAttachments.conversationId, conversationIds), isNull(juniorAttachments.deleteRequestedAt)));
13612
+ }
13613
+ /**
13614
+ * Delete purged attachments in one bounded batch.
13615
+ *
13616
+ * Eligible rows are 30 days old, explicitly purge-marked, or owned by a
13617
+ * conversation whose transcript is already purged. Delete object keys first,
13618
+ * then remove rows that are still eligible so a failed blob delete remains
13619
+ * retryable.
13620
+ */
13621
+ async function collectAttachmentGarbage(args) {
13622
+ const expiresBefore = /* @__PURE__ */ new Date(args.nowMs - ATTACHMENT_RETENTION_MS);
13623
+ const rows = await args.db.db().select({
13624
+ id: juniorAttachments.id,
13625
+ storageKey: juniorAttachments.storageKey
13626
+ }).from(juniorAttachments).innerJoin(juniorConversations, eq(juniorAttachments.conversationId, juniorConversations.conversationId)).where(and(eq(juniorAttachments.storageProvider, args.storage.provider), or(lte(juniorAttachments.createdAt, expiresBefore), isNotNull(juniorAttachments.deleteRequestedAt), isNotNull(juniorConversations.transcriptPurgedAt)))).orderBy(asc(juniorAttachments.createdAt), asc(juniorAttachments.id)).limit(args.limit ?? ATTACHMENT_GC_BATCH_LIMIT);
13627
+ if (rows.length === 0) return { deleted: 0 };
13628
+ await args.storage.delete(rows.map((row) => row.storageKey));
13629
+ const stillEligible = await args.db.db().select({ id: juniorAttachments.id }).from(juniorAttachments).innerJoin(juniorConversations, eq(juniorAttachments.conversationId, juniorConversations.conversationId)).where(and(inArray(juniorAttachments.id, rows.map((row) => row.id)), or(lte(juniorAttachments.createdAt, expiresBefore), isNotNull(juniorAttachments.deleteRequestedAt), isNotNull(juniorConversations.transcriptPurgedAt))));
13630
+ if (stillEligible.length === 0) return { deleted: 0 };
13631
+ return { deleted: (await args.db.db().delete(juniorAttachments).where(inArray(juniorAttachments.id, stillEligible.map((row) => row.id))).returning({ id: juniorAttachments.id })).length };
13632
+ }
13633
+ //#endregion
10872
13634
  //#region src/chat/slack/tools/send-files.ts
10873
13635
  const sendFilesResultSchema = juniorToolOutputSchema.extend({
10874
13636
  deduplicated: z.boolean().optional(),
@@ -11505,7 +14267,7 @@ function createSlackCanvasWriteTool(state) {
11505
14267
  //#endregion
11506
14268
  //#region src/chat/slack/tool-support/list/api.ts
11507
14269
  function normalizeKey(value) {
11508
- return value.toLowerCase().replace(/[^a-z0-9]+/g, "_");
14270
+ return (value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "_");
11509
14271
  }
11510
14272
  /** Infer well-known column roles (title, completed, assignee, due date) from a list schema. */
11511
14273
  function inferListColumnMap(schema = []) {
@@ -11590,11 +14352,10 @@ async function createTodoList(name) {
11590
14352
  /** Read a Slack list schema and map its standard columns. */
11591
14353
  async function getListColumnMap(listId) {
11592
14354
  const client = getSlackClient();
11593
- const file = (await withSlackRetries(() => client.files.info({ file: listId }), 3, {
14355
+ return inferListColumnMap((await withSlackRetries(() => client.files.info({ file: listId }), 3, {
11594
14356
  action: "files.info",
11595
14357
  idempotent: true
11596
- })).file;
11597
- return inferListColumnMap(file?.list_metadata?.schema ?? []);
14358
+ })).file?.list_metadata?.schema);
11598
14359
  }
11599
14360
  /** Add one or more items to an existing Slack list. */
11600
14361
  async function addListItems(input) {
@@ -12097,25 +14858,6 @@ function renderAttachmentText(input) {
12097
14858
  return getAttachmentPayload(input).slice(0, MAX_ATTACHMENTS).map(renderAttachment).filter((line) => line.length > 0).map((line) => `[attachment] ${line}`).join("\n").slice(0, MAX_ATTACHMENT_TEXT_CHARS);
12098
14859
  }
12099
14860
  //#endregion
12100
- //#region src/chat/slack/attachments.ts
12101
- /** Save the vision summary for a stored Slack attachment. */
12102
- async function recordSlackAttachment(args) {
12103
- await args.db.db().update(juniorAttachments).set({ visionSummary: args.visionSummary }).where(and(eq(juniorAttachments.id, args.attachmentId), eq(juniorAttachments.conversationId, args.conversationId), isNull(juniorAttachments.deleteRequestedAt)));
12104
- }
12105
- /** Read a saved vision summary for one live attachment. */
12106
- async function readAttachmentVisionSummary(args) {
12107
- const [row] = await args.db.db().select({ visionSummary: juniorAttachments.visionSummary }).from(juniorAttachments).where(and(eq(juniorAttachments.id, args.attachmentId), eq(juniorAttachments.conversationId, args.conversationId), isNull(juniorAttachments.deleteRequestedAt)));
12108
- return row?.visionSummary;
12109
- }
12110
- /** Match live conversation attachments to Slack file ids. */
12111
- async function matchSlackAttachments(args) {
12112
- if (args.providerIds.length === 0) return [];
12113
- return await args.db.db().select({
12114
- id: juniorAttachments.id,
12115
- providerId: juniorAttachments.providerId
12116
- }).from(juniorAttachments).where(and(eq(juniorAttachments.conversationId, args.conversationId), eq(juniorAttachments.provider, "slack"), inArray(juniorAttachments.providerId, args.providerIds), isNull(juniorAttachments.deleteRequestedAt)));
12117
- }
12118
- //#endregion
12119
14861
  //#region src/chat/slack/tools/thread-read.ts
12120
14862
  const MAX_THREAD_READ_CHARS = 4e4;
12121
14863
  /**
@@ -12289,10 +15031,15 @@ function createSlackThreadReadTool(context, attachmentDeps) {
12289
15031
  }
12290
15032
  //#endregion
12291
15033
  //#region src/chat/slack/users.ts
15034
+ const profileFieldsSchema = z.object({ fields: z.record(z.string(), z.object({
15035
+ value: z.string().optional(),
15036
+ alt: z.string().optional(),
15037
+ label: z.string().optional()
15038
+ }).nullable()).nullish() });
12292
15039
  function normalizeUser(raw) {
12293
- const rawFields = raw.profile?.fields;
15040
+ const rawFields = profileFieldsSchema.parse(raw.profile ?? {}).fields;
12294
15041
  const profileFields = [];
12295
- if (rawFields && typeof rawFields === "object") for (const [id, field] of Object.entries(rawFields)) {
15042
+ if (rawFields) for (const [id, field] of Object.entries(rawFields)) {
12296
15043
  if (!field) continue;
12297
15044
  profileFields.push({
12298
15045
  id,
@@ -12341,7 +15088,7 @@ async function lookupSlackUserByEmail(email) {
12341
15088
  try {
12342
15089
  result = await withSlackRetries(() => client.users.lookupByEmail({ email }), 3, { action: "users.lookupByEmail" });
12343
15090
  } catch (error) {
12344
- if (error.apiError === "users_not_found") return null;
15091
+ if (error instanceof SlackActionError && error.apiError === "users_not_found") return null;
12345
15092
  throw error;
12346
15093
  }
12347
15094
  const user = result.user;
@@ -14002,6 +16749,32 @@ function createWriteFileTool() {
14002
16749
  });
14003
16750
  }
14004
16751
  //#endregion
16752
+ //#region src/stats.ts
16753
+ function utcDate(nowMs) {
16754
+ return new Date(nowMs).toISOString().slice(0, 10);
16755
+ }
16756
+ /** Increment one daily named counter. */
16757
+ async function incrementStat(key, options = {}) {
16758
+ const date = utcDate(options.nowMs ?? Date.now());
16759
+ await getDb().insert(juniorStats).values({
16760
+ ...key,
16761
+ date,
16762
+ count: 1
16763
+ }).onConflictDoUpdate({
16764
+ target: [
16765
+ juniorStats.date,
16766
+ juniorStats.namespace,
16767
+ juniorStats.metric,
16768
+ juniorStats.name
16769
+ ],
16770
+ set: { count: sql`${juniorStats.count} + 1` }
16771
+ });
16772
+ }
16773
+ /** Read daily counters inside an inclusive UTC date range. */
16774
+ async function readStats(start, end) {
16775
+ return await getDb().select().from(juniorStats).where(and(gte(juniorStats.date, start), lte(juniorStats.date, end))).orderBy(asc(juniorStats.date), asc(juniorStats.namespace), asc(juniorStats.metric), asc(juniorStats.name));
16776
+ }
16777
+ //#endregion
14005
16778
  //#region src/chat/workspaces/tools.ts
14006
16779
  /** Record optional reporting without changing the switchWorkspace outcome. */
14007
16780
  async function tryRecordWorkspaceSwitchStat(workspace) {
@@ -14520,53 +17293,6 @@ function buildSandboxInput(toolName, params) {
14520
17293
  return params;
14521
17294
  }
14522
17295
  //#endregion
14523
- //#region src/chat/services/pending-auth.ts
14524
- const AUTH_LINK_REUSE_WINDOW_MS = 6e5;
14525
- const NON_REQUEST_SKIPPED_REASON_PREFIXES = ["directed_to_other_party:", "side_conversation:"];
14526
- function isSkippedNonRequest(reason) {
14527
- return NON_REQUEST_SKIPPED_REASON_PREFIXES.some((prefix) => reason?.startsWith(prefix));
14528
- }
14529
- /** Decide whether the same agent-run session can reuse its fresh auth link. */
14530
- function canReusePendingAuthLink(args) {
14531
- const { pendingAuth } = args;
14532
- if (!pendingAuth) return false;
14533
- return pendingAuth.kind === args.kind && pendingAuth.provider === args.provider && pendingAuth.actorId === args.actorId && pendingAuth.scope === args.scope && pendingAuth.sessionId === args.sessionId && pendingAuth.linkSentAtMs + AUTH_LINK_REUSE_WINDOW_MS > (args.nowMs ?? Date.now());
14534
- }
14535
- function getConversationPendingAuth(args) {
14536
- const pendingAuth = args.conversation.processing.pendingAuth;
14537
- if (!pendingAuth) return;
14538
- if (pendingAuth.kind !== args.kind || pendingAuth.provider !== args.provider || pendingAuth.actorId !== args.actorId || pendingAuth.scope !== args.scope) return;
14539
- return pendingAuth;
14540
- }
14541
- function clearPendingAuth(conversation, sessionId) {
14542
- if (!conversation.processing.pendingAuth) return;
14543
- if (sessionId && conversation.processing.pendingAuth.sessionId !== sessionId) return;
14544
- conversation.processing.pendingAuth = void 0;
14545
- }
14546
- /** Mark the prior blocked turn abandoned after a new auth attempt replaces it. */
14547
- async function abandonReplacedPendingAuth(args) {
14548
- if (args.previousPendingAuth && args.previousPendingAuth.sessionId !== args.nextPendingAuth.sessionId && args.conversationId) await abandonTurnRecord({
14549
- conversationId: args.conversationId,
14550
- turnId: args.previousPendingAuth.sessionId,
14551
- errorMessage: "Abandoned by a newer auth-blocked request in the same conversation."
14552
- });
14553
- }
14554
- /**
14555
- * Decide whether an auth callback still belongs to the latest real human
14556
- * request, ignoring bot-authored and passive bystander rows while treating
14557
- * failed human turns and opt-outs as newer freshness blockers.
14558
- */
14559
- function isPendingAuthLatestRequest(conversation, pendingAuth) {
14560
- for (let index = conversation.messages.length - 1; index >= 0; index -= 1) {
14561
- const message = conversation.messages[index];
14562
- if (message?.role !== "user") continue;
14563
- if (message.author?.isBot) continue;
14564
- if (isSkippedNonRequest(message.meta?.skippedReason)) continue;
14565
- return buildDeterministicTurnId(message.id) === pendingAuth.sessionId;
14566
- }
14567
- return false;
14568
- }
14569
- //#endregion
14570
17296
  //#region src/chat/services/plugin-auth-orchestration.ts
14571
17297
  var PluginAuthorizationPauseError = class extends AuthorizationPauseError {
14572
17298
  constructor(provider, providerDisplayName, disposition, requestText) {
@@ -15600,7 +18326,7 @@ function parseMcpAuthSession(value) {
15600
18326
  if (typeof value !== "string") return;
15601
18327
  try {
15602
18328
  const parsed = JSON.parse(value);
15603
- if (!isRecord$2(parsed)) return;
18329
+ if (!isRecord$3(parsed)) return;
15604
18330
  if (parsed.schemaVersion !== 2 || typeof parsed.authSessionId !== "string" || typeof parsed.provider !== "string" || typeof parsed.userId !== "string" || typeof parsed.conversationId !== "string" || typeof parsed.sessionId !== "string" || typeof parsed.userMessage !== "string" || typeof parsed.createdAtMs !== "number" || typeof parsed.updatedAtMs !== "number") return;
15605
18331
  const destination = parsed.destination === void 0 ? void 0 : parseDestination(parsed.destination);
15606
18332
  if (parsed.destination !== void 0 && !destination) return;
@@ -15621,7 +18347,7 @@ function parseMcpAuthSession(value) {
15621
18347
  ...typeof parsed.channelId === "string" ? { channelId: parsed.channelId } : void 0,
15622
18348
  ...typeof parsed.threadTs === "string" ? { threadTs: parsed.threadTs } : void 0,
15623
18349
  ...typeof parsed.toolChannelId === "string" ? { toolChannelId: parsed.toolChannelId } : void 0,
15624
- ...isRecord$2(parsed.configuration) ? { configuration: parsed.configuration } : void 0,
18350
+ ...isRecord$3(parsed.configuration) ? { configuration: parsed.configuration } : void 0,
15625
18351
  ...typeof parsed.authorizationUrl === "string" ? { authorizationUrl: parsed.authorizationUrl } : void 0,
15626
18352
  ...typeof parsed.codeVerifier === "string" ? { codeVerifier: parsed.codeVerifier } : void 0
15627
18353
  };
@@ -15633,7 +18359,7 @@ function parseServerSession(value) {
15633
18359
  if (typeof value !== "string") return;
15634
18360
  try {
15635
18361
  const parsed = JSON.parse(value);
15636
- if (!isRecord$2(parsed) || typeof parsed.sessionId !== "string" || typeof parsed.updatedAtMs !== "number") return;
18362
+ if (!isRecord$3(parsed) || typeof parsed.sessionId !== "string" || typeof parsed.updatedAtMs !== "number") return;
15637
18363
  return {
15638
18364
  sessionId: parsed.sessionId,
15639
18365
  updatedAtMs: parsed.updatedAtMs
@@ -15646,11 +18372,11 @@ function parseStoredCredentials(value) {
15646
18372
  if (typeof value !== "string") return;
15647
18373
  try {
15648
18374
  const parsed = JSON.parse(value);
15649
- if (!isRecord$2(parsed)) return;
18375
+ if (!isRecord$3(parsed)) return;
15650
18376
  return {
15651
- ...isRecord$2(parsed.clientInformation) ? { clientInformation: parsed.clientInformation } : void 0,
15652
- ...isRecord$2(parsed.discoveryState) ? { discoveryState: parsed.discoveryState } : void 0,
15653
- ...isRecord$2(parsed.tokens) ? { tokens: parsed.tokens } : void 0
18377
+ ...isRecord$3(parsed.clientInformation) ? { clientInformation: parsed.clientInformation } : void 0,
18378
+ ...isRecord$3(parsed.discoveryState) ? { discoveryState: parsed.discoveryState } : void 0,
18379
+ ...isRecord$3(parsed.tokens) ? { tokens: parsed.tokens } : void 0
15654
18380
  };
15655
18381
  } catch {
15656
18382
  return;
@@ -19004,7 +21730,7 @@ async function executeAgentRunInPrivacyContext(run, conversationPrivacy, runLogC
19004
21730
  ...getProviderErrorAttributes(providerRetry.providerError),
19005
21731
  "app.ai.provider_error.retry_attempt": providerRetryAttempt
19006
21732
  });
19007
- await sleep$1(providerRetry.delayMs, signal);
21733
+ await sleep$2(providerRetry.delayMs, signal);
19008
21734
  run = agent.continue();
19009
21735
  }
19010
21736
  } catch (error) {
@@ -19170,7 +21896,7 @@ function createAgentRunner(execute, options) {
19170
21896
  const canBindSpawn = Boolean(bindSpawnAgent) && isExperimentalFeatureEnabled("subagents");
19171
21897
  return { run: async (run) => {
19172
21898
  const spawnAgent = bindSpawnAgent && canBindSpawn && !isAgentRunFeatureDisabled(run.disabledFeatures, "subagents") ? bindSpawnAgent(run) : void 0;
19173
- return await execute({
21899
+ const nextRun = {
19174
21900
  ...run,
19175
21901
  environment: {
19176
21902
  ...run.environment,
@@ -19181,7 +21907,21 @@ function createAgentRunner(execute, options) {
19181
21907
  ...run.durability,
19182
21908
  spawnAgent
19183
21909
  } } : void 0
19184
- }, streamFn);
21910
+ };
21911
+ if (run.instruction.storedAttachments?.length) {
21912
+ const { loadInputImages } = await import("./images-8siTAUNW.js").then((n) => n.n);
21913
+ const storage = nextRun.environment?.attachmentStorage;
21914
+ if (!storage) throw new Error("Attachment storage is unavailable.");
21915
+ nextRun.instruction = {
21916
+ ...run.instruction,
21917
+ attachments: await loadInputImages({
21918
+ attachments: run.instruction.storedAttachments,
21919
+ conversationId: run.conversationId,
21920
+ storage
21921
+ })
21922
+ };
21923
+ }
21924
+ return await execute(nextRun, streamFn);
19185
21925
  } };
19186
21926
  }
19187
21927
  async function waitForAgentSettlement(runPromise) {
@@ -19220,4 +21960,4 @@ async function runAgentWithTimeout(agentRunner, run, timeoutMs) {
19220
21960
  }
19221
21961
  }
19222
21962
  //#endregion
19223
- export { clearSandboxEgressSignals as $, renderAttachmentText as A, AgentInvocationLimitError as B, resolvePluginOAuthAccount as C, toolInvocationDestination as Ct, lookupSlackUserProfile as D, setConfigDefaults as Dt, unpublishedArtifactHeaders as E, getConfigDefaults as Et, getNextRunAtMs as F, isEventMailboxMetadata as G, processWorkspaceSnapshotJob as H, agentBindingSchema as I, recordFinishedTurnForAutomatedLimit as J, renderTaskInput as K, agentInvocationSchema as L, listThreadReplies as M, logScheduledAutomationRunSkipped as N, readAttachmentVisionSummary as O, scheduledAutomationRunAttributes as P, SANDBOX_EGRESS_PROXY_PATH as Q, createAgentInvocationSchema as R, latestProgressStatus as S, actorFromRun as St, readPublicArtifact as T, shouldEmitDevAgentTrace as Tt, ingestEvent as U, getMcpProviderErrorAttributes as V, EVENT_WAIT_MS as W, normalizeSandboxEgressTracePropagationDomains as X, resolveSandboxEgressProviderForHost as Y, shouldPropagateSandboxEgressTrace as Z, putMcpStoredOAuthCredentials as _, slackExternalRefIdSchema as _t, isExperimentalFeatureEnabled as a, parseOAuthStatePayload as at, getConversationPendingAuth as b, isVisionImageMediaType as bt, createContextCompactor as c, escapeSlackMrkdwnText as ct, deleteMcpAuthSession as d, normalizeSlackStatusText as dt, consumeSandboxEgressAuthRequiredSignal as et, deleteMcpAuthSessionsForUserProvider as f, truncateStatusText as ft, getMcpStoredOAuthCredentials as g, slackEntitySchema as gt, getMcpAuthSession as h, removeReactionFromMessage as ht, getExperimentalFeatures as i, formatProviderLabel as it, renderBlockText as j, recordSlackAttachment as k, executeCredentialedEgressRequest as l, formatSlackLink as lt, deleteMcpStoredOAuthCredentials as m, postSlackMessage as mt, runAgentWithTimeout as n, parseSandboxEgressCredentialToken as nt, setExperimentalFeatures as o, resolveBaseUrl as ot, deleteMcpServerSessionId as p, addReactionToMessage as pt, maybePostAutomatedTurnLimitNotice as q, experimental_exports as r, sandboxEgressSignalsResponseSchema as rt, executeAgentRun as s, startOAuthFlow as st, createAgentRunner as t, consumeSandboxEgressPermissionDeniedSignal as tt, finalizeMcpAuthorization as u, normalizeSlackReplyMarkdown as ut, PluginCredentialFailureError as v, createUserTokenStore as vt, publicArtifactHeaders as w, AuthorizationFlowDisabledError as wt, isPendingAuthLatestRequest as x, RetryableDeliveryError as xt, clearPendingAuth as y, buildSteeringPiMessage as yt, AgentInvocationBusyError as z };
21963
+ export { collectEventAutomationMatchKeys as $, recordFinishedTurnForAutomatedLimit as $t, requestAttachmentDeletion as A, getConfigDefaults as An, deadLetterAttempt as At, isListedScheduledAutomation as B, removeActiveConversation as Bt, readPublicArtifact as C, createUserTokenStore as Cn, cancelHumanFacingPendingMessages as Ct, renderBlockText as D, toolInvocationDestination as Dn, completeConversationStop as Dt, renderAttachmentText as E, actorFromRun as En, clearExpiredConversationLease as Et, resolveSlackChannelIdFromThreadId as F, hasRunnableConversationWork as Ft, readScheduledAutomation as G, CONVERSATION_WORK_CHECK_IN_INTERVAL_MS as Gt, listScheduledAutomations as H, requestConversationWork as Ht, listThreadReplies as I, listActiveConversationIds as It, scheduledRunId as J, hasRunnableConversationWork$1 as Jt, saveScheduledAutomation as K, CONVERSATION_WORK_STALE_ENQUEUE_MS as Kt, logScheduledAutomationRunSkipped as L, recordAttemptFailure as Lt, parseSlackThreadId as M, ensureConversationWake as Mt, readSlackRawMessageContext as N, getConversation as Nt, collectAttachmentGarbage as O, AuthorizationFlowDisabledError as On, completeConversationWork as Ot, resolveSlackChannelIdFromMessage as P, getConversationWorkState as Pt, effectiveTaskOutcomes as Q, maybePostAutomatedTurnLimitNotice as Qt, scheduledAutomationRunAttributes as R, recordConversationRetry as Rt, publicArtifactHeaders as S, slackExternalRefIdSchema as Sn, beginConversationResume as St, lookupSlackUserProfile as T, RetryableDeliveryError as Tn, clearConsumedConversationWake as Tt, listScheduledAutomationsForTeam as U, startConversationWork as Ut, listPublicScheduledAutomationsForTeams as V, requestAnotherSlice as Vt, parseScheduledAutomationRow as W, stopConversationWork as Wt, scheduledRunSchema as X, isInvalidConversationRecordError as Xt, withScheduledAutomationLock as Y, isFinalAttempt as Yt, eventAutomationTriggerAvailable as Z, renderTaskInput as Zt, putMcpStoredOAuthCredentials as _, truncateStatusText as _n, getEventCatalog as _t, isExperimentalFeatureEnabled as a, consumeSandboxEgressAuthRequiredSignal as an, listEventAutomationsCreatedBy as at, readStats as b, removeReactionFromMessage as bn, appendAndEnqueueExclusiveInboundMessage as bt, createContextCompactor as c, sandboxEgressSignalsResponseSchema as cn, agentInvocationSchema as ct, deleteMcpAuthSession as d, resolveBaseUrl as dn, AgentInvocationLimitError as dt, resolveSandboxEgressProviderForHost as en, deleteEventAutomation as et, deleteMcpAuthSessionsForUserProvider as f, startOAuthFlow as fn, getMcpProviderErrorAttributes as ft, getMcpStoredOAuthCredentials as g, normalizeSlackStatusText as gn, isEventMailboxMetadata as gt, getMcpAuthSession as h, normalizeSlackReplyMarkdown as hn, EVENT_WAIT_MS as ht, getExperimentalFeatures as i, clearSandboxEgressSignals as in, listDeletedEventAutomationsCreatedBy as it, storeAttachment as j, setConfigDefaults as jn, drainConversationMailbox as jt, readLiveAttachment as k, shouldEmitDevAgentTrace as kn, countPendingConversationMessages as kt, executeCredentialedEgressRequest as l, formatProviderLabel as ln, createAgentInvocationSchema as lt, deleteMcpStoredOAuthCredentials as m, formatSlackLink as mn, ingestEvent as mt, runAgentWithTimeout as n, shouldPropagateSandboxEgressTrace as nn, findMatchingEventAutomations as nt, setExperimentalFeatures as o, consumeSandboxEgressPermissionDeniedSignal as on, listPublicEventAutomationsForTeams as ot, deleteMcpServerSessionId as p, escapeSlackMrkdwnText as pn, processWorkspaceSnapshotJob as pt, saveScheduledAutomationInLock as q, hasConversationStop as qt, experimental_exports as r, SANDBOX_EGRESS_PROXY_PATH as rn, getEventAutomation as rt, executeAgentRun as s, parseSandboxEgressCredentialToken as sn, agentBindingSchema as st, createAgentRunner as t, normalizeSandboxEgressTracePropagationDomains as tn, eventAutomationBelongsToUser as tt, finalizeMcpAuthorization as u, parseOAuthStatePayload as un, AgentInvocationBusyError as ut, PluginCredentialFailureError as v, addReactionToMessage as vn, eventGuidance as vt, unpublishedArtifactHeaders as w, buildSteeringPiMessage as wn, checkInConversationWork as wt, resolvePluginOAuthAccount as x, slackEntitySchema as xn, appendAndEnqueueInboundMessage as xt, latestProgressStatus as y, postSlackMessage as yn, ackMessages as yt, getNextRunAtMs as z, releaseConversationWork as zt };