@open-everywhere/provider-slack 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/access/slack-attachment-fetch.ts +2 -1
- package/agent/app-manifest.ts +15 -1
- package/agent/channels/slack-agent-session.ts +249 -0
- package/agent/channels/slack-attachments.ts +20 -11
- package/agent/channels/slack-bot-ask.ts +101 -0
- package/agent/channels/slack-commands.ts +178 -17
- package/agent/channels/slack-context.ts +0 -5
- package/agent/channels/slack-events.ts +110 -33
- package/agent/channels/slack-follow-ups.ts +148 -0
- package/agent/channels/slack-friendly-names.ts +55 -0
- package/agent/channels/slack-front.ts +262 -27
- package/agent/channels/slack-home.ts +9 -3
- package/agent/channels/slack-identity.ts +9 -1
- package/agent/channels/slack-memory-canvas.ts +197 -54
- package/agent/channels/slack-origin-codec.ts +9 -0
- package/agent/channels/slack-post-arm.ts +68 -11
- package/agent/channels/slack-read.ts +27 -2
- package/agent/channels/slack-reply-blocks.ts +180 -0
- package/agent/channels/slack-reply-feedback.ts +75 -0
- package/agent/channels/slack-shortcut.ts +1 -1
- package/agent/channels/slack-voice-clips.ts +82 -0
- package/agent/channels/slack-wake-backscroll.ts +0 -5
- package/agent/channels/slack-web-api.ts +4 -2
- package/agent/channels/slack.ts +81 -19
- package/agent/docs-generated.ts +1282 -2
- package/agent/ops.ts +14 -21
- package/agent/register.ts +25 -0
- package/agent/setup/credentials.ts +17 -0
- package/agent/setup/page.ts +6 -3
- package/agent/surface-parity.ts +2 -0
- package/dist/agent/access/slack-attachment-fetch.d.ts.map +1 -1
- package/dist/agent/access/slack-attachment-fetch.js.map +1 -1
- package/dist/agent/app-manifest.d.ts.map +1 -1
- package/dist/agent/app-manifest.js +15 -1
- package/dist/agent/app-manifest.js.map +1 -1
- package/dist/agent/channels/slack-agent-session.d.ts +81 -0
- package/dist/agent/channels/slack-agent-session.d.ts.map +1 -0
- package/dist/agent/channels/slack-agent-session.js +198 -0
- package/dist/agent/channels/slack-agent-session.js.map +1 -0
- package/dist/agent/channels/slack-attachments.d.ts +11 -0
- package/dist/agent/channels/slack-attachments.d.ts.map +1 -1
- package/dist/agent/channels/slack-attachments.js +21 -14
- package/dist/agent/channels/slack-attachments.js.map +1 -1
- package/dist/agent/channels/slack-bot-ask.d.ts +30 -0
- package/dist/agent/channels/slack-bot-ask.d.ts.map +1 -0
- package/dist/agent/channels/slack-bot-ask.js +91 -0
- package/dist/agent/channels/slack-bot-ask.js.map +1 -0
- package/dist/agent/channels/slack-commands.d.ts +113 -10
- package/dist/agent/channels/slack-commands.d.ts.map +1 -1
- package/dist/agent/channels/slack-commands.js +151 -18
- package/dist/agent/channels/slack-commands.js.map +1 -1
- package/dist/agent/channels/slack-context.d.ts +1 -2
- package/dist/agent/channels/slack-context.d.ts.map +1 -1
- package/dist/agent/channels/slack-context.js +0 -4
- package/dist/agent/channels/slack-context.js.map +1 -1
- package/dist/agent/channels/slack-events.d.ts +47 -97
- package/dist/agent/channels/slack-events.d.ts.map +1 -1
- package/dist/agent/channels/slack-events.js +90 -22
- package/dist/agent/channels/slack-events.js.map +1 -1
- package/dist/agent/channels/slack-follow-ups.d.ts +44 -0
- package/dist/agent/channels/slack-follow-ups.d.ts.map +1 -0
- package/dist/agent/channels/slack-follow-ups.js +129 -0
- package/dist/agent/channels/slack-follow-ups.js.map +1 -0
- package/dist/agent/channels/slack-friendly-names.d.ts +10 -0
- package/dist/agent/channels/slack-friendly-names.d.ts.map +1 -0
- package/dist/agent/channels/slack-friendly-names.js +45 -0
- package/dist/agent/channels/slack-friendly-names.js.map +1 -0
- package/dist/agent/channels/slack-front.d.ts +38 -46
- package/dist/agent/channels/slack-front.d.ts.map +1 -1
- package/dist/agent/channels/slack-front.js +230 -23
- package/dist/agent/channels/slack-front.js.map +1 -1
- package/dist/agent/channels/slack-home.d.ts.map +1 -1
- package/dist/agent/channels/slack-home.js +10 -3
- package/dist/agent/channels/slack-home.js.map +1 -1
- package/dist/agent/channels/slack-identity.d.ts +7 -21
- package/dist/agent/channels/slack-identity.d.ts.map +1 -1
- package/dist/agent/channels/slack-identity.js +3 -1
- package/dist/agent/channels/slack-identity.js.map +1 -1
- package/dist/agent/channels/slack-memory-canvas.d.ts +21 -5
- package/dist/agent/channels/slack-memory-canvas.d.ts.map +1 -1
- package/dist/agent/channels/slack-memory-canvas.js +166 -55
- package/dist/agent/channels/slack-memory-canvas.js.map +1 -1
- package/dist/agent/channels/slack-origin-codec.d.ts.map +1 -1
- package/dist/agent/channels/slack-origin-codec.js +10 -0
- package/dist/agent/channels/slack-origin-codec.js.map +1 -1
- package/dist/agent/channels/slack-post-arm.d.ts +21 -1
- package/dist/agent/channels/slack-post-arm.d.ts.map +1 -1
- package/dist/agent/channels/slack-post-arm.js +57 -10
- package/dist/agent/channels/slack-post-arm.js.map +1 -1
- package/dist/agent/channels/slack-read.d.ts +11 -1
- package/dist/agent/channels/slack-read.d.ts.map +1 -1
- package/dist/agent/channels/slack-read.js +27 -3
- package/dist/agent/channels/slack-read.js.map +1 -1
- package/dist/agent/channels/slack-reply-blocks.d.ts +29 -0
- package/dist/agent/channels/slack-reply-blocks.d.ts.map +1 -0
- package/dist/agent/channels/slack-reply-blocks.js +172 -0
- package/dist/agent/channels/slack-reply-blocks.js.map +1 -0
- package/dist/agent/channels/slack-reply-feedback.d.ts +23 -0
- package/dist/agent/channels/slack-reply-feedback.d.ts.map +1 -0
- package/dist/agent/channels/slack-reply-feedback.js +63 -0
- package/dist/agent/channels/slack-reply-feedback.js.map +1 -0
- package/dist/agent/channels/slack-shortcut.d.ts +10 -0
- package/dist/agent/channels/slack-shortcut.d.ts.map +1 -1
- package/dist/agent/channels/slack-shortcut.js +1 -1
- package/dist/agent/channels/slack-shortcut.js.map +1 -1
- package/dist/agent/channels/slack-voice-clips.d.ts +16 -0
- package/dist/agent/channels/slack-voice-clips.d.ts.map +1 -0
- package/dist/agent/channels/slack-voice-clips.js +80 -0
- package/dist/agent/channels/slack-voice-clips.js.map +1 -0
- package/dist/agent/channels/slack-wake-backscroll.d.ts +0 -2
- package/dist/agent/channels/slack-wake-backscroll.d.ts.map +1 -1
- package/dist/agent/channels/slack-wake-backscroll.js +0 -4
- package/dist/agent/channels/slack-wake-backscroll.js.map +1 -1
- package/dist/agent/channels/slack-web-api.d.ts.map +1 -1
- package/dist/agent/channels/slack-web-api.js +4 -2
- package/dist/agent/channels/slack-web-api.js.map +1 -1
- package/dist/agent/channels/slack.d.ts +22 -1
- package/dist/agent/channels/slack.d.ts.map +1 -1
- package/dist/agent/channels/slack.js +70 -16
- package/dist/agent/channels/slack.js.map +1 -1
- package/dist/agent/docs-generated.d.ts.map +1 -1
- package/dist/agent/docs-generated.js +1282 -2
- package/dist/agent/docs-generated.js.map +1 -1
- package/dist/agent/ops.d.ts +1 -1
- package/dist/agent/ops.d.ts.map +1 -1
- package/dist/agent/ops.js +15 -21
- package/dist/agent/ops.js.map +1 -1
- package/dist/agent/register.d.ts.map +1 -1
- package/dist/agent/register.js +22 -1
- package/dist/agent/register.js.map +1 -1
- package/dist/agent/setup/credentials.d.ts +12 -0
- package/dist/agent/setup/credentials.d.ts.map +1 -1
- package/dist/agent/setup/credentials.js +17 -0
- package/dist/agent/setup/credentials.js.map +1 -1
- package/dist/agent/setup/page.d.ts.map +1 -1
- package/dist/agent/setup/page.js +6 -3
- package/dist/agent/setup/page.js.map +1 -1
- package/dist/agent/surface-parity.d.ts.map +1 -1
- package/dist/agent/surface-parity.js +2 -0
- package/dist/agent/surface-parity.js.map +1 -1
- package/package.json +40 -3
|
@@ -38,22 +38,28 @@
|
|
|
38
38
|
// urlencoded body (Eve's own `eve_input:` HITL clicks, view_submission, slash
|
|
39
39
|
// commands) still forwards exactly as before.
|
|
40
40
|
// 4. Idempotency + per-thread FIFO guard the wake (see EventDedupe / ThreadFifo below).
|
|
41
|
+
import { auditErrorDetail } from "@open-everywhere/core/agent/adapter-api.js";
|
|
41
42
|
import { parseSlackUserDeactivation, routeSlackUserDeactivation } from "./slack-offboarding.js";
|
|
42
43
|
import { POST } from "eve/channels";
|
|
43
44
|
import { SLACK_SIGNATURE_HEADER, SLACK_TIMESTAMP_HEADER, slackSignature, slackSignatureMatches } from "./slack-signature.js";
|
|
44
45
|
import { slackSigningSecret } from "../setup/credentials.js";
|
|
45
46
|
import { slackContinuationToken } from "eve/channels/slack";
|
|
46
47
|
import { callSlackWebApi } from "./slack-web-api.js";
|
|
47
|
-
import { auditDmBotAuthorDrop, auditDmDriverAdmitted, dmDoorAdmitsDriver, parseMessageEvent, routeMessageEvent, liveDeps, routeNameDropEvent, liveNameDropDeps, parseChannelJoinEvent, routeChannelJoinEvent, liveChannelJoinDeps, parseSlackReactionEvent, routeReactionEvent, liveReactionDeps, routeSupervisorEvent, liveSupervisorDeps, supervisorFifoKey, bindableFor, bindableForJoin, bindableForReaction, slackSingleWindowMentionPolicy, slackDomainUsesUnifiedWindow, slackFrontTenantFenceDeclined, slackInteractivityTenantFenceDeclined, } from "./slack-events.js";
|
|
48
|
+
import { auditDmBotAuthorDrop, auditDmDriverAdmitted, dmDoorAdmitsDriver, UNCHANGED_EDIT_DROP_REASON, parseMessageEvent, routeMessageEvent, liveDeps, routeNameDropEvent, liveNameDropDeps, parseChannelJoinEvent, routeChannelJoinEvent, liveChannelJoinDeps, parseSlackReactionEvent, routeReactionEvent, liveReactionDeps, routeSupervisorEvent, liveSupervisorDeps, supervisorFifoKey, bindableFor, bindableForJoin, bindableForReaction, bindableForSlackIds, slackSingleWindowMentionPolicy, slackDomainUsesUnifiedWindow, slackFrontTenantFenceDeclined, slackInteractivityTenantFenceDeclined, } from "./slack-events.js";
|
|
48
49
|
import { slackInteractivityTenantIds } from "./slack-tenant-fence.js";
|
|
49
|
-
import { EventDedupe, ThreadFifo, getOrCreateDmSeat as liveGetOrCreateDmSeat, touchDmSeat as liveTouchDmSeat, resolvePartition, domainKeyOf, isThreadEngaged, divertInboundToUnifiedWindow, isPrunedDeploymentWakeError, markUndeliveredAfterHeal, isUndeliveredAfterHeal, healPrunedDeploymentStrand, mintWakeId, slackVendorEventTime, spreadVendorTs, resolveWakeTrust, ambientSupervisorEnabled, getConfig, getOperationalStore, OE_APPROVAL_PREFIX, handleApprovalPayload as liveHandleApprovalPayload, OE_SETKEY_ACTION_PREFIX, OE_SETKEY_MODAL_CALLBACK, handleSetKeyClick as liveHandleSetKeyClick, handleSetKeyViewSubmission as liveHandleSetKeyViewSubmission, OE_CONNECT_ACTION_PREFIX, handleConnectClick as liveHandleConnectClick, OE_GRANT_ACTION_PREFIX, handleGrantCardClick as liveHandleGrantCardClick, buildRecord, enqueue as enqueueLogRecord, flushLogShip, stampNonMemberChatAuth, externalPolicyIsInert, recordWakeTail, tailAuthorGrain, dmWindowRegistryKey, stampSurfaceEventVerified, readPersonOptOut, auditPersonOptOutRefusal, PERSON_OPT_OUT_SHORTCUT_NOTICE, PERSON_OPT_OUT_UNREADABLE_NOTICE, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
50
|
+
import { EventDedupe, ThreadFifo, getOrCreateDmSeat as liveGetOrCreateDmSeat, touchDmSeat as liveTouchDmSeat, resolvePartition, auditSurfaceModeFrom, domainKeyOf, isThreadEngaged, divertInboundToUnifiedWindow, isPrunedDeploymentWakeError, markUndeliveredAfterHeal, isUndeliveredAfterHeal, healPrunedDeploymentStrand, mintWakeId, slackVendorEventTime, spreadVendorTs, resolveWakeTrust, ambientSupervisorEnabled, getConfig, getOperationalStore, OE_APPROVAL_PREFIX, handleApprovalPayload as liveHandleApprovalPayload, OE_SETKEY_ACTION_PREFIX, OE_SETKEY_MODAL_CALLBACK, handleSetKeyClick as liveHandleSetKeyClick, handleSetKeyViewSubmission as liveHandleSetKeyViewSubmission, OE_CONNECT_ACTION_PREFIX, handleConnectClick as liveHandleConnectClick, OE_GRANT_ACTION_PREFIX, handleGrantCardClick as liveHandleGrantCardClick, buildRecord, enqueue as enqueueLogRecord, flushLogShip, stampNonMemberChatAuth, externalPolicyIsInert, recordWakeTail, tailAuthorGrain, dmWindowRegistryKey, stampSurfaceEventVerified, readPersonOptOut, auditPersonOptOutRefusal, PERSON_OPT_OUT_SHORTCUT_NOTICE, PERSON_OPT_OUT_UNREADABLE_NOTICE, admitAskedBotReply, deploymentOrg, } from "@open-everywhere/core/agent/adapter-api.js";
|
|
51
|
+
import { botReplyCandidate, slackBotReplyFacts } from "./slack-bot-ask.js";
|
|
50
52
|
import { sidePanelViewedRoom, slackDmSystemNote } from "./slack-side-panel.js";
|
|
51
|
-
import {
|
|
53
|
+
import { parseAgentSessionStopped, routeAgentSessionStop, setSlackSessionStatus, slackSessionTitle } from "./slack-agent-session.js";
|
|
54
|
+
import { parseSlackFeedbackClick, routeSlackFeedbackClick } from "./slack-reply-feedback.js";
|
|
55
|
+
import { followUpMessageEvent, followUpPressKey, markFollowUpPressed, parseSlackFollowUpClick } from "./slack-follow-ups.js";
|
|
56
|
+
import { renderWakeMessage, slackChannelFactsFor, slackAuthorIsMember, slackClickerIsGuest, slackWakeAuthorFacts, slackExternalPolicyForRoom, applySlackExternalPolicy, stripUnfencedSlackAttachments, slackEnterpriseIdFromEnvelope, slackForm, } from "./slack-events.js";
|
|
52
57
|
import { buildSlackAuth, withForwardedSlackEnvelope } from "./slack-identity.js";
|
|
53
58
|
import { holdRouteWork, routeHoldQuiesced, withSlackRouteHold } from "./slack-route-hold.js";
|
|
54
59
|
import { liveMemoryCanvasDeps, recheckChannelCanvas } from "./slack-memory-canvas.js";
|
|
55
60
|
import { handleHomeClick, handleHomeOpened, handleHomeSubmission, liveSlackHomeDeps, parseHomeClick, parseHomeOpened, parseHomeSubmission, } from "./slack-home.js";
|
|
56
61
|
import { persistSlackAttachment, registerSlackAttachment } from "./slack-attachments.js";
|
|
62
|
+
import { hasSlackVoiceClip, withSlackVoiceClips } from "./slack-voice-clips.js";
|
|
57
63
|
import { parseSlackShortcutPayload, shortcutMessageEvent, shortcutWakeKey, } from "./slack-shortcut.js";
|
|
58
64
|
/** The public URL Slack POSTs every subscribed event to (Event Subscriptions Request URL). */
|
|
59
65
|
export const PUBLIC_SLACK_ROUTE = "/eve/v1/slack";
|
|
@@ -217,6 +223,19 @@ export function classifyEnvelope(rawBody, contentType) {
|
|
|
217
223
|
if (grantClick !== undefined) {
|
|
218
224
|
return { kind: "grant-click", reason: "block_actions oe_grant: click — ours, never forwarded", payload: grantClick };
|
|
219
225
|
}
|
|
226
|
+
// OPE-2520 — the native thumbs under an answer. Its action id is exactly `oe_feedback`, which no
|
|
227
|
+
// prefix above matches, so the order among these branches does not matter.
|
|
228
|
+
const feedbackPayload = decodeInteractivityPayload(rawBody);
|
|
229
|
+
const feedbackClick = parseSlackFeedbackClick(feedbackPayload);
|
|
230
|
+
if (feedbackClick !== undefined) {
|
|
231
|
+
return { kind: "feedback-click", reason: `block_actions oe_feedback ${feedbackClick.rating} — ours, never forwarded`, payload: feedbackPayload, click: feedbackClick };
|
|
232
|
+
}
|
|
233
|
+
// OPE-2516 — a suggested follow-up under an answer. Its action ids start `oe_followup:`, which no
|
|
234
|
+
// branch above matches.
|
|
235
|
+
const followUpClick = parseSlackFollowUpClick(feedbackPayload);
|
|
236
|
+
if (followUpClick !== undefined) {
|
|
237
|
+
return { kind: "followup-click", reason: "block_actions oe_followup: press in a DM — ours, never forwarded", payload: feedbackPayload, click: followUpClick };
|
|
238
|
+
}
|
|
220
239
|
// THE HOME TAB (OPE-2451). Its clicks carry an `oe_home:` action id and its one modal a callback
|
|
221
240
|
// id of its own, so neither can be mistaken for another door's payload, nor for eve's.
|
|
222
241
|
const homePayload = decodeInteractivityPayload(rawBody);
|
|
@@ -279,6 +298,10 @@ export function classifyEnvelope(rawBody, contentType) {
|
|
|
279
298
|
// forward costs eve a parse and a re-verify for every click the person makes in Slack.
|
|
280
299
|
return { kind: "context-change", reason: "app_context_changed — the room rides the next side-panel message; nothing to wake" };
|
|
281
300
|
}
|
|
301
|
+
if (eventType === "agent_session_stopped") {
|
|
302
|
+
// SLACK'S NATIVE STOP (OPE-2519). The subscription is what makes Slack show the button at all.
|
|
303
|
+
return { kind: "session-stop", reason: "agent_session_stopped — stop the session's running turn", env };
|
|
304
|
+
}
|
|
282
305
|
if (eventType === "user_change") {
|
|
283
306
|
// OPE-2423: slackChannel drops it, and a DEACTIVATED user is ours — automatic offboarding.
|
|
284
307
|
return { kind: "user-change", reason: "user_change — a deactivation report candidate", env };
|
|
@@ -377,10 +400,6 @@ export function resolveBotIdentity() {
|
|
|
377
400
|
}
|
|
378
401
|
return botIdentityPromise;
|
|
379
402
|
}
|
|
380
|
-
/** Reset the cached identity (tests only). */
|
|
381
|
-
export function _resetBotIdentity() {
|
|
382
|
-
botIdentityPromise = undefined;
|
|
383
|
-
}
|
|
384
403
|
const OK = () => new Response("ok");
|
|
385
404
|
/**
|
|
386
405
|
* The Enterprise Grid rung a forwarded delivery carries, paired with the message it describes
|
|
@@ -517,7 +536,7 @@ const logOutcome = (path, channelId, ts) => (outcome) => {
|
|
|
517
536
|
* ({@link markUndeliveredAfterHeal}) before it rethrows, so {@link recordWakeFailure} can give the
|
|
518
537
|
* row its own reason.
|
|
519
538
|
*/
|
|
520
|
-
|
|
539
|
+
async function deliverWithStrandRetry(deliver) {
|
|
521
540
|
try {
|
|
522
541
|
return await deliver();
|
|
523
542
|
}
|
|
@@ -547,10 +566,11 @@ export function recordWakeFailure(key, err) {
|
|
|
547
566
|
// `void`-ed write sits OUTSIDE that promise and so races the freeze after the 200 all the same.
|
|
548
567
|
void holdRouteWork((async () => {
|
|
549
568
|
try {
|
|
550
|
-
const
|
|
569
|
+
const bindable = {
|
|
551
570
|
channel: { kind: "slack" },
|
|
552
571
|
session: { auth: { current: { attributes: { team_id: "", channel_id: channelId }, principalId: "", principalType: "slack-user" } } },
|
|
553
|
-
}
|
|
572
|
+
};
|
|
573
|
+
const p = resolvePartition(bindable);
|
|
554
574
|
// OPE-970: a retry that ALSO failed after the pruned-deployment heal gets its own reason —
|
|
555
575
|
// otherwise it is indistinguishable from any other send failure, and the only way to tell
|
|
556
576
|
// "the seat is fine now but this message was lost" from "a transient blip" was to read the
|
|
@@ -562,10 +582,11 @@ export function recordWakeFailure(key, err) {
|
|
|
562
582
|
platform: p.platform,
|
|
563
583
|
workspace: p.workspace,
|
|
564
584
|
surface: p.surface,
|
|
585
|
+
...auditSurfaceModeFrom(bindable),
|
|
565
586
|
action: "surface.wake_failed",
|
|
566
587
|
detail: {
|
|
567
588
|
fifoKey: key,
|
|
568
|
-
message:
|
|
589
|
+
message: auditErrorDetail(err).error,
|
|
569
590
|
...(undeliveredAfterHeal
|
|
570
591
|
? { reason: "pruned-deployment-heal-retry-failed", remedy: "seat healed for its next wake; this message got no answer" }
|
|
571
592
|
: {}),
|
|
@@ -661,10 +682,6 @@ async function liveDmPartnerTrust(userId, teamId) {
|
|
|
661
682
|
const member = await slackAuthorIsMember(userId, teamId);
|
|
662
683
|
return member === undefined ? undefined : member ? "internal" : "external";
|
|
663
684
|
}
|
|
664
|
-
/** Reset the cached DM-partner trust (tests only). */
|
|
665
|
-
export function _resetDmTrustCache() {
|
|
666
|
-
_resetAuthorMemberCache();
|
|
667
|
-
}
|
|
668
685
|
// `wrapDmSeatFrom` (OPE-90) is DELETED at OPE-351 stage 2. It existed to remap the address of eve's
|
|
669
686
|
// NATIVE DM send onto a rotatable seat token — a bolt-on to a dispatch path a DM no longer takes. The
|
|
670
687
|
// DM is now diverted onto the `unified` channel before the forward ever happens (`scheduleDmWake`
|
|
@@ -716,9 +733,27 @@ export function mentionSiblingSuppressed(input) {
|
|
|
716
733
|
return false;
|
|
717
734
|
return input.authoredText.includes(`<@${input.botUserId}>`);
|
|
718
735
|
}
|
|
736
|
+
/**
|
|
737
|
+
* Would a TYPED message in this thread reach the agent (OPE-2524)? The thread roads hear a voice clip only then, so a clip
|
|
738
|
+
* is handled exactly as the same words typed. That is an engagement row that was not stood down: an invited thread, or a
|
|
739
|
+
* name-drop watcher thread (whose typed follow-ups `shouldDeliverReply` admits, and which wakes with `oeUninvited`). A
|
|
740
|
+
* STOOD-DOWN row is the one case `isThreadEngaged` admits and the road then drops (`shouldDeliverReply`'s `stopped`
|
|
741
|
+
* leg, `routeSupervisorEvent`'s stood-down silence), so a clip heard there would be downloaded and billed for nothing.
|
|
742
|
+
* Fail-closed: an unreadable row is not heard.
|
|
743
|
+
*/
|
|
744
|
+
async function threadHearsClips(p, threadTs) {
|
|
745
|
+
try {
|
|
746
|
+
const row = await getOperationalStore().getEngagement(p.org, p.surfaceKey, threadTs);
|
|
747
|
+
return row !== null && row.stopped !== true;
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
return false;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
719
753
|
export function buildFrontHandler(innerHandler, opts = {}) {
|
|
720
754
|
const dedupe = opts.dedupe ?? new EventDedupe();
|
|
721
755
|
const fifo = opts.fifo ?? new ThreadFifo(recordWakeFailure);
|
|
756
|
+
const hearVoice = opts.hearVoiceClips ?? ((ev) => withSlackVoiceClips(ev));
|
|
722
757
|
const getBotIdentity = opts.getBotIdentity ?? resolveBotIdentity;
|
|
723
758
|
const now = opts.now ?? (() => Date.now());
|
|
724
759
|
// THE SIGNING SECRET RESOLVES PER REQUEST, AND IT IS ASYNC SINCE OPE-2231. It used to be a
|
|
@@ -747,6 +782,7 @@ export function buildFrontHandler(innerHandler, opts = {}) {
|
|
|
747
782
|
const homeClicked = opts.handleHomeClick ?? ((click) => handleHomeClick(click, liveSlackHomeDeps()));
|
|
748
783
|
const recheckCanvas = opts.recheckChannelCanvas ?? ((input) => recheckChannelCanvas(input, liveMemoryCanvasDeps()));
|
|
749
784
|
const homeSubmitted = opts.handleHomeSubmission ?? ((submission) => handleHomeSubmission(submission, liveSlackHomeDeps()));
|
|
785
|
+
const admitBotReply = opts.admitBotReply ?? ((ev, self) => admitAskedBotReply(slackBotReplyFacts(ev, deploymentOrg(), self), ev.authoredText));
|
|
750
786
|
const ambientEnabledFor = opts.ambientEnabledFor ?? ((surfaceKey) => ambientSupervisorEnabled(surfaceKey, getConfig().loadDomains()));
|
|
751
787
|
const seatedSurface = opts.seatedSurfaceFor ?? slackDomainUsesUnifiedWindow;
|
|
752
788
|
const mentionPolicyFor = opts.mentionPolicyFor ?? slackSingleWindowMentionPolicy;
|
|
@@ -922,7 +958,11 @@ export function buildFrontHandler(innerHandler, opts = {}) {
|
|
|
922
958
|
// `onWake` runs SYNCHRONOUSLY, once, immediately before a wake is scheduled — and never on the
|
|
923
959
|
// addressed-drop path below. The shortcut route (OPE-1442) uses it to seed the attachment
|
|
924
960
|
// registry only for a click that actually wakes something; every other caller omits it.
|
|
925
|
-
|
|
961
|
+
// `hearsMentionClip` (OPE-2524) is an explicit opt-in, set by the diverted @mention road ONLY. `directMention` alone
|
|
962
|
+
// is not enough: a channel MESSAGE SHORTCUT is also an addressed wake, but the clip on its target message is someone
|
|
963
|
+
// ELSE's speech, which the shortcut fences as untrusted, and hearing it would put that person's words outside the
|
|
964
|
+
// fence at the presser's trust. A shortcut's clip is not heard.
|
|
965
|
+
const scheduleUnownedWake = (ev, label, directMention = false, onWake, seatEngaged = false, hearsMentionClip = false) => {
|
|
926
966
|
let ambient = false;
|
|
927
967
|
let surfaceKey;
|
|
928
968
|
try {
|
|
@@ -971,7 +1011,24 @@ export function buildFrontHandler(innerHandler, opts = {}) {
|
|
|
971
1011
|
// supervisorFifoKey needs no store read either, so it stays a stable, synchronous key
|
|
972
1012
|
// on the hot path exactly like the token-based key did.
|
|
973
1013
|
const supKey = supervisorFifoKey(ev.channelId);
|
|
974
|
-
args.waitUntil(fifo.enqueue(supKey,
|
|
1014
|
+
args.waitUntil(fifo.enqueue(supKey, async () => {
|
|
1015
|
+
// A voice clip is heard on an ADDRESSED seat wake only — a mention, or an engaged thread that was not stood
|
|
1016
|
+
// down. An ambient wake on unaddressed talk never downloads a room's audio (OPE-2524, slack-voice-clips.ts).
|
|
1017
|
+
// A bot author is dropped by the seat's own gate, so its clip is never paid for. The clip test comes first, so
|
|
1018
|
+
// a message with no clip pays no extra store read. The thread leg also asks the room's fail-closed internal
|
|
1019
|
+
// confirmation FIRST, the one `shouldSupervisorWake` drops on later: a thread in a room that became shared, or
|
|
1020
|
+
// cannot be confirmed internal, wakes nothing, so its clip must not reach speech-to-text. (A mention already
|
|
1021
|
+
// passed that confirmation in the single-window policy.)
|
|
1022
|
+
const hears = !ev.botId &&
|
|
1023
|
+
hasSlackVoiceClip(ev) &&
|
|
1024
|
+
((directMention && hearsMentionClip) ||
|
|
1025
|
+
(seatEngaged &&
|
|
1026
|
+
ev.threadTs !== undefined &&
|
|
1027
|
+
(await channelFactsFor(ev.channelId).then((f) => f.internal === true, () => false)) &&
|
|
1028
|
+
(await threadHearsClips(resolvePartition(bindableFor(ev)), ev.threadTs))));
|
|
1029
|
+
const heard = hears ? await hearVoice(ev) : ev;
|
|
1030
|
+
return deliverWithStrandRetry(() => routeSupervisorEvent(heard, withSingleWindowDiversion(liveSupervisorDeps(send), args.to, surfaceKey), directMention ? { directMention: true } : undefined).then(logOutcome(`${label}-supervisor`, ev.channelId, ev.ts)));
|
|
1031
|
+
}));
|
|
975
1032
|
return;
|
|
976
1033
|
}
|
|
977
1034
|
// 🔴 A NAME-DROP WAKES THE ROOM'S STANDING SEAT (OPE-1725). `routeNameDropEvent` stays the
|
|
@@ -1053,7 +1110,15 @@ export function buildFrontHandler(innerHandler, opts = {}) {
|
|
|
1053
1110
|
const scheduleDmWake = async (ev, dmKind,
|
|
1054
1111
|
/** The DM door admitted this bot author as the deployment's named e2e driver (OPE-1023).
|
|
1055
1112
|
* Defaults FALSE, so every existing call site is unchanged. */
|
|
1056
|
-
driverAdmitted = false
|
|
1113
|
+
driverAdmitted = false,
|
|
1114
|
+
/** Set only for a real `message.im` the person sent, which belongs to a Slack agent session
|
|
1115
|
+
* (OPE-2519): the words the person TYPED, before any voice clip was heard. A shortcut's minted
|
|
1116
|
+
* ts names no message, and a group DM has no agent session, so both leave it undefined and
|
|
1117
|
+
* leave the session untouched. The title reads only these words: a voice clip's transcript
|
|
1118
|
+
* rides `authoredText` behind a `[voice clip]` marker, and a clip OE could not hear adds a
|
|
1119
|
+
* host-written line, so neither is the person's typed text. */
|
|
1120
|
+
agentSessionTypedText) => {
|
|
1121
|
+
const opensAgentSession = agentSessionTypedText !== undefined;
|
|
1057
1122
|
// 🔴 THE TENANT FENCE, AND THIS IS ITS CHOKEPOINT FOR THE WHOLE DM FAMILY (OPE-1644). Before
|
|
1058
1123
|
// this line `slack-front.ts` consulted the fence NOWHERE, while the mention door, the four
|
|
1059
1124
|
// dispatching engagement routes, the channel-join door and the `/oe` slash door all did — so a
|
|
@@ -1204,6 +1269,8 @@ export function buildFrontHandler(innerHandler, opts = {}) {
|
|
|
1204
1269
|
channelType: dmKind,
|
|
1205
1270
|
isPrivate: true,
|
|
1206
1271
|
...(partnerTrust ? { dmPartnerTrust: partnerTrust } : {}),
|
|
1272
|
+
// OPE-2519: the 1:1 message's Slack agent session — its thread, or itself when top-level.
|
|
1273
|
+
...(opensAgentSession && !isGroup ? { agentSessionTs: ev.threadTs ?? ev.ts } : {}),
|
|
1207
1274
|
// 🔴 THE GROUP DM CARRIES THE PLATFORM'S OWN SHARING VERDICT (OPE-1486), which is what a
|
|
1208
1275
|
// channel wake has always stamped (`authForEvent`) and an mpim wake never did. `bindSlack`
|
|
1209
1276
|
// reads the pair through `slackTrust`, so this is the binding's only route to the fact
|
|
@@ -1256,6 +1323,18 @@ export function buildFrontHandler(innerHandler, opts = {}) {
|
|
|
1256
1323
|
wakeId,
|
|
1257
1324
|
...(seat.freshGeneration && seat.tail.length > 0 ? { backscroll: seat.tail } : {}),
|
|
1258
1325
|
});
|
|
1326
|
+
// THE SESSION TITLE (OPE-2519). A top-level 1:1 message opens a new conversation in the Messages
|
|
1327
|
+
// tab or the side panel, and Slack keys its agent session by that message. Create the session with
|
|
1328
|
+
// a title from the person's own words BEFORE the wake: the turn's `processing` call would
|
|
1329
|
+
// otherwise create it untitled first, and Slack applies a title only on creation. That same rule
|
|
1330
|
+
// keeps a title the person typed over ours. A group DM, a shortcut and a thread reply open no new
|
|
1331
|
+
// session. The title reads the TYPED text alone, before any voice clip was heard: `text` also
|
|
1332
|
+
// carries the lines of a forwarded or quoted message, which are another conversation's words.
|
|
1333
|
+
if (opensAgentSession && !isGroup && !ev.threadTs && ev.ts) {
|
|
1334
|
+
const title = slackSessionTitle(agentSessionTypedText);
|
|
1335
|
+
if (title)
|
|
1336
|
+
await setSlackSessionStatus({ channelId: ev.channelId, threadTs: ev.ts, status: "active", title });
|
|
1337
|
+
}
|
|
1259
1338
|
// 🔴 NO `replyAnchorTs` ON A TOP-LEVEL DM — the reply stays FLAT in the conversation. The
|
|
1260
1339
|
// CHANNEL divert DOES set one (slack-events.ts), because threading an answer under its trigger
|
|
1261
1340
|
// keeps a busy shared channel tidy; the slack post arm reads `threadTs ?? replyAnchorTs`
|
|
@@ -1395,6 +1474,16 @@ export function buildFrontHandler(innerHandler, opts = {}) {
|
|
|
1395
1474
|
// is the one cost no placement in this branch avoids.
|
|
1396
1475
|
if (slackFrontTenantFenceDeclined(dev, `dm-${decision.dmKind}`, "front-dm-branch"))
|
|
1397
1476
|
return fencedAck(OK());
|
|
1477
|
+
// 🔴 AN EDIT THAT CHANGED NOTHING IS NOT A WAKE (OPE-2577). OE's own agent-session status calls
|
|
1478
|
+
// edit the person's root message, and each edit reaches this door as a `message_changed` with
|
|
1479
|
+
// the person as author. Woken, each one was a new inbound wake that made the answer to the
|
|
1480
|
+
// person's real message stale. See {@link isUnchangedSlackEdit}. Above the driver carve-out, so
|
|
1481
|
+
// the driver's DM and a person's DM take the same drop. A log line only, no audit row: three of
|
|
1482
|
+
// these follow every DM turn, the same reasoning that keeps our own reply echo out of the trail.
|
|
1483
|
+
if (dev.unchangedEdit) {
|
|
1484
|
+
console.log(`[open-everywhere:slack-front] ${decision.dmKind} ${dev.channelId}/${dev.ts}: no wake — ${UNCHANGED_EDIT_DROP_REASON}`);
|
|
1485
|
+
return OK();
|
|
1486
|
+
}
|
|
1398
1487
|
// A bot-authored DM is never a wake — a no-op 200. NOT a forward: forwarding a bot_message
|
|
1399
1488
|
// would hand eve's native door a turn the DM window is meant to own.
|
|
1400
1489
|
//
|
|
@@ -1484,7 +1573,10 @@ export function buildFrontHandler(innerHandler, opts = {}) {
|
|
|
1484
1573
|
// never shared with a channel supervisor's or a thread's. The mention-carried divert below
|
|
1485
1574
|
// enqueues on the SAME key, so a group DM's two legs can never run concurrently on one seat.
|
|
1486
1575
|
args.waitUntil(fifo.enqueue(dmFifoKey(dev.channelId), async () => {
|
|
1487
|
-
|
|
1576
|
+
// A voice clip is heard ONCE, before the retry wrapper, so a strand retry never pays for a second
|
|
1577
|
+
// transcription (OPE-2524, slack-voice-clips.ts). A DM is always addressed, so its clips are always heard.
|
|
1578
|
+
const heard = await hearVoice(dev);
|
|
1579
|
+
await deliverWithStrandRetry(() => scheduleDmWake(heard, decision.dmKind, driverAdmitted, decision.dmKind === "im" ? dev.authoredText : undefined));
|
|
1488
1580
|
console.log(`[open-everywhere:slack-front] ${decision.dmKind} ${dev.channelId}/${dev.ts}: woke — diverted to the DM window seat`);
|
|
1489
1581
|
}));
|
|
1490
1582
|
return OK();
|
|
@@ -1643,7 +1735,7 @@ export function buildFrontHandler(innerHandler, opts = {}) {
|
|
|
1643
1735
|
// directMention path (slack-events.ts routeSupervisorEvent) and re-checks its own
|
|
1644
1736
|
// fail-closed internal confirmation — the SAME process-local TTL-cached conversations.info
|
|
1645
1737
|
// the policy just populated, so the two reads on this one webhook are cache-coherent.
|
|
1646
|
-
scheduleUnownedWake(mev, "single-window-mention", true);
|
|
1738
|
+
scheduleUnownedWake(mev, "single-window-mention", true, undefined, false, true);
|
|
1647
1739
|
return OK();
|
|
1648
1740
|
}
|
|
1649
1741
|
// 🔴 THE GROUP-DM MENTION LEG (OPE-351 stage 4). An @mention is how a human addresses the bot in
|
|
@@ -1680,7 +1772,9 @@ export function buildFrontHandler(innerHandler, opts = {}) {
|
|
|
1680
1772
|
// re-reading a field that was never there; `scheduleDmWake` stamps it onto the wake auth,
|
|
1681
1773
|
// which is what makes `bindSlack` bind mode `mpim` on this leg exactly as on the other.
|
|
1682
1774
|
args.waitUntil(fifo.enqueue(dmFifoKey(mev.channelId), async () => {
|
|
1683
|
-
|
|
1775
|
+
// A bot's group-DM mention is not heard: `scheduleDmWake` drops a bot author after this step.
|
|
1776
|
+
const heard = mev.botId ? mev : await hearVoice(mev);
|
|
1777
|
+
await deliverWithStrandRetry(() => scheduleDmWake({ ...heard, channelType: "mpim" }, "mpim"));
|
|
1684
1778
|
}));
|
|
1685
1779
|
return OK();
|
|
1686
1780
|
}
|
|
@@ -1827,6 +1921,51 @@ export function buildFrontHandler(innerHandler, opts = {}) {
|
|
|
1827
1921
|
}));
|
|
1828
1922
|
return new Response(null, { status: 200 });
|
|
1829
1923
|
}
|
|
1924
|
+
if (decision.kind === "feedback-click") {
|
|
1925
|
+
// THE TENANT FENCE FIRST (OPE-1680), as every click branch: a foreign workspace writes no row.
|
|
1926
|
+
if (slackInteractivityTenantFenceDeclined(interactivityFenceIds(decision.payload), "feedback-click", "front-interactivity-branch")) {
|
|
1927
|
+
return fencedAck(new Response(null, { status: 200 }));
|
|
1928
|
+
}
|
|
1929
|
+
// OPE-2520 — a rating, recorded after the acknowledgement. Slack draws the pressed state itself,
|
|
1930
|
+
// so the empty 200 is the whole answer, and the row write never holds the click up.
|
|
1931
|
+
const click = decision.click;
|
|
1932
|
+
args.waitUntil(routeSlackFeedbackClick(click)
|
|
1933
|
+
.then((recorded) => {
|
|
1934
|
+
if (!recorded)
|
|
1935
|
+
console.warn("[open-everywhere:slack-front] a thumbs click was not recorded");
|
|
1936
|
+
})
|
|
1937
|
+
.catch(() => undefined));
|
|
1938
|
+
return new Response(null, { status: 200 });
|
|
1939
|
+
}
|
|
1940
|
+
if (decision.kind === "followup-click") {
|
|
1941
|
+
// OPE-2516 — the person pressed a suggested follow-up under an answer in their DM.
|
|
1942
|
+
// THE TENANT FENCE FIRST (OPE-1680), as every click branch. `scheduleDmWake` refuses the same
|
|
1943
|
+
// workspace on its own; this spares an unadmitted one the rewrite and the dedupe write.
|
|
1944
|
+
const click = decision.click;
|
|
1945
|
+
const sev = followUpMessageEvent(click, now());
|
|
1946
|
+
if (slackInteractivityTenantFenceDeclined(interactivityFenceIds(decision.payload), "followup-click", "front-interactivity-branch")) {
|
|
1947
|
+
return fencedAck(new Response(null, { status: 200 }));
|
|
1948
|
+
}
|
|
1949
|
+
// One answer takes one press: a double press, or Slack's redelivery of one, wakes the seat once.
|
|
1950
|
+
if (!(await notYetSeenKey(followUpPressKey(click), sev)))
|
|
1951
|
+
return new Response(null, { status: 200 });
|
|
1952
|
+
// ACK FIRST, WORK AFTER, as the shortcut branch: the rewrite and the wake both ride `waitUntil`.
|
|
1953
|
+
// The rewrite removes the buttons and shows what the person sent. The wake takes the SAME road a
|
|
1954
|
+
// typed DM takes, so the seat, the trust probe, the external-participant policy and the spend
|
|
1955
|
+
// gate all decide it. The agent session is touched only when the answer sits in a thread: a
|
|
1956
|
+
// top-level answer has no session thread for a minted ts to name.
|
|
1957
|
+
args.waitUntil(markFollowUpPressed(click)
|
|
1958
|
+
.then((rewritten) => {
|
|
1959
|
+
if (!rewritten)
|
|
1960
|
+
console.log(`[open-everywhere:slack-front] follow-up press in ${click.channelId}: the answer was not rewritten`);
|
|
1961
|
+
})
|
|
1962
|
+
.catch(() => undefined));
|
|
1963
|
+
args.waitUntil(fifo.enqueue(dmFifoKey(click.channelId), async () => {
|
|
1964
|
+
await deliverWithStrandRetry(() => scheduleDmWake(sev, "im", false, click.threadTs === undefined ? undefined : click.text));
|
|
1965
|
+
console.log(`[open-everywhere:slack-front] follow-up press in ${click.channelId}: woke the DM window`);
|
|
1966
|
+
}));
|
|
1967
|
+
return new Response(null, { status: 200 });
|
|
1968
|
+
}
|
|
1830
1969
|
if (decision.kind === "home-open" || decision.kind === "home-click" || decision.kind === "home-submit") {
|
|
1831
1970
|
// THE HOME TAB (OPE-2451). THE TENANT FENCE FIRST (OPE-1680), for the reason the approval branch
|
|
1832
1971
|
// states: the page reads and changes a person's memory, keyed to a bare per-workspace member id,
|
|
@@ -1980,6 +2119,42 @@ export function buildFrontHandler(innerHandler, opts = {}) {
|
|
|
1980
2119
|
args.waitUntil(routeUserDeactivation(deactivation).then((outcome) => console.log(`[open-everywhere:slack-front] user_change ${deactivation.userId}: offboarding ${outcome.kind}${outcome.kind === "refused" ? ` (${outcome.reason})` : ""}`)));
|
|
1981
2120
|
return OK();
|
|
1982
2121
|
}
|
|
2122
|
+
if (decision.kind === "session-stop") {
|
|
2123
|
+
// SLACK'S NATIVE STOP (OPE-2519) — the same cancel door as the ✋ arm below, with the same fence
|
|
2124
|
+
// in front of it: an unadmitted workspace's press must never reach `args.attachSession`.
|
|
2125
|
+
const stop = parseAgentSessionStopped(decision.env);
|
|
2126
|
+
if (!stop)
|
|
2127
|
+
return OK();
|
|
2128
|
+
const stopEnterpriseId = slackEnterpriseIdFromEnvelope(decision.env);
|
|
2129
|
+
if (slackInteractivityTenantFenceDeclined({
|
|
2130
|
+
...(stop.teamId === undefined ? {} : { teamId: stop.teamId }),
|
|
2131
|
+
...(stopEnterpriseId === undefined ? {} : { enterpriseId: stopEnterpriseId }),
|
|
2132
|
+
channelId: stop.channelId,
|
|
2133
|
+
userId: stop.userId,
|
|
2134
|
+
}, "session-stop", "front-session-stop-branch")) {
|
|
2135
|
+
return fencedAck(OK());
|
|
2136
|
+
}
|
|
2137
|
+
// A Slack RETRY of the same press must not send a second cancel: in-memory first, then durable.
|
|
2138
|
+
const skey = wakeKey(decision.env, stop.channelId, stop.eventTs ?? stop.threadTs);
|
|
2139
|
+
if (dedupe.seen(skey, now()))
|
|
2140
|
+
return OK();
|
|
2141
|
+
dedupe.mark(skey, now());
|
|
2142
|
+
try {
|
|
2143
|
+
const org = resolvePartition(bindableForSlackIds({ teamId: stop.teamId, channelId: stop.channelId, userId: stop.userId })).org;
|
|
2144
|
+
if (!(await markEventSeen(org, skey, new Date(now()).toISOString())))
|
|
2145
|
+
return OK();
|
|
2146
|
+
}
|
|
2147
|
+
catch {
|
|
2148
|
+
/* unresolvable partition → in-memory dedupe only (fail-open, as for a reaction) */
|
|
2149
|
+
}
|
|
2150
|
+
if (!args.attachSession) {
|
|
2151
|
+
console.warn(`[open-everywhere:slack-front] session stop ${stop.channelId}/${stop.threadTs}: no cancel door on this route — nothing stopped`);
|
|
2152
|
+
return OK();
|
|
2153
|
+
}
|
|
2154
|
+
const attachSession = args.attachSession;
|
|
2155
|
+
args.waitUntil(routeAgentSessionStop(stop, { attachSession }).then((r) => console.log(`[open-everywhere:slack-front] session stop ${stop.channelId}/${stop.threadTs}: ${r.outcome}`)));
|
|
2156
|
+
return OK();
|
|
2157
|
+
}
|
|
1983
2158
|
if (decision.kind === "reaction") {
|
|
1984
2159
|
// Inbound reaction (feedback signal). Parse it; an unusable envelope is a no-op 200.
|
|
1985
2160
|
const rev = parseSlackReactionEvent(decision.env);
|
|
@@ -2034,10 +2209,35 @@ export function buildFrontHandler(innerHandler, opts = {}) {
|
|
|
2034
2209
|
const ev = parseMessageEvent(decision.env);
|
|
2035
2210
|
if (!ev)
|
|
2036
2211
|
return OK();
|
|
2212
|
+
// An edit that changed no words and no files (a link unfurl, for example) is not news here either:
|
|
2213
|
+
// the engagement and supervisor gates keep `message_changed` so a person's REAL edit wakes, and
|
|
2214
|
+
// this keeps a metadata-only one out (OPE-2577, {@link isUnchangedSlackEdit}).
|
|
2215
|
+
if (ev.unchangedEdit)
|
|
2216
|
+
return OK();
|
|
2037
2217
|
persistAttachments(ev); // durable upload registry (task #107) — so a delegated worker can read this message's files
|
|
2218
|
+
// 🔴 THE ASKED-THREAD EXCEPTION (OPE-2426): the only road by which another bot's message WAKES the
|
|
2219
|
+
// agent on a real deployment. (An attributable bot's earlier post can still enter a woken seat's
|
|
2220
|
+
// backscroll as context-only awareness: slack-wake-backscroll.ts `isBackscrollVisible`.) The other bot admissions are the test-bed e2e driver settings (OPE-1023):
|
|
2221
|
+
// `wake.driverAllowlist` on the engagement and mention doors, channel messages included, and
|
|
2222
|
+
// `wake.e2eDrivers` on the DM door and the seat's own reads. A real deployment leaves both empty. A bot's reply in a thread where the person's DM asked THAT bot, inside the window, is
|
|
2223
|
+
// relayed to the person's DM and stops here: it never wakes this channel's seat, and the host posts
|
|
2224
|
+
// nothing in the thread. Every other bot message — a top-level post, a thread nobody asked it in, a
|
|
2225
|
+
// different bot, a late reply, an edit — gets null and falls through to the ordinary bot-author drop
|
|
2226
|
+
// below, unchanged. It runs BEFORE mention suppression, so a reply that also @mentions the agent is
|
|
2227
|
+
// still relayed. Its `app_mention` twin is dropped as a bot author further down: by
|
|
2228
|
+
// `shouldSupervisorWake` on the single-window road, or by the native mention handler (slack.ts). The relay
|
|
2229
|
+
// claims each message once, so a retried event relays nothing twice.
|
|
2230
|
+
const identity = await getBotIdentity();
|
|
2231
|
+
if (botReplyCandidate(ev)) {
|
|
2232
|
+
const admitted = await admitBotReply(ev, identity).catch(() => null);
|
|
2233
|
+
if (admitted) {
|
|
2234
|
+
args.waitUntil(admitted.relay().then((route) => console.log(`[open-everywhere:slack-front] asked-thread bot reply ${ev.channelId}/${ev.threadTs}: relayed to the asker's DM (${String(route)})`)));
|
|
2235
|
+
return OK();
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2038
2238
|
// Mention-sibling suppression — see {@link mentionSiblingSuppressed} for the whole rule and for
|
|
2039
2239
|
// why an EDIT is exempt from it.
|
|
2040
|
-
const { userId: botUserId } =
|
|
2240
|
+
const { userId: botUserId } = identity;
|
|
2041
2241
|
if (mentionSiblingSuppressed({ authoredText: ev.authoredText, ...(ev.subtype === undefined ? {} : { subtype: ev.subtype }), ...(botUserId === undefined ? {} : { botUserId }) })) {
|
|
2042
2242
|
return OK();
|
|
2043
2243
|
}
|
|
@@ -2095,7 +2295,14 @@ export function buildFrontHandler(innerHandler, opts = {}) {
|
|
|
2095
2295
|
// failure mode — two burst replies racing — is cosmetic, unlike the duplicate-delivery
|
|
2096
2296
|
// problem the durable dedupe above closes.)
|
|
2097
2297
|
const threadKey = slackContinuationToken(ev.channelId, ev.threadTs);
|
|
2098
|
-
args.waitUntil(
|
|
2298
|
+
args.waitUntil(
|
|
2299
|
+
// A human's voice clip is heard here unless the thread was stood down (OPE-2524): `shouldDeliverReply` drops a bot
|
|
2300
|
+
// author and a stood-down thread, so their clips are never paid for. The clip test comes first, so a message with
|
|
2301
|
+
// no clip pays no extra store read.
|
|
2302
|
+
fifo.enqueue(threadKey, async () => {
|
|
2303
|
+
const heard = !ev.botId && hasSlackVoiceClip(ev) && (await threadHearsClips(p, ev.threadTs)) ? await hearVoice(ev) : ev;
|
|
2304
|
+
return routeMessageEvent(heard, liveDeps(send)).then(logOutcome("engaged-thread", ev.channelId, ev.threadTs));
|
|
2305
|
+
}));
|
|
2099
2306
|
return OK();
|
|
2100
2307
|
}
|
|
2101
2308
|
}
|