@open-everywhere/provider-notion 0.1.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 (186) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +48 -0
  4. package/agent/channels/notion-acting-identity.ts +88 -0
  5. package/agent/channels/notion-ancestry-trust.ts +458 -0
  6. package/agent/channels/notion-api.ts +1290 -0
  7. package/agent/channels/notion-bot-user.ts +250 -0
  8. package/agent/channels/notion-catch-up.ts +339 -0
  9. package/agent/channels/notion-credentials.ts +417 -0
  10. package/agent/channels/notion-engagement.ts +372 -0
  11. package/agent/channels/notion-event-normalizer.ts +587 -0
  12. package/agent/channels/notion-identity.ts +271 -0
  13. package/agent/channels/notion-ingress-drain.ts +289 -0
  14. package/agent/channels/notion-ingress.ts +182 -0
  15. package/agent/channels/notion-message-ops.ts +140 -0
  16. package/agent/channels/notion-origin-codec.ts +38 -0
  17. package/agent/channels/notion-post-arm.ts +189 -0
  18. package/agent/channels/notion-question-return.ts +223 -0
  19. package/agent/channels/notion-read.ts +283 -0
  20. package/agent/channels/notion-scope.ts +560 -0
  21. package/agent/channels/notion-stop.ts +169 -0
  22. package/agent/channels/notion-supervisor.ts +856 -0
  23. package/agent/channels/notion-webhook-timing.ts +131 -0
  24. package/agent/channels/notion-webhook-verifier.ts +55 -0
  25. package/agent/channels/notion-webhook.ts +963 -0
  26. package/agent/channels/notion-work-search.ts +74 -0
  27. package/agent/channels/notion-work.ts +144 -0
  28. package/agent/channels/notion.ts +1242 -0
  29. package/agent/contributions.ts +39 -0
  30. package/agent/docs-generated.ts +15 -0
  31. package/agent/memory/blocks.ts +295 -0
  32. package/agent/memory/memory.ts +622 -0
  33. package/agent/provider-events.ts +198 -0
  34. package/agent/register.ts +144 -0
  35. package/agent/schedules/notion-ingress.ts +20 -0
  36. package/agent/surface-parity.ts +105 -0
  37. package/agent/tools/notion_search.ts +110 -0
  38. package/agent/tools/read_notion_comments.ts +250 -0
  39. package/agent/tools/read_notion_page.ts +351 -0
  40. package/dist/agent/channels/notion-acting-identity.d.ts +23 -0
  41. package/dist/agent/channels/notion-acting-identity.d.ts.map +1 -0
  42. package/dist/agent/channels/notion-acting-identity.js +85 -0
  43. package/dist/agent/channels/notion-acting-identity.js.map +1 -0
  44. package/dist/agent/channels/notion-ancestry-trust.d.ts +208 -0
  45. package/dist/agent/channels/notion-ancestry-trust.d.ts.map +1 -0
  46. package/dist/agent/channels/notion-ancestry-trust.js +331 -0
  47. package/dist/agent/channels/notion-ancestry-trust.js.map +1 -0
  48. package/dist/agent/channels/notion-api.d.ts +655 -0
  49. package/dist/agent/channels/notion-api.d.ts.map +1 -0
  50. package/dist/agent/channels/notion-api.js +938 -0
  51. package/dist/agent/channels/notion-api.js.map +1 -0
  52. package/dist/agent/channels/notion-bot-user.d.ts +107 -0
  53. package/dist/agent/channels/notion-bot-user.d.ts.map +1 -0
  54. package/dist/agent/channels/notion-bot-user.js +192 -0
  55. package/dist/agent/channels/notion-bot-user.js.map +1 -0
  56. package/dist/agent/channels/notion-catch-up.d.ts +64 -0
  57. package/dist/agent/channels/notion-catch-up.d.ts.map +1 -0
  58. package/dist/agent/channels/notion-catch-up.js +289 -0
  59. package/dist/agent/channels/notion-catch-up.js.map +1 -0
  60. package/dist/agent/channels/notion-credentials.d.ts +210 -0
  61. package/dist/agent/channels/notion-credentials.d.ts.map +1 -0
  62. package/dist/agent/channels/notion-credentials.js +360 -0
  63. package/dist/agent/channels/notion-credentials.js.map +1 -0
  64. package/dist/agent/channels/notion-engagement.d.ts +219 -0
  65. package/dist/agent/channels/notion-engagement.d.ts.map +1 -0
  66. package/dist/agent/channels/notion-engagement.js +283 -0
  67. package/dist/agent/channels/notion-engagement.js.map +1 -0
  68. package/dist/agent/channels/notion-event-normalizer.d.ts +194 -0
  69. package/dist/agent/channels/notion-event-normalizer.d.ts.map +1 -0
  70. package/dist/agent/channels/notion-event-normalizer.js +542 -0
  71. package/dist/agent/channels/notion-event-normalizer.js.map +1 -0
  72. package/dist/agent/channels/notion-identity.d.ts +119 -0
  73. package/dist/agent/channels/notion-identity.d.ts.map +1 -0
  74. package/dist/agent/channels/notion-identity.js +205 -0
  75. package/dist/agent/channels/notion-identity.js.map +1 -0
  76. package/dist/agent/channels/notion-ingress-drain.d.ts +66 -0
  77. package/dist/agent/channels/notion-ingress-drain.d.ts.map +1 -0
  78. package/dist/agent/channels/notion-ingress-drain.js +185 -0
  79. package/dist/agent/channels/notion-ingress-drain.js.map +1 -0
  80. package/dist/agent/channels/notion-ingress.d.ts +64 -0
  81. package/dist/agent/channels/notion-ingress.d.ts.map +1 -0
  82. package/dist/agent/channels/notion-ingress.js +130 -0
  83. package/dist/agent/channels/notion-ingress.js.map +1 -0
  84. package/dist/agent/channels/notion-message-ops.d.ts +3 -0
  85. package/dist/agent/channels/notion-message-ops.d.ts.map +1 -0
  86. package/dist/agent/channels/notion-message-ops.js +126 -0
  87. package/dist/agent/channels/notion-message-ops.js.map +1 -0
  88. package/dist/agent/channels/notion-origin-codec.d.ts +7 -0
  89. package/dist/agent/channels/notion-origin-codec.d.ts.map +1 -0
  90. package/dist/agent/channels/notion-origin-codec.js +32 -0
  91. package/dist/agent/channels/notion-origin-codec.js.map +1 -0
  92. package/dist/agent/channels/notion-post-arm.d.ts +3 -0
  93. package/dist/agent/channels/notion-post-arm.d.ts.map +1 -0
  94. package/dist/agent/channels/notion-post-arm.js +185 -0
  95. package/dist/agent/channels/notion-post-arm.js.map +1 -0
  96. package/dist/agent/channels/notion-question-return.d.ts +77 -0
  97. package/dist/agent/channels/notion-question-return.d.ts.map +1 -0
  98. package/dist/agent/channels/notion-question-return.js +183 -0
  99. package/dist/agent/channels/notion-question-return.js.map +1 -0
  100. package/dist/agent/channels/notion-read.d.ts +62 -0
  101. package/dist/agent/channels/notion-read.d.ts.map +1 -0
  102. package/dist/agent/channels/notion-read.js +257 -0
  103. package/dist/agent/channels/notion-read.js.map +1 -0
  104. package/dist/agent/channels/notion-scope.d.ts +188 -0
  105. package/dist/agent/channels/notion-scope.d.ts.map +1 -0
  106. package/dist/agent/channels/notion-scope.js +433 -0
  107. package/dist/agent/channels/notion-scope.js.map +1 -0
  108. package/dist/agent/channels/notion-stop.d.ts +48 -0
  109. package/dist/agent/channels/notion-stop.d.ts.map +1 -0
  110. package/dist/agent/channels/notion-stop.js +141 -0
  111. package/dist/agent/channels/notion-stop.js.map +1 -0
  112. package/dist/agent/channels/notion-supervisor.d.ts +462 -0
  113. package/dist/agent/channels/notion-supervisor.d.ts.map +1 -0
  114. package/dist/agent/channels/notion-supervisor.js +482 -0
  115. package/dist/agent/channels/notion-supervisor.js.map +1 -0
  116. package/dist/agent/channels/notion-webhook-timing.d.ts +35 -0
  117. package/dist/agent/channels/notion-webhook-timing.d.ts.map +1 -0
  118. package/dist/agent/channels/notion-webhook-timing.js +99 -0
  119. package/dist/agent/channels/notion-webhook-timing.js.map +1 -0
  120. package/dist/agent/channels/notion-webhook-verifier.d.ts +5 -0
  121. package/dist/agent/channels/notion-webhook-verifier.d.ts.map +1 -0
  122. package/dist/agent/channels/notion-webhook-verifier.js +53 -0
  123. package/dist/agent/channels/notion-webhook-verifier.js.map +1 -0
  124. package/dist/agent/channels/notion-webhook.d.ts +242 -0
  125. package/dist/agent/channels/notion-webhook.d.ts.map +1 -0
  126. package/dist/agent/channels/notion-webhook.js +675 -0
  127. package/dist/agent/channels/notion-webhook.js.map +1 -0
  128. package/dist/agent/channels/notion-work-search.d.ts +9 -0
  129. package/dist/agent/channels/notion-work-search.d.ts.map +1 -0
  130. package/dist/agent/channels/notion-work-search.js +69 -0
  131. package/dist/agent/channels/notion-work-search.js.map +1 -0
  132. package/dist/agent/channels/notion-work.d.ts +68 -0
  133. package/dist/agent/channels/notion-work.d.ts.map +1 -0
  134. package/dist/agent/channels/notion-work.js +126 -0
  135. package/dist/agent/channels/notion-work.js.map +1 -0
  136. package/dist/agent/channels/notion.d.ts +95 -0
  137. package/dist/agent/channels/notion.d.ts.map +1 -0
  138. package/dist/agent/channels/notion.js +1081 -0
  139. package/dist/agent/channels/notion.js.map +1 -0
  140. package/dist/agent/contributions.d.ts +32 -0
  141. package/dist/agent/contributions.d.ts.map +1 -0
  142. package/dist/agent/contributions.js +37 -0
  143. package/dist/agent/contributions.js.map +1 -0
  144. package/dist/agent/docs-generated.d.ts +3 -0
  145. package/dist/agent/docs-generated.d.ts.map +1 -0
  146. package/dist/agent/docs-generated.js +13 -0
  147. package/dist/agent/docs-generated.js.map +1 -0
  148. package/dist/agent/memory/blocks.d.ts +58 -0
  149. package/dist/agent/memory/blocks.d.ts.map +1 -0
  150. package/dist/agent/memory/blocks.js +264 -0
  151. package/dist/agent/memory/blocks.js.map +1 -0
  152. package/dist/agent/memory/memory.d.ts +157 -0
  153. package/dist/agent/memory/memory.d.ts.map +1 -0
  154. package/dist/agent/memory/memory.js +471 -0
  155. package/dist/agent/memory/memory.js.map +1 -0
  156. package/dist/agent/provider-events.d.ts +3 -0
  157. package/dist/agent/provider-events.d.ts.map +1 -0
  158. package/dist/agent/provider-events.js +196 -0
  159. package/dist/agent/provider-events.js.map +1 -0
  160. package/dist/agent/register.d.ts +5 -0
  161. package/dist/agent/register.d.ts.map +1 -0
  162. package/dist/agent/register.js +128 -0
  163. package/dist/agent/register.js.map +1 -0
  164. package/dist/agent/schedules/notion-ingress.d.ts +6 -0
  165. package/dist/agent/schedules/notion-ingress.d.ts.map +1 -0
  166. package/dist/agent/schedules/notion-ingress.js +16 -0
  167. package/dist/agent/schedules/notion-ingress.js.map +1 -0
  168. package/dist/agent/surface-parity.d.ts +3 -0
  169. package/dist/agent/surface-parity.d.ts.map +1 -0
  170. package/dist/agent/surface-parity.js +99 -0
  171. package/dist/agent/surface-parity.js.map +1 -0
  172. package/dist/agent/tools/notion_search.d.ts +56 -0
  173. package/dist/agent/tools/notion_search.d.ts.map +1 -0
  174. package/dist/agent/tools/notion_search.js +100 -0
  175. package/dist/agent/tools/notion_search.js.map +1 -0
  176. package/dist/agent/tools/read_notion_comments.d.ts +57 -0
  177. package/dist/agent/tools/read_notion_comments.d.ts.map +1 -0
  178. package/dist/agent/tools/read_notion_comments.js +236 -0
  179. package/dist/agent/tools/read_notion_comments.js.map +1 -0
  180. package/dist/agent/tools/read_notion_page.d.ts +53 -0
  181. package/dist/agent/tools/read_notion_page.d.ts.map +1 -0
  182. package/dist/agent/tools/read_notion_page.js +327 -0
  183. package/dist/agent/tools/read_notion_page.js.map +1 -0
  184. package/package.json +231 -0
  185. package/tsconfig.build.json +22 -0
  186. package/tsconfig.json +15 -0
@@ -0,0 +1,169 @@
1
+ // THE NOTION STOP ARM (OPE-989) — the nearest honest equivalent to the ✋ reaction.
2
+ //
3
+ // ════════════════════════════════════════════════════════════════════════════════════════════════
4
+ // 🔴 NOTION SENDS NO REACTION WEBHOOK. THIS IS THE VENDOR EVIDENCE, AND IT IS WHY THIS FILE READS A
5
+ // COMMENT INSTEAD OF AN EMOJI.
6
+ // ════════════════════════════════════════════════════════════════════════════════════════════════
7
+ //
8
+ // Notion's complete webhook event catalogue (developers.notion.com/reference/webhooks-events-
9
+ // delivery, read 2026-08-23) is `page.*`, `database.*`/`data_source.*` and exactly THREE comment
10
+ // events: `comment.created`, `comment.updated`, `comment.deleted`. There is no reaction resource on
11
+ // any of them — the same gap GitHub's webhook catalogue has, and the same answer applies: the
12
+ // trigger is a bare `stop` comment, read at the ingress before anything is dispatched.
13
+ // `channels/turn-cancel.ts`'s `isStopDirective` owns the grammar (one word, optional `/oe ` prefix,
14
+ // optional trailing punctuation) and its header carries the reasoning for why the match is narrow.
15
+ //
16
+ // UNLIKE GITHUB, THE WEBHOOK CARRIES NO TEXT. A Notion `comment.created` delivery is a SIGNAL ONLY
17
+ // (the comment id, not its body — notion-webhook.ts's header), so the two-step fetch
18
+ // (`retrieveComment`) that the webhook core already runs for the ambient path is what supplies the
19
+ // text this arm reads. There is no separate "parse the raw delivery" function here the way GitHub's
20
+ // `parseGitHubStopComment` has one: `notionCommentIsStopDirective` takes the ALREADY-FETCHED comment.
21
+ //
22
+ // THE ACTOR GUARD FAILS CLOSED, THE SAME WAY THE AMBIENT LOOP GUARD DOES (notion-supervisor.ts
23
+ // `classifyNotionComment`). The agent's own reply rides the same webhook (`createCommentReply` fires
24
+ // a fresh `comment.created`), so behind `cancel()` a fail-OPEN identity check is a self-cancel:
25
+ // 1. UNRESOLVED PRIMARY bot user id ⇒ refuse. Without our own id we cannot rule out that this IS
26
+ // our own comment, so treating it as a stop would be a coin flip on self-cancellation.
27
+ // 2. SELF-authored (`created_by.id` in {@link NotionOwnBotIds.all}) ⇒ refuse. The set is EVERY
28
+ // configured integration's bot user id (OPE-1921), so a sibling integration's own comment
29
+ // cannot cancel this one's turn.
30
+ // 3. UNIDENTIFIABLE author (no `created_by.id`) ⇒ refuse (OPE-1906). Arm 2 is the only one that
31
+ // recognises our own words, and with no id it cannot run — what is left is arm 4, on a field
32
+ // Notion OMITS. So an author nobody can name could cancel the page's running turn.
33
+ // 4. BOT/integration-authored (`created_by.type` present and not `"person"`) ⇒ refuse — the
34
+ // bot-to-bot loop guard. 🔴 A BACKSTOP, never the self-check: `created_by.type` is usually
35
+ // ABSENT on a comment, so a FOREIGN bot reads here as a person. Arm 2's set is what answers
36
+ // "is it ours"; arm 5 is what answers "we cannot tell".
37
+ // 5. INCOMPLETE OWN-ID SET on an author Notion states nothing about ⇒ refuse (OPE-1921). One
38
+ // configured integration's id did not resolve, so arm 2 could not have recognised that
39
+ // integration's own `stop`. `classifyNotionComment` step 5 fails closed on the same fact.
40
+ // Any of these five leaves the comment UNCLASSIFIED as a stop, not rejected outright: the webhook
41
+ // core falls through to its ordinary ambient/classify path, where the very same checks drop it again
42
+ // for the ordinary reason.
43
+ // 🔴 A GENUINE HUMAN `stop` PASSES ALL FIVE ONLY WHILE THIS DEPLOYMENT CAN NAME ITS OWN IDS, and the
44
+ // two identity arms are the exceptions. Arm 1 (primary unresolved) and arm 5 (a sibling unresolved,
45
+ // on an author Notion states nothing about) refuse a REAL person's `stop` too: the comment then takes
46
+ // the ordinary classify path, which drops it for its own fail-closed reason, so no `cancelAnchoredTurn`
47
+ // runs and the person gets no reply. That is the accepted cost of never self-cancelling on a coin
48
+ // flip — an identity we cannot establish is not treated as a human — and it is why the drop is
49
+ // recorded under the classifier's reason rather than as a cancel.
50
+ //
51
+ // THE CONVERSATION KEY IS THE PAGE, NOT THE DISCUSSION. The Notion doc-session is PAGE-scoped
52
+ // (notion-supervisor.ts: "a page-scoped standing session aware of all the doc's discussions"), so a
53
+ // stop trigger anywhere on the page names the newest running turn on that page — the `/oe stop`
54
+ // grain the Teams arm's header documents. `notionConversationKey` (notion-identity.ts) is the ONE
55
+ // derivation both {@link bindNotion}'s `anchorChannelId` and this file's `routeNotionStopComment`
56
+ // use, so the registry write and the registry read can never drift into two spellings.
57
+ //
58
+ // IT SUPPRESSES THE REST OF THE PIPELINE FOR ITS OWN DELIVERY. notion-webhook.ts's `deliver()` runs
59
+ // this check BEFORE the ambient gate and BEFORE classify/dispatch — waking the supervisor on the very
60
+ // comment that asked it to stand down would be exactly the failure GitHub's `github.ts` avoids by
61
+ // suppressing its own ambient leg for a stop delivery.
62
+
63
+ import {
64
+ cancelAnchoredTurn,
65
+ resolvePartition,
66
+ turnChannelKey,
67
+ type AttachSessionForCancel,
68
+ type CancelOutcome,
69
+ } from "@open-everywhere/core/agent/adapter-api.js";
70
+ import { isStopDirective } from "@open-everywhere/core/agent/adapter-api.js";
71
+ import { buildNotionAuth, notionConversationKey } from "./notion-identity.js";
72
+ import { richTextToPlainText, type NotionComment } from "./notion-api.js";
73
+ import type { NotionOwnBotIds } from "./notion-bot-user.js";
74
+
75
+ /**
76
+ * Is this FETCHED comment a genuine, human-authored stop directive? NEVER THROWS.
77
+ *
78
+ * FAIL CLOSED throughout — see this file's header for the five actor-guard arms. An unresolved
79
+ * PRIMARY id refuses EVERYTHING, not just the self-check: without our own identity there is no
80
+ * distinguishing "a human said stop" from "we said stop", and the safe answer is to not treat it as
81
+ * a stop at all (it still reaches the ordinary ambient path, which drops it for its own reasons).
82
+ */
83
+ export function notionCommentIsStopDirective(comment: NotionComment, own: NotionOwnBotIds): boolean {
84
+ if (!isStopDirective(richTextToPlainText(comment.rich_text))) return false;
85
+ if (!own.primary) return false; // can't rule out self — fail closed
86
+ const author = comment.created_by;
87
+ // OUR OWN WORDS, from ANY configured integration (OPE-1921) — never only the one that verified
88
+ // this delivery. A sibling integration's `stop` used to reach `cancelAnchoredTurn`.
89
+ if (author?.id !== undefined && own.all.has(author.id)) return false;
90
+ // 🔴 AN AUTHOR WE CANNOT IDENTIFY IS NOT A HUMAN (OPE-1906). The self-check above is the only arm
91
+ // that recognises our own words, and with no `created_by.id` it cannot run — the line below would
92
+ // then rest on `created_by.type`, which Notion OMITS on a comment. Without this, an unidentifiable
93
+ // author could CANCEL the page's running turn while the same author saying anything else is
94
+ // dropped by `classifyNotionComment` step 3. Same posture, one step apart.
95
+ if (!author?.id) return false; // unidentified author — fail closed
96
+ if (author.type && author.type !== "person") return false; // bot/integration-authored
97
+ // 🔴 A SIBLING WE COULD NOT RESOLVE IS A SELF-CHECK THAT DID NOT RUN (OPE-1921). With one
98
+ // integration's bot id missing, this author is either a person or that integration, and Notion's
99
+ // partial `created_by` cannot say which — so a self-cancel is back on the table. Refuse.
100
+ if (!own.complete && author.type !== "person") return false;
101
+ return true;
102
+ }
103
+
104
+ /** The facts a stop delivery carries, ALREADY RESOLVED by the webhook core's own steps (the
105
+ * authenticated page, the roster-aware workspace, the delivery's selected integration) — this file
106
+ * never re-derives any of them from the payload. */
107
+ export interface NotionStopComment {
108
+ readonly workspaceId?: string;
109
+ readonly pageId: string;
110
+ readonly discussionId?: string;
111
+ readonly credentialId?: string;
112
+ /** WHO asked (comment.created_by.id) — recorded on the audit row, never used to decide (the
113
+ * actor guard already ran in {@link notionCommentIsStopDirective}). */
114
+ readonly requesterId?: string;
115
+ }
116
+
117
+ /** Injected seams, so parse + guard + cancel is unit-tested without a live Notion integration. */
118
+ export interface NotionStopDeps {
119
+ readonly attachSession: AttachSessionForCancel;
120
+ /** Injectable for the test; defaults to the shared `cancelAnchoredTurn`. There is no second
121
+ * cancel implementation behind this — the seam exists to observe the call, never to replace it. */
122
+ readonly cancel?: typeof cancelAnchoredTurn;
123
+ }
124
+
125
+ /** Why the arm did what it did — for the caller's log, never for a person to read. */
126
+ export type NotionStopOutcome =
127
+ | { readonly stopped: false; readonly reason: string }
128
+ | { readonly stopped: true; readonly outcome: CancelOutcome };
129
+
130
+ /**
131
+ * Cancel the turn running on the page this stop directive was posted into.
132
+ *
133
+ * THE CONVERSATION KEY ONLY. A comment or discussion id is not the turn's anchor message — the
134
+ * anchor is the human's triggering message, which the registry keys on the PAGE (see this file's
135
+ * header) — so offering an `anchorKey` here would be a lookup that always misses.
136
+ */
137
+ export async function routeNotionStopComment(stop: NotionStopComment, deps: NotionStopDeps): Promise<NotionStopOutcome> {
138
+ const conversationKey = notionConversationKey(stop.workspaceId, stop.pageId);
139
+ const channelKey = conversationKey ? turnChannelKey("notion", conversationKey) : undefined;
140
+ if (!channelKey) return { stopped: false, reason: "stop: no conversation key" };
141
+ const partition = resolvePartition({
142
+ channel: { kind: "notion" },
143
+ session: {
144
+ auth: {
145
+ current: buildNotionAuth({
146
+ workspaceId: stop.workspaceId,
147
+ pageId: stop.pageId,
148
+ ...(stop.discussionId ? { discussionId: stop.discussionId } : {}),
149
+ ...(stop.credentialId ? { credentialId: stop.credentialId } : {}),
150
+ ...(stop.requesterId ? { principalId: stop.requesterId } : {}),
151
+ }),
152
+ },
153
+ },
154
+ });
155
+ const result = await (deps.cancel ?? cancelAnchoredTurn)({
156
+ org: partition.org,
157
+ platform: "notion",
158
+ workspace: partition.workspace,
159
+ ...(stop.requesterId ? { requestedBy: stop.requesterId } : {}),
160
+ // `reaction` names the AFFORDANCE class, not the vendor mechanism: a member pressed the stop
161
+ // control this surface offers. `command` is the slash-command door and `tool` is the agent's
162
+ // own verb — neither is what happened here. The audit row's job is "which door do people reach
163
+ // for", and a Notion stop comment is the same door as a Slack ✋.
164
+ via: "reaction",
165
+ query: { channelKey },
166
+ attachSession: deps.attachSession,
167
+ });
168
+ return { stopped: true, outcome: result.outcome };
169
+ }