@open-everywhere/provider-discord 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.
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +47 -0
- package/agent/access/discord-grant-click.ts +237 -0
- package/agent/approvals/discord-admin.ts +227 -0
- package/agent/approvals/discord-card.ts +248 -0
- package/agent/approvals/discord-flow.ts +469 -0
- package/agent/approvals/discord-scope-label.ts +27 -0
- package/agent/channels/discord-approval-card.ts +373 -0
- package/agent/channels/discord-backscroll.ts +593 -0
- package/agent/channels/discord-bot-name.ts +113 -0
- package/agent/channels/discord-channel-info.ts +83 -0
- package/agent/channels/discord-directory.ts +179 -0
- package/agent/channels/discord-dispatch-target.ts +103 -0
- package/agent/channels/discord-events.ts +175 -0
- package/agent/channels/discord-front.ts +355 -0
- package/agent/channels/discord-gateway.ts +3256 -0
- package/agent/channels/discord-identity.ts +269 -0
- package/agent/channels/discord-interaction-defer.ts +123 -0
- package/agent/channels/discord-message-ops.ts +127 -0
- package/agent/channels/discord-origin-codec.ts +105 -0
- package/agent/channels/discord-post-arm.ts +230 -0
- package/agent/channels/discord-post.ts +463 -0
- package/agent/channels/discord-read.ts +140 -0
- package/agent/channels/discord-rest.ts +85 -0
- package/agent/channels/discord.ts +244 -0
- package/agent/commands/discord-command-sync.ts +285 -0
- package/agent/commands/discord-commands.ts +291 -0
- package/agent/contributions.ts +31 -0
- package/agent/docs-generated.ts +15 -0
- package/agent/identity/discord-link-consent.ts +124 -0
- package/agent/ops.ts +14 -0
- package/agent/provider-events.ts +86 -0
- package/agent/register.ts +147 -0
- package/agent/surface-parity.ts +95 -0
- package/dist/agent/access/discord-grant-click.d.ts +25 -0
- package/dist/agent/access/discord-grant-click.d.ts.map +1 -0
- package/dist/agent/access/discord-grant-click.js +210 -0
- package/dist/agent/access/discord-grant-click.js.map +1 -0
- package/dist/agent/approvals/discord-admin.d.ts +63 -0
- package/dist/agent/approvals/discord-admin.d.ts.map +1 -0
- package/dist/agent/approvals/discord-admin.js +219 -0
- package/dist/agent/approvals/discord-admin.js.map +1 -0
- package/dist/agent/approvals/discord-card.d.ts +102 -0
- package/dist/agent/approvals/discord-card.d.ts.map +1 -0
- package/dist/agent/approvals/discord-card.js +217 -0
- package/dist/agent/approvals/discord-card.js.map +1 -0
- package/dist/agent/approvals/discord-flow.d.ts +109 -0
- package/dist/agent/approvals/discord-flow.d.ts.map +1 -0
- package/dist/agent/approvals/discord-flow.js +375 -0
- package/dist/agent/approvals/discord-flow.js.map +1 -0
- package/dist/agent/approvals/discord-scope-label.d.ts +3 -0
- package/dist/agent/approvals/discord-scope-label.d.ts.map +1 -0
- package/dist/agent/approvals/discord-scope-label.js +27 -0
- package/dist/agent/approvals/discord-scope-label.js.map +1 -0
- package/dist/agent/channels/discord-approval-card.d.ts +81 -0
- package/dist/agent/channels/discord-approval-card.d.ts.map +1 -0
- package/dist/agent/channels/discord-approval-card.js +303 -0
- package/dist/agent/channels/discord-approval-card.js.map +1 -0
- package/dist/agent/channels/discord-backscroll.d.ts +209 -0
- package/dist/agent/channels/discord-backscroll.d.ts.map +1 -0
- package/dist/agent/channels/discord-backscroll.js +505 -0
- package/dist/agent/channels/discord-backscroll.js.map +1 -0
- package/dist/agent/channels/discord-bot-name.d.ts +19 -0
- package/dist/agent/channels/discord-bot-name.d.ts.map +1 -0
- package/dist/agent/channels/discord-bot-name.js +98 -0
- package/dist/agent/channels/discord-bot-name.js.map +1 -0
- package/dist/agent/channels/discord-channel-info.d.ts +22 -0
- package/dist/agent/channels/discord-channel-info.d.ts.map +1 -0
- package/dist/agent/channels/discord-channel-info.js +65 -0
- package/dist/agent/channels/discord-channel-info.js.map +1 -0
- package/dist/agent/channels/discord-directory.d.ts +11 -0
- package/dist/agent/channels/discord-directory.d.ts.map +1 -0
- package/dist/agent/channels/discord-directory.js +170 -0
- package/dist/agent/channels/discord-directory.js.map +1 -0
- package/dist/agent/channels/discord-dispatch-target.d.ts +15 -0
- package/dist/agent/channels/discord-dispatch-target.d.ts.map +1 -0
- package/dist/agent/channels/discord-dispatch-target.js +99 -0
- package/dist/agent/channels/discord-dispatch-target.js.map +1 -0
- package/dist/agent/channels/discord-events.d.ts +58 -0
- package/dist/agent/channels/discord-events.d.ts.map +1 -0
- package/dist/agent/channels/discord-events.js +150 -0
- package/dist/agent/channels/discord-events.js.map +1 -0
- package/dist/agent/channels/discord-front.d.ts +115 -0
- package/dist/agent/channels/discord-front.d.ts.map +1 -0
- package/dist/agent/channels/discord-front.js +279 -0
- package/dist/agent/channels/discord-front.js.map +1 -0
- package/dist/agent/channels/discord-gateway.d.ts +783 -0
- package/dist/agent/channels/discord-gateway.d.ts.map +1 -0
- package/dist/agent/channels/discord-gateway.js +2591 -0
- package/dist/agent/channels/discord-gateway.js.map +1 -0
- package/dist/agent/channels/discord-identity.d.ts +110 -0
- package/dist/agent/channels/discord-identity.d.ts.map +1 -0
- package/dist/agent/channels/discord-identity.js +201 -0
- package/dist/agent/channels/discord-identity.js.map +1 -0
- package/dist/agent/channels/discord-interaction-defer.d.ts +33 -0
- package/dist/agent/channels/discord-interaction-defer.d.ts.map +1 -0
- package/dist/agent/channels/discord-interaction-defer.js +102 -0
- package/dist/agent/channels/discord-interaction-defer.js.map +1 -0
- package/dist/agent/channels/discord-message-ops.d.ts +3 -0
- package/dist/agent/channels/discord-message-ops.d.ts.map +1 -0
- package/dist/agent/channels/discord-message-ops.js +123 -0
- package/dist/agent/channels/discord-message-ops.js.map +1 -0
- package/dist/agent/channels/discord-origin-codec.d.ts +7 -0
- package/dist/agent/channels/discord-origin-codec.d.ts.map +1 -0
- package/dist/agent/channels/discord-origin-codec.js +98 -0
- package/dist/agent/channels/discord-origin-codec.js.map +1 -0
- package/dist/agent/channels/discord-post-arm.d.ts +3 -0
- package/dist/agent/channels/discord-post-arm.d.ts.map +1 -0
- package/dist/agent/channels/discord-post-arm.js +195 -0
- package/dist/agent/channels/discord-post-arm.js.map +1 -0
- package/dist/agent/channels/discord-post.d.ts +120 -0
- package/dist/agent/channels/discord-post.d.ts.map +1 -0
- package/dist/agent/channels/discord-post.js +426 -0
- package/dist/agent/channels/discord-post.js.map +1 -0
- package/dist/agent/channels/discord-read.d.ts +40 -0
- package/dist/agent/channels/discord-read.d.ts.map +1 -0
- package/dist/agent/channels/discord-read.js +128 -0
- package/dist/agent/channels/discord-read.js.map +1 -0
- package/dist/agent/channels/discord-rest.d.ts +33 -0
- package/dist/agent/channels/discord-rest.d.ts.map +1 -0
- package/dist/agent/channels/discord-rest.js +75 -0
- package/dist/agent/channels/discord-rest.js.map +1 -0
- package/dist/agent/channels/discord.d.ts +3 -0
- package/dist/agent/channels/discord.d.ts.map +1 -0
- package/dist/agent/channels/discord.js +230 -0
- package/dist/agent/channels/discord.js.map +1 -0
- package/dist/agent/commands/discord-command-sync.d.ts +66 -0
- package/dist/agent/commands/discord-command-sync.d.ts.map +1 -0
- package/dist/agent/commands/discord-command-sync.js +224 -0
- package/dist/agent/commands/discord-command-sync.js.map +1 -0
- package/dist/agent/commands/discord-commands.d.ts +45 -0
- package/dist/agent/commands/discord-commands.d.ts.map +1 -0
- package/dist/agent/commands/discord-commands.js +266 -0
- package/dist/agent/commands/discord-commands.js.map +1 -0
- package/dist/agent/contributions.d.ts +17 -0
- package/dist/agent/contributions.d.ts.map +1 -0
- package/dist/agent/contributions.js +31 -0
- package/dist/agent/contributions.js.map +1 -0
- package/dist/agent/docs-generated.d.ts +3 -0
- package/dist/agent/docs-generated.d.ts.map +1 -0
- package/dist/agent/docs-generated.js +13 -0
- package/dist/agent/docs-generated.js.map +1 -0
- package/dist/agent/identity/discord-link-consent.d.ts +13 -0
- package/dist/agent/identity/discord-link-consent.d.ts.map +1 -0
- package/dist/agent/identity/discord-link-consent.js +111 -0
- package/dist/agent/identity/discord-link-consent.js.map +1 -0
- package/dist/agent/ops.d.ts +2 -0
- package/dist/agent/ops.d.ts.map +1 -0
- package/dist/agent/ops.js +13 -0
- package/dist/agent/ops.js.map +1 -0
- package/dist/agent/provider-events.d.ts +3 -0
- package/dist/agent/provider-events.d.ts.map +1 -0
- package/dist/agent/provider-events.js +78 -0
- package/dist/agent/provider-events.js.map +1 -0
- package/dist/agent/register.d.ts +5 -0
- package/dist/agent/register.d.ts.map +1 -0
- package/dist/agent/register.js +125 -0
- package/dist/agent/register.js.map +1 -0
- package/dist/agent/surface-parity.d.ts +3 -0
- package/dist/agent/surface-parity.d.ts.map +1 -0
- package/dist/agent/surface-parity.js +90 -0
- package/dist/agent/surface-parity.js.map +1 -0
- package/package.json +185 -0
- package/tsconfig.build.json +22 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,783 @@
|
|
|
1
|
+
import { type RouteHandlerArgs } from "eve/channels";
|
|
2
|
+
import { type OpenEverywhereDiscordAuth } from "./discord-identity.js";
|
|
3
|
+
import { splitForDiscord } from "./discord-post.js";
|
|
4
|
+
import { type DiscordFetchDeps } from "./discord-rest.js";
|
|
5
|
+
import { type AttachSessionForCancel, contextLineIngestion, type IngestedChatMessage, type ExternalPolicy, type AuditInput, type FinalMessageAction, type NormalizedWake, type WakeEngagementTier, type WakeTrust, type AuthorTrustQuery, type SupervisorGrain, type SingleWindowMentionPolicy, type Partition, type ContextLine, type WakeMessageInput } from "@open-everywhere/core/agent/adapter-api.js";
|
|
6
|
+
export { splitForDiscord };
|
|
7
|
+
import { type ReactionSinkDeps, type ReactionIngestOutcome, type ReactionVerb } from "@open-everywhere/core/agent/adapter-api.js";
|
|
8
|
+
export interface DiscordGatewayState {
|
|
9
|
+
channelId: string;
|
|
10
|
+
guildId?: string;
|
|
11
|
+
originalChannelId?: string;
|
|
12
|
+
originalMessageId?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface DiscordRelayMessage {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly content: string;
|
|
17
|
+
readonly channelId: string;
|
|
18
|
+
/** Discord's numeric channel `type` as a STRING (relay sends the enum number). */
|
|
19
|
+
readonly channelType?: string;
|
|
20
|
+
readonly guildId?: string;
|
|
21
|
+
readonly author: {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly username?: string;
|
|
24
|
+
readonly bot: boolean;
|
|
25
|
+
};
|
|
26
|
+
readonly mentionsBot: boolean;
|
|
27
|
+
readonly referencedMessageId?: string;
|
|
28
|
+
/** The bot's own display name AS SEEN IN THIS GUILD (nickname, else global name) β
|
|
29
|
+
* what a human actually types to address it. Sent by relay β₯ 5c67655; absent from an
|
|
30
|
+
* older relay, which simply keeps the wake gate mention/engagement-only (fail-closed). */
|
|
31
|
+
readonly botName?: string;
|
|
32
|
+
/** The receiving bot's user id β multi-bot attribution (item 27): when N relay clients
|
|
33
|
+
* share one process, each forwards every message, and botId is how dedupe and persona
|
|
34
|
+
* routing tell the copies apart. Same relay payload as botName; optional for old relays. */
|
|
35
|
+
readonly botId?: string;
|
|
36
|
+
/** Whether the message author is a PENDING guild member (membership screening incomplete) β
|
|
37
|
+
* the OPE-60 guest/unverified demotion signal. Discord stamps `member.pending` on gateway
|
|
38
|
+
* member objects; forwarded by a relay that includes it (absent from older relays β the
|
|
39
|
+
* binding keeps today's fail-closed/vouch behaviour). Undefined for DMs (no guild member). */
|
|
40
|
+
readonly memberPending?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The guild ROLE ids this author holds (Discord stamps `member.roles` on the gateway member
|
|
43
|
+
* object β the SAME object `member.pending` rides on, so this costs no extra API call). The input
|
|
44
|
+
* to the author-trust register's membership patterns (OPE-516): a
|
|
45
|
+
* `{ membership: "discord/<guildId>/role:<roleId>" }` declaration matches an author whose roles
|
|
46
|
+
* include that id.
|
|
47
|
+
*
|
|
48
|
+
* Absent for a DM (no guild member) and from an older relay that omits the member object β both
|
|
49
|
+
* simply mean no membership evidence, which the register leg treats as "matched nothing".
|
|
50
|
+
*/
|
|
51
|
+
readonly memberRoles?: readonly string[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Defensively parse the relay envelope. Returns null for anything not recognizably a relay
|
|
55
|
+
* MESSAGE_CREATE with the identity facts we MUST have to dispatch + reply β a channel id (for
|
|
56
|
+
* the post-back + partition) and an author id (for identity). `content` may legitimately be
|
|
57
|
+
* empty (Message Content Intent disabled surfaces empty strings), so it isn't required here;
|
|
58
|
+
* the route drops empty-content turns separately. NEVER throws.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseRelayPayload(raw: unknown): DiscordRelayMessage | null;
|
|
61
|
+
export interface DiscordRelayReaction {
|
|
62
|
+
readonly verb: ReactionVerb;
|
|
63
|
+
/** The reacted message's id + channel. */
|
|
64
|
+
readonly messageId: string;
|
|
65
|
+
readonly channelId: string;
|
|
66
|
+
readonly guildId?: string;
|
|
67
|
+
readonly channelType?: string;
|
|
68
|
+
/** The emoji: a unicode char (π) or a custom emoji NAME. Bucketed by classifyEmoji. */
|
|
69
|
+
readonly emoji: string;
|
|
70
|
+
readonly reactorId: string;
|
|
71
|
+
/** The AUTHOR of the reacted message, when the relay could resolve it β drives onBotReply. */
|
|
72
|
+
readonly messageAuthorId?: string;
|
|
73
|
+
/** The receiving bot's own user id (relay-supplied) β onBotReply compares against it. */
|
|
74
|
+
readonly botId?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Defensively parse a relay REACTION envelope, or null when it isn't one (wrong source/event,
|
|
78
|
+
* or missing the ids ingestion needs). NEVER throws. Distinguished from a message payload by
|
|
79
|
+
* the `event` discriminator + a `reaction` object (vs `message`).
|
|
80
|
+
*/
|
|
81
|
+
export declare function parseRelayReaction(raw: unknown): DiscordRelayReaction | null;
|
|
82
|
+
export interface DiscordRelayHeartbeat {
|
|
83
|
+
/** Identifies the relay PROCESS (its own bot user id) β never a guild/channel/user fact. */
|
|
84
|
+
readonly relayId: string;
|
|
85
|
+
/** The relay's own clock, epoch milliseconds, at the moment it sent this beat. */
|
|
86
|
+
readonly ts: number;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Defensively parse a relay HEARTBEAT envelope, or null when it isn't one (wrong source/event, or
|
|
90
|
+
* missing a field). NEVER throws. Distinguished from a message/reaction envelope by the `event`
|
|
91
|
+
* discriminator + a `heartbeat` object (vs `message`/`reaction`) β mutually exclusive with both,
|
|
92
|
+
* pinned by scripts/discord-gateway-test.ts.
|
|
93
|
+
*/
|
|
94
|
+
export declare function parseRelayHeartbeat(raw: unknown): DiscordRelayHeartbeat | null;
|
|
95
|
+
/** The audit row a heartbeat writes. Pure β no I/O β so the shape is testable without a live
|
|
96
|
+
* store; the route below is the only caller that actually persists it via `appendAudit`. The
|
|
97
|
+
* action name (`discord.heartbeat`) is what `agent/ops/census.ts`'s `CHANNEL_REPORTED_HEARTBEATS`
|
|
98
|
+
* reads by. `workspace`/`surface` name the RELAY, never a guild β this signal is guild-agnostic
|
|
99
|
+
* by design, which is exactly what lets it separate a dead relay from a quiet one. */
|
|
100
|
+
export declare function discordHeartbeatAuditRow(heartbeat: DiscordRelayHeartbeat, org: string): AuditInput;
|
|
101
|
+
/** Dependencies for {@link routeDiscordReaction} β injected so parse+route+record is unit-tested
|
|
102
|
+
* without a live store. `attachSession` is eve's cancel door, present ONLY when the route was
|
|
103
|
+
* built with it (the β stop arm) β absent β a β is ingested as an ordinary reaction. */
|
|
104
|
+
export interface DiscordReactionRouteDeps {
|
|
105
|
+
readonly sink: ReactionSinkDeps;
|
|
106
|
+
readonly attachSession?: AttachSessionForCancel;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Route one parsed Discord reaction into the surface-neutral ingestion seam. The relay already
|
|
110
|
+
* dropped bot reactors, but we double-check against the relayed botId (defence in depth) so the
|
|
111
|
+
* bot's own reaction is never recorded as feedback. onBotReply = the reacted message was authored
|
|
112
|
+
* by the bot; fail-safe when either id is missing (telemetry only, never a false feedback row).
|
|
113
|
+
*/
|
|
114
|
+
export declare function routeDiscordReaction(rx: DiscordRelayReaction, deps: DiscordReactionRouteDeps): Promise<ReactionIngestOutcome>;
|
|
115
|
+
export interface RelayRejection {
|
|
116
|
+
readonly status: number;
|
|
117
|
+
readonly reason: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Gate one relay delivery. Returns null when it may proceed, else the {@link RelayRejection}.
|
|
121
|
+
* `secret` defaults to the live env read but is injectable so the gate is unit-tested without
|
|
122
|
+
* touching process.env. Fail-closed: unset/empty secret β 401; missing/malformed/mismatched
|
|
123
|
+
* signature β 401 (verifyWebhookSignature returns false for a null or non-`sha256=` header).
|
|
124
|
+
*/
|
|
125
|
+
export declare function relayGate(signatureHeader: string | null, rawBody: string, secret?: string | undefined): RelayRejection | null;
|
|
126
|
+
/**
|
|
127
|
+
* Reconstruct the dispatch auth from a relay message, or null to DROP. Reuses
|
|
128
|
+
* buildDiscordAuth verbatim (channels/discord.ts uses the same for the interaction path) so
|
|
129
|
+
* the identity seam (identity/binding.ts bindDiscord) resolves the real guild/channel/user β
|
|
130
|
+
* workspace/surface/mode/visibility. Drops bot authors: we never act on another bot (incl.
|
|
131
|
+
* ourselves) β mirrors discord.ts's authFromInteraction and the relay's own bot filter.
|
|
132
|
+
*/
|
|
133
|
+
export declare function authForRelayMessage(msg: DiscordRelayMessage): OpenEverywhereDiscordAuth | null;
|
|
134
|
+
/**
|
|
135
|
+
* Is this bot-authored relay message's author id a KNOWN e2e-driver id (OPE-647) β pinned to
|
|
136
|
+
* SOME guild in `OE_TEST_DRIVER_IDS` β but NOT to this message's own guild? `authForRelayMessage`
|
|
137
|
+
* already refuses the message either way: `isAllowedDriver` is guild-pinned by format
|
|
138
|
+
* (`guildId:botUserId`), so a driver id posting outside its pinned guild never passes. This
|
|
139
|
+
* classifier exists only to tell that refusal APART from routine "some unrelated real bot
|
|
140
|
+
* posted" noise, so a driver credential surfacing in the wrong guild gets a NAMED audit row
|
|
141
|
+
* instead of vanishing into the ordinary bot-drop silence. Pure β no I/O β so every branch is
|
|
142
|
+
* unit-tested against a hand-built allowlist, with no live Discord and no store.
|
|
143
|
+
*/
|
|
144
|
+
export declare function isMisplacedDriver(guildId: string | undefined, botUserId: string, allowlist?: ReadonlySet<string>): boolean;
|
|
145
|
+
/**
|
|
146
|
+
* The AUTHOR-TRUST question for this message (OPE-516) β the operator's own declaration of who is
|
|
147
|
+
* internal in this guild, which is the ONLY author-grain evidence Discord can produce. Discord has
|
|
148
|
+
* no organisation concept, so `discordTrust` can never refute an author; `member.pending` is a
|
|
149
|
+
* SCREENING verdict, not a membership one. The register fills that hole from reviewed config.
|
|
150
|
+
*
|
|
151
|
+
* Returns `{}` for a DM: a DM has no guild, so there is no tenant to arm and no roles to hold. The
|
|
152
|
+
* DM's trust question is about the PARTNER and is answered by the surface register instead β the
|
|
153
|
+
* same split `ChatWakeRoom.dm` makes on the room leg. A spread-able fragment rather than a
|
|
154
|
+
* `| undefined` value so the call sites stay one expression and an absent author is byte-identical
|
|
155
|
+
* to before this existed.
|
|
156
|
+
*
|
|
157
|
+
* The tenant key is built from the guild id and NOTHING ELSE. The platform and tenant halves of a
|
|
158
|
+
* principal key are surface facts, never properties of the id (identity/principal-key.ts) β and a
|
|
159
|
+
* snowflake is a bare digit string that would collide happily across vendors if it were sniffed.
|
|
160
|
+
*/
|
|
161
|
+
export declare function authorFacts(msg: DiscordRelayMessage): {
|
|
162
|
+
readonly authorTrust?: AuthorTrustQuery;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* THE EXTERNAL-PARTICIPANT VERDICT for one Discord message (OPE-507 / OPE-504) β pure, so the four
|
|
166
|
+
* outcomes are tested without a REST call, a store or a live relay. `dispatchTurn` calls it once and
|
|
167
|
+
* switches on the answer.
|
|
168
|
+
*
|
|
169
|
+
* The four values name WHAT THE CALLER OWES, which is the only thing that differs between them:
|
|
170
|
+
* `proceed` β nothing suppressed. The shipped grains always return this.
|
|
171
|
+
* `withheld-addressed` β an excluded author @mentioned or DM'd. Owed a PRIVATE notice, nothing else.
|
|
172
|
+
* `withheld-ambient` β an excluded author spoke ambiently. Owed NOTHING; suppress in silence.
|
|
173
|
+
* `silent-room` β the room is classified external and the operator asked for silence. Owed
|
|
174
|
+
* the once-ever suppressed-wake announcement.
|
|
175
|
+
*
|
|
176
|
+
* π΄ THE AUTHOR GRAIN IS TESTED FIRST, AND THE ORDER MATTERS. An excluded author in a silenced room
|
|
177
|
+
* must get the AUTHOR outcome: they are owed a notice they can act on ("ask someone inside"), where
|
|
178
|
+
* the room announcement tells the whole room something about the room. Reversing this would answer
|
|
179
|
+
* the wrong question to the wrong audience.
|
|
180
|
+
*/
|
|
181
|
+
export type DiscordExternalVerdict = "proceed" | "withheld-addressed" | "withheld-ambient" | "silent-room";
|
|
182
|
+
export declare function discordExternalVerdict(input: {
|
|
183
|
+
readonly policy: ExternalPolicy;
|
|
184
|
+
readonly author: IngestedChatMessage;
|
|
185
|
+
/** The message @mentions the bot, or arrived in a DM β an ADDRESSED message is owed an answer. */
|
|
186
|
+
readonly addressed: boolean;
|
|
187
|
+
readonly isDm: boolean;
|
|
188
|
+
/** The DOMAIN-grain surface key (a thread asks its parent channel), for the room classification. */
|
|
189
|
+
readonly roomKey: string;
|
|
190
|
+
}): DiscordExternalVerdict;
|
|
191
|
+
/**
|
|
192
|
+
* Is this channel type a THREAD? Discord 10 (announcement thread) / 11 (public thread) /
|
|
193
|
+
* 12 (private thread) β the surfaces whose channel id is NOT the id domains and spend
|
|
194
|
+
* caps are declared against (those name the PARENT channel). Pure; drives the
|
|
195
|
+
* parent-grain stamping in dispatchTurn (item 22).
|
|
196
|
+
*/
|
|
197
|
+
export declare function isThreadChannelType(channelType: string | undefined): boolean;
|
|
198
|
+
/**
|
|
199
|
+
* Is this message a direct message to the bot? Discord channel type 1 (DM) / 3 (group DM).
|
|
200
|
+
* A DM is BY DEFINITION addressed to the bot β there is no one else it could be for β so it
|
|
201
|
+
* wakes without a mention and its text is a direct instruction (no judged preamble), exactly
|
|
202
|
+
* like a Slack DM. Pure.
|
|
203
|
+
*/
|
|
204
|
+
export declare function isDmChannel(channelType: string | undefined): boolean;
|
|
205
|
+
/**
|
|
206
|
+
* Should this ambient message wake the agent? A DM always wakes (it's inherently addressed to
|
|
207
|
+
* the bot). An @mention is a direct address (guaranteed reply, like a Slack mention); otherwise
|
|
208
|
+
* the channel must already be ENGAGED β the agent was pulled in earlier and keeps following
|
|
209
|
+
* without re-tagging (engagement.ts, the same registry Slack uses). A non-mention message in a
|
|
210
|
+
* non-engaged guild channel is dropped, so the bot stays quiet in channels it wasn't invited
|
|
211
|
+
* into. Pure β unit-tested without the operational store.
|
|
212
|
+
*/
|
|
213
|
+
export declare function shouldDispatchDiscord(input: {
|
|
214
|
+
readonly mentionsBot: boolean;
|
|
215
|
+
readonly isEngaged: boolean;
|
|
216
|
+
readonly isDm: boolean;
|
|
217
|
+
}): boolean;
|
|
218
|
+
/**
|
|
219
|
+
* Is this message a NAME-DROP candidate β Slack's nameDropWake brought to Discord (item 25;
|
|
220
|
+
* live 2026-07-06 Tommy addressed the bot by display name and it only "worked" where the
|
|
221
|
+
* channel happened to be engaged)? Pure pre-filter, cheapest checks first; the caller still
|
|
222
|
+
* gates on the domain's `nameDropWake` flag (config, evaluated at DOMAIN grain β a thread
|
|
223
|
+
* checks its parent channel's domain). Disqualifiers:
|
|
224
|
+
* - mention / DM / engaged β those wake paths own the message already;
|
|
225
|
+
* - a bot author β never self/bot-trigger;
|
|
226
|
+
* - anything but a guild conversation surface (text 0, announcement 5, threads 10/11/12).
|
|
227
|
+
* DELIBERATE Slack DEVIATION: Slack excludes thread replies ("the engagement path owns
|
|
228
|
+
* threads") but on Discord threads ARE the primary conversation surface and an
|
|
229
|
+
* UN-engaged thread (TTL'd out, or human-created) that says the bot's name is a clear
|
|
230
|
+
* address with no other wake path β the exact live gap. Forum containers (15) and
|
|
231
|
+
* unknown types stay excluded, fail-safe;
|
|
232
|
+
* - no relayed bot name (older relay) β fail-closed to mention/engagement-only, so
|
|
233
|
+
* `identity.name`/`wake.nameDrop.include` alone never fires this without a live name too.
|
|
234
|
+
* Unlike Slack there is NO externally-shared-channel probe: Discord has no shared-channel
|
|
235
|
+
* concept β guild membership IS the boundary, so a guild surface is internal by construction.
|
|
236
|
+
* The text is matched against the UNIFIED wake token set (OPE-571): `input.botName` (the live
|
|
237
|
+
* Discord display name) UNION `identity.name`, extended by `wake.nameDrop.include` and narrowed
|
|
238
|
+
* by `wake.nameDrop.exclude` β see {@link wakeNameDropTokens}, the same builder Slack's
|
|
239
|
+
* `buildWakeTriggers` uses, so the two surfaces' wake set stays one rule.
|
|
240
|
+
*/
|
|
241
|
+
export declare function nameDropEligible(input: {
|
|
242
|
+
readonly mentionsBot: boolean;
|
|
243
|
+
readonly isDm: boolean;
|
|
244
|
+
readonly isEngaged: boolean;
|
|
245
|
+
readonly authorIsBot: boolean;
|
|
246
|
+
readonly channelType?: string;
|
|
247
|
+
readonly content: string;
|
|
248
|
+
readonly botName?: string;
|
|
249
|
+
}): boolean;
|
|
250
|
+
/**
|
|
251
|
+
* Low-level create-message: `POST /channels/{id}/messages` with the bot token. Returns the NEW
|
|
252
|
+
* message's id on success (needed by the holding-message flow so completion can edit it), or null
|
|
253
|
+
* on any failure β no token, non-ok status, network error, or a response missing an id. NEVER
|
|
254
|
+
* throws. postDiscordReply + postHoldingMessage are both built on this so the auth/error handling
|
|
255
|
+
* lives in one place (the bot token is a SECRET β never logged).
|
|
256
|
+
*/
|
|
257
|
+
export declare function discordCreateMessage(channelId: string, content: string, deps?: DiscordFetchDeps): Promise<string | null>;
|
|
258
|
+
/**
|
|
259
|
+
* Post the agent's reply to a Discord channel via REST. Best-effort per chunk: a failed chunk is
|
|
260
|
+
* logged (with status, never the token) and the rest still attempt. No DISCORD_BOT_TOKEN β each
|
|
261
|
+
* chunk logs and no-ops (the turn already ran; silence is the only degradation). This is the plain
|
|
262
|
+
* post-back used when status UX is off, and the fail-open fallback when the holding-message flow
|
|
263
|
+
* couldn't run.
|
|
264
|
+
*/
|
|
265
|
+
/**
|
|
266
|
+
* Open (or re-open) the 1:1 DM channel with one user and return its id β `POST /users/@me/channels`.
|
|
267
|
+
* Idempotent on Discord's side: it returns the EXISTING DM channel when one is already open, so
|
|
268
|
+
* calling it repeatedly creates nothing.
|
|
269
|
+
*
|
|
270
|
+
* It exists for ONE caller: the external-sender notice (OPE-507 behaviour 1), which owes the sender a
|
|
271
|
+
* PRIVATE message. Fail-soft to `null` β see {@link postExternalSenderNotice} for why a failure here
|
|
272
|
+
* must stay silent rather than fall back to the channel.
|
|
273
|
+
*/
|
|
274
|
+
export declare function openDiscordDm(userId: string, deps?: DiscordFetchDeps): Promise<string | null>;
|
|
275
|
+
/** Test hook: drop every recorded throttle timestamp. Production correctness needs this
|
|
276
|
+
* map to live for the whole process (the throttle is meant to span turns), but the
|
|
277
|
+
* golden-transcript scenario harness (discord-scenario-harness.ts) runs many scenarios in ONE
|
|
278
|
+
* process and reuses small deterministic channel/thread ids ("TH1", β¦) across them β so
|
|
279
|
+
* without a reset, whether a later scenario's typing throttle "holds" depends on the
|
|
280
|
+
* REAL wall-clock gap since an earlier, unrelated scenario last wrote the same key: fast
|
|
281
|
+
* test execution (<7s between scenarios) silently suppresses a typing re-trigger the
|
|
282
|
+
* golden doesn't expect to be suppressed; a slow/loaded run (>7s) doesn't, and the extra
|
|
283
|
+
* POST diverges the golden β a genuine flake pinned as OPE-69 (confirmed by forcing an
|
|
284
|
+
* 8s real delay between scenarios 01/02, which flipped the throttle and diverged
|
|
285
|
+
* 02-thread-followup-resume). Called by ScenarioRunner.scenario() before every scenario
|
|
286
|
+
* so each starts cold, exactly like clearDiscordInfoCacheForTest(). */
|
|
287
|
+
export declare function clearTypingThrottleForTest(): void;
|
|
288
|
+
/** True when a typing re-trigger is due for this channel (and records it as fired). Pure
|
|
289
|
+
* given an injected clock β unit-tested; the default clock is Date.now. */
|
|
290
|
+
export declare function typingRefreshDue(channelId: string, nowMs?: number, state?: Map<string, number>): boolean;
|
|
291
|
+
/** Close the gate for a judged wake: hold all typing until the reply is proven. */
|
|
292
|
+
export declare function closeTypingGate(channelId: string, gates?: Set<string>): void;
|
|
293
|
+
/** Open (clear) the gate β addressed dispatch, reply proven, or turn over. */
|
|
294
|
+
export declare function openTypingGate(channelId: string, gates?: Set<string>): void;
|
|
295
|
+
export declare function typingGateOpen(channelId: string, gates?: Set<string>): boolean;
|
|
296
|
+
/**
|
|
297
|
+
* Feed the streamed assistant text into the gate; returns whether typing may fire. Mirrors
|
|
298
|
+
* slack-events.ts isNoReply EXACTLY (trimStart + case-sensitive startsWith): while the text
|
|
299
|
+
* is still a prefix of the sentinel the verdict is UNDECIDED (hold); once it starts with the
|
|
300
|
+
* full sentinel it IS a judged silence (hold β the final reply will be swallowed); any
|
|
301
|
+
* deviation proves a real reply, so the gate opens and stays open for the rest of the turn.
|
|
302
|
+
*/
|
|
303
|
+
export declare function typingGateFeed(channelId: string, messageSoFar: string, gates?: Set<string>): boolean;
|
|
304
|
+
export declare function triggerDiscordTyping(channelId: string, deps?: DiscordFetchDeps): Promise<void>;
|
|
305
|
+
/**
|
|
306
|
+
* Add or remove the bot's own reaction on a message: `{PUT|DELETE}
|
|
307
|
+
* /channels/{id}/messages/{messageId}/reactions/{emoji}/@me` with the emoji URL-encoded. Used to
|
|
308
|
+
* show π the moment we decide to respond and swap to β
(or just clear π) on completion. Purely a
|
|
309
|
+
* status affordance β best-effort, any failure is logged and swallowed, NEVER throws, and a missing
|
|
310
|
+
* token no-ops. (The bot token is a SECRET β never logged.)
|
|
311
|
+
*/
|
|
312
|
+
export declare function setDiscordReaction(channelId: string, messageId: string, emoji: string, action: "PUT" | "DELETE", deps?: DiscordFetchDeps): Promise<void>;
|
|
313
|
+
/** Strip ONE leading bot-mention token (`<@123>` / `<@!123>`) β shared by thread naming and
|
|
314
|
+
* the stop-verb match, both of which care about the words AFTER the address. Pure. */
|
|
315
|
+
export declare function stripLeadingMention(content: string): string;
|
|
316
|
+
/**
|
|
317
|
+
* Should a mention here open a new thread? True ONLY for a base guild text (0) or announcement
|
|
318
|
+
* (5) channel when the toggle is on. False for threads (10/11/12 β already a thread surface),
|
|
319
|
+
* DMs / group DMs (1/3 β no threads), forum channels (15 β a forum post is itself a thread;
|
|
320
|
+
* don't nest), and any unknown type (fail-safe: keep today's inline behaviour). Pure.
|
|
321
|
+
*/
|
|
322
|
+
export declare function shouldAutoThread(channelType: string | undefined, enabled: boolean): boolean;
|
|
323
|
+
/**
|
|
324
|
+
* Read the discord.autoThread toggle. Default ON β auto-thread is the on-brand behaviour;
|
|
325
|
+
* a deployer opts OUT of it. Only the explicit "false"/"0" disable; anything else (unset,
|
|
326
|
+
* "true", "1", β¦) leaves it on. `env` is injectable so the toggle is unit-tested without
|
|
327
|
+
* touching process.env. Pure.
|
|
328
|
+
*/
|
|
329
|
+
export declare function autoThreadEnabled(env?: string): boolean;
|
|
330
|
+
/**
|
|
331
|
+
* Read the discord.statusUx toggle. Default ON β the "working" affordances (π/β
reaction + a
|
|
332
|
+
* holding message edited into the reply) are on-brand, so a deployer opts OUT. Only the explicit
|
|
333
|
+
* "false"/"0" disable; anything else (unset, "true", "1", β¦) leaves it on. When off, the surface
|
|
334
|
+
* behaves EXACTLY as before this feature: one plain reply post, no reaction, no holding message.
|
|
335
|
+
* `env` is injectable so the toggle is unit-tested without touching process.env. Pure. Mirrors
|
|
336
|
+
* autoThreadEnabled verbatim.
|
|
337
|
+
*/
|
|
338
|
+
export declare function statusUxEnabled(env?: string): boolean;
|
|
339
|
+
/**
|
|
340
|
+
* Derive a Discord thread name from the triggering message. Strips a single leading bot
|
|
341
|
+
* mention token (`<@123>` or `<@!123>`), collapses whitespace, and truncates to Discord's
|
|
342
|
+
* 100-char cap (trimming back to a word boundary when one sits reasonably near the cut).
|
|
343
|
+
* Falls back to `Conversation with <botName>` when nothing usable remains (e.g. a bare
|
|
344
|
+
* mention with no text). Pure + deterministic β no model call (v1; a cheap-model title is a
|
|
345
|
+
* noted later enhancement).
|
|
346
|
+
*/
|
|
347
|
+
export declare function deriveThreadName(content: string, botName?: string): string;
|
|
348
|
+
/**
|
|
349
|
+
* Create a Discord thread hanging off the triggering message:
|
|
350
|
+
* `POST /channels/{channelId}/messages/{messageId}/threads` with the bot token. Returns the
|
|
351
|
+
* new thread's channel id on success, or null on ANY failure (no token, non-ok status,
|
|
352
|
+
* network error, or a response missing an id). NEVER throws β threading is fail-OPEN: a null
|
|
353
|
+
* here degrades the turn to an inline reply, it never drops the turn. Mirrors postDiscordReply's
|
|
354
|
+
* auth + error handling exactly (including: the bot token is a SECRET β never logged).
|
|
355
|
+
*/
|
|
356
|
+
export declare function createDiscordThread(channelId: string, messageId: string, name: string, deps?: DiscordFetchDeps): Promise<string | null>;
|
|
357
|
+
/**
|
|
358
|
+
* Compute the dispatch surface for a turn, given the thread-create outcome. This is the
|
|
359
|
+
* RE-KEY: when a thread was created (threadId non-null), the ENTIRE turn moves onto the thread
|
|
360
|
+
* channel id β identity/auth, continuation token, reply-target state, and engagement key all
|
|
361
|
+
* point at the thread, so the conversation lives there (its own memory silo + durable session)
|
|
362
|
+
* and the base channel is only where the mention landed. When no thread was created (threadId
|
|
363
|
+
* null β either not auto-threadable or a fail-open fallback), the turn stays on the base
|
|
364
|
+
* channel exactly as today. Pure β the fail-open contract is unit-tested by passing an id / null
|
|
365
|
+
* without any live Discord call.
|
|
366
|
+
*/
|
|
367
|
+
export declare function dispatchSurfaceFor(msg: DiscordRelayMessage, baseAuth: OpenEverywhereDiscordAuth, threadId: string | null): {
|
|
368
|
+
auth: OpenEverywhereDiscordAuth;
|
|
369
|
+
continuationToken: string;
|
|
370
|
+
state: DiscordGatewayState;
|
|
371
|
+
engagementKey: string;
|
|
372
|
+
};
|
|
373
|
+
/** How to reconcile the π reaction on completion (auto-threaded mentions only). */
|
|
374
|
+
export type ReactionResolution = "clear" | "none";
|
|
375
|
+
/** How to deliver the final reply. Always a FRESH post β never an edit (no "(edited)" tag). */
|
|
376
|
+
export type CompletionDelivery = {
|
|
377
|
+
readonly kind: "post";
|
|
378
|
+
readonly chunks: readonly string[];
|
|
379
|
+
} | {
|
|
380
|
+
readonly kind: "none";
|
|
381
|
+
};
|
|
382
|
+
export interface CompletionPlan {
|
|
383
|
+
readonly reaction: ReactionResolution;
|
|
384
|
+
readonly delivery: CompletionDelivery;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Translate the shared finalMessageAction decision into the concrete completion operations. Pure β
|
|
388
|
+
* no I/O, no env, fully unit-tested.
|
|
389
|
+
* - post β POST the reply fresh (split to Discord's 2000-char cap); swap πββ
when this turn
|
|
390
|
+
* placed a swap-tracked reaction (auto-threaded mention).
|
|
391
|
+
* - silent β post nothing; clear the π if one was swap-tracked (don't leave "seen, working"
|
|
392
|
+
* standing on a mention the agent decided not to answer).
|
|
393
|
+
* - buffer-tool-status β interim tool turn, not the final reply β do nothing at all.
|
|
394
|
+
* `reactionActive` = this turn seeded original-message coords (status UX on + auto-threaded).
|
|
395
|
+
* The non-threaded π (DMs, in-thread mentions) is a plain "seen" ack, deliberately NOT swapped
|
|
396
|
+
* or cleared β the answer lands right under the message, so the ack can simply stand.
|
|
397
|
+
*/
|
|
398
|
+
export declare function planCompletion(input: {
|
|
399
|
+
readonly action: FinalMessageAction;
|
|
400
|
+
readonly reactionActive: boolean;
|
|
401
|
+
/** An approval card was posted this turn (approvals/card-signal.ts) β the card IS the
|
|
402
|
+
* reply, so the model's narration is suppressed (same rule as Slack: "the card should
|
|
403
|
+
* suffice"). The turn still counts as answered β the π still clears. */
|
|
404
|
+
readonly cardPostedThisTurn?: boolean;
|
|
405
|
+
}): CompletionPlan;
|
|
406
|
+
/**
|
|
407
|
+
* Assemble the session state SEEDED into `send` for a turn. This seed only takes effect when
|
|
408
|
+
* the send STARTS a session (eve drops it on the resume path β see the session-state comment
|
|
409
|
+
* up top), which is exactly why `original` is only ever passed on the auto-thread turn: a fresh
|
|
410
|
+
* thread id guarantees a fresh session, so the seeded reaction coords are guaranteed to belong
|
|
411
|
+
* to THIS turn's triggering message. The reply surface (`channelId`) follows the re-key (the
|
|
412
|
+
* thread when threaded); the reaction target stays pinned to the mention in the BASE channel so
|
|
413
|
+
* π/β
land where the human is looking. No original coords β completion does no reaction swap.
|
|
414
|
+
*/
|
|
415
|
+
export declare function buildGatewayState(input: {
|
|
416
|
+
readonly replyChannelId: string;
|
|
417
|
+
/** The guild the reply surface is in β seeded so the completion handler can rebuild the
|
|
418
|
+
* engagement partition (see DiscordGatewayState.guildId). Absent for DMs (no guild). */
|
|
419
|
+
readonly guildId?: string;
|
|
420
|
+
readonly original?: {
|
|
421
|
+
readonly channelId: string;
|
|
422
|
+
readonly messageId: string;
|
|
423
|
+
};
|
|
424
|
+
}): DiscordGatewayState;
|
|
425
|
+
/**
|
|
426
|
+
* Consume this turn's reaction-swap coordinates from the LIVE session state: return the
|
|
427
|
+
* original-message coords and CLEAR them (consume-once). The clearing is the correctness
|
|
428
|
+
* guarantee on resumed sessions β a later turn can never react on a PREVIOUS turn's message;
|
|
429
|
+
* the coords only ever exist on the session-creating auto-thread turn. Mutates the passed
|
|
430
|
+
* state in place (eve write-back persists it). Pure in/out mapping β unit-tested.
|
|
431
|
+
*/
|
|
432
|
+
export declare function takeTurnStatus(state: DiscordGatewayState): {
|
|
433
|
+
original?: {
|
|
434
|
+
channelId: string;
|
|
435
|
+
messageId: string;
|
|
436
|
+
};
|
|
437
|
+
};
|
|
438
|
+
/** The cross-channel dispatch primitive the route hands the dispatch (eve's public
|
|
439
|
+
* `args.to` β the same primitive github.ts's fan-out + divert legs use; it was `args.receive`
|
|
440
|
+
* before eve 0.31 split the call into `to(channel, target).send(message, options)`). */
|
|
441
|
+
export type UnifiedReceive = RouteHandlerArgs["to"];
|
|
442
|
+
/**
|
|
443
|
+
* THE DISCORD PRIVACY FLOOR (fail-closed): may this surface join a PRIVATE cross-surface window?
|
|
444
|
+
*
|
|
445
|
+
* Every single-window surface needs a POSITIVE privacy fact before its wakes may share a context
|
|
446
|
+
* window with the domain's private Slack/GitHub/Linear/Notion conversations (GitHub:
|
|
447
|
+
* `repository.private === true`; Slack: the channel positively confirmed internal; Notion: the
|
|
448
|
+
* operator-vouch register). Discord's honest answer is that NO reliable positive signal exists
|
|
449
|
+
* in what reaches this gateway:
|
|
450
|
+
* - The guild is the tenant boundary (external people aren't guild members), but guilds are
|
|
451
|
+
* routinely PUBLIC/community servers with open invites β guild membership alone proves
|
|
452
|
+
* nothing about who can read or post.
|
|
453
|
+
* - The relay's MESSAGE_CREATE payload carries NO channel-permission data. The identity
|
|
454
|
+
* layer's `everyoneCanView` field (discord-identity.ts) is documented "absent today" β and
|
|
455
|
+
* its `discordVisibility` default of `private` is the WRONG direction here: that default is
|
|
456
|
+
* conservative for MEMORY (learnings stay surface-tier), but for window ADMISSION
|
|
457
|
+
* "default-private" would be permissive β it would admit every guild channel, including
|
|
458
|
+
* @everyone-viewable channels in public community guilds. A default is not a positive fact.
|
|
459
|
+
* So, exactly like Notion (whose public_url was similarly misleading), the floor is the
|
|
460
|
+
* OPERATOR-VOUCH register (identity/trust.ts isVouchedInternal β the deployer's out-of-band
|
|
461
|
+
* "this guild/channel is members-only internal" assertion, keyed on the workspaceKey
|
|
462
|
+
* `discord/<guild>` or a surfaceKey `discord/<guild>/<channel>`; a thread also honours a vouch
|
|
463
|
+
* on its PARENT channel via the domain-grain key), or the OE_TRUST=internal blanket switch for
|
|
464
|
+
* internal-only deploys. Code enforces, the operator vouches β never an inference from a payload
|
|
465
|
+
* field. Fail-CLOSED: un-vouched β false β the wake stays on the native per-channel path.
|
|
466
|
+
*
|
|
467
|
+
* DMs / group-DMs (no guildId) NEVER pass β unconditionally, even under OE_TRUST=internal
|
|
468
|
+
* (mirroring how the e2e driver allowlist excludes guildless surfaces): a DM is a PERSONAL
|
|
469
|
+
* surface with no tenant boundary at all, and its content must never enter a team-shared window.
|
|
470
|
+
*
|
|
471
|
+
* ATTACKER ANALYSIS (what goes wrong if this rule is too loose): if a public/@everyone-readable
|
|
472
|
+
* channel were admitted, anyone who can join the guild (open invite) could (a) READ private
|
|
473
|
+
* cross-surface context β an @mention here would recall, into a public channel, whatever the
|
|
474
|
+
* shared window absorbed from the domain's private repos/channels/docs β and (b) WRITE into the
|
|
475
|
+
* shared context: their message text enters the one window every surface's supervisor judges
|
|
476
|
+
* from, a persistent cross-surface prompt-injection foothold. The vouch register makes admission
|
|
477
|
+
* an explicit operator assertion with exactly that blast radius in view.
|
|
478
|
+
*
|
|
479
|
+
* Pure (vouch + env injectable) β the truth table is unit-tested without a live register
|
|
480
|
+
* (scripts/unified-window-test.ts). Mirrors notion.ts's floor + github.ts's positive-privacy gate.
|
|
481
|
+
*/
|
|
482
|
+
export declare function discordSingleWindowPrivacyOk(input: {
|
|
483
|
+
readonly guildId: string | undefined;
|
|
484
|
+
/** `discord/<guild>` β a workspace-level vouch covers every channel in the guild. */
|
|
485
|
+
readonly workspaceKey: string;
|
|
486
|
+
/** `discord/<guild>/<channel-or-thread>` β a surface-level vouch narrows to one. */
|
|
487
|
+
readonly surfaceKey: string;
|
|
488
|
+
/** The DOMAIN-grain key (a thread's parent channel β domainKeyOf); a vouch on the parent
|
|
489
|
+
* channel covers its threads. Equal to surfaceKey for a non-thread. */
|
|
490
|
+
readonly domainKey?: string;
|
|
491
|
+
}, deps?: {
|
|
492
|
+
readonly vouched?: (workspaceKey: string, surfaceKey: string) => boolean;
|
|
493
|
+
readonly trustEnv?: string | undefined;
|
|
494
|
+
}): boolean;
|
|
495
|
+
/**
|
|
496
|
+
* The Discord arm of the ONE single-window mention decision β a thin shim over the shared seam
|
|
497
|
+
* (unified-window.ts {@link singleWindowMentionPolicy}), exactly like github.ts's
|
|
498
|
+
* shouldSuppressNativeMention and slack-events.ts's slackSingleWindowMentionPolicy:
|
|
499
|
+
* `suppressNative` βΊ the divert owns the @mention (this file's native dispatch β auto-thread,
|
|
500
|
+
* π, typing, per-channel session β stands down); `addressing` is the `direct-mention` stamp the
|
|
501
|
+
* diverted wake carries so the unified supervisor knows it is owed an answer β which it must give
|
|
502
|
+
* through the `reply` tool, since OPE-1982 deleted the prose route. Both derive from the SAME predicate (`singleWindow && privacyOk && isMention`), so
|
|
503
|
+
* suppress-and-keep stay perfectly complementary: never a double reply, never a gap. `privacyOk`
|
|
504
|
+
* is {@link discordSingleWindowPrivacyOk}'s verdict (fail-closed). Pure β unit-tested truth table.
|
|
505
|
+
*/
|
|
506
|
+
export declare function discordSingleWindowMentionPolicy(singleWindow: boolean, isMention: boolean, privacyOk: boolean): SingleWindowMentionPolicy;
|
|
507
|
+
/** Everything {@link buildDiscordUnifiedWake} needs β extracted by the divert wiring, built
|
|
508
|
+
* directly by tests. */
|
|
509
|
+
export interface DiscordUnifiedWakeInput {
|
|
510
|
+
readonly guildId: string;
|
|
511
|
+
/** The conversation the trigger arrived in (a thread's OWN channel id when in a thread). */
|
|
512
|
+
readonly channelId: string;
|
|
513
|
+
/** The thread's parent channel, when known (auth enrichment) β the locus CONTAINER. */
|
|
514
|
+
readonly parentChannelId?: string;
|
|
515
|
+
readonly isThread: boolean;
|
|
516
|
+
readonly addressing: "direct-mention" | "channel-watch" | "name-drop";
|
|
517
|
+
/** This conversation carries an ENGAGED registry row β an @mention (or an earned reply) invited
|
|
518
|
+
* the agent and the invitation still stands β so a JUDGED wake states `follow-up="thread-engaged"`
|
|
519
|
+
* (channels/engaged-wake.ts). Unset/false β no attribute, byte-identical. */
|
|
520
|
+
readonly threadEngaged?: boolean;
|
|
521
|
+
readonly wakeId: string;
|
|
522
|
+
readonly engagementTier?: WakeEngagementTier;
|
|
523
|
+
/** The OUTGOING generation's recorded TAIL (OPE-794), rendered as this first wake's backscroll.
|
|
524
|
+
* Only a FRESH generation's first wake carries one. */
|
|
525
|
+
readonly tail?: readonly WakeMessageInput[];
|
|
526
|
+
/** The hydrated surrounding conversation, STRUCTURED β `renderWake` renders it through core's own
|
|
527
|
+
* road, which de-duplicates it against {@link tail} and caps BELOW that (OPE-1749). This road used
|
|
528
|
+
* to hand a pre-rendered string down beside the wake, which bypassed both. */
|
|
529
|
+
readonly contextLines?: readonly ContextLine[];
|
|
530
|
+
/** `renderContext`'s provenance label for {@link contextLines}. */
|
|
531
|
+
readonly contextSource?: string;
|
|
532
|
+
/** The bot's display name β name-drop wakes only (what the human actually said). */
|
|
533
|
+
readonly botName?: string;
|
|
534
|
+
/** This surface could not read its own history since the seat's last wake (core
|
|
535
|
+
* channels/seat-catch-up.ts). Rendered as a trusted `<system-note>`; unset β no note. */
|
|
536
|
+
readonly catchUpUnavailable?: string;
|
|
537
|
+
readonly trigger: {
|
|
538
|
+
readonly id: string;
|
|
539
|
+
readonly author: string;
|
|
540
|
+
readonly authorId: string;
|
|
541
|
+
readonly sentAt: string;
|
|
542
|
+
readonly text: string;
|
|
543
|
+
/**
|
|
544
|
+
* Resolved per-wake trust tier (identity/wake-trust.ts).
|
|
545
|
+
*
|
|
546
|
+
* π΄ REQUIRED, NOT OPTIONAL (OPE-596) β see LinearSupervisorWakeInput.trigger.trust. The field
|
|
547
|
+
* used to default UPWARD (`?? (direct ? "principal" : "peer")`), which is fail-OPEN, and since
|
|
548
|
+
* OPE-577 the tier decides the `<untrusted_external_data>` fence. A caller that cannot resolve
|
|
549
|
+
* a tier states the fail-closed one itself: `relay` for a machine author, `unauth` for an
|
|
550
|
+
* unvouchable human.
|
|
551
|
+
*/
|
|
552
|
+
readonly trust: WakeTrust;
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Build the NormalizedWake for one DIVERTED Discord wake β the exact shape
|
|
557
|
+
* buildGitHubSupervisorWake produces for GitHub, with Discord's locus mapping: container =
|
|
558
|
+
* the (parent) channel, conversation = the thread when the trigger was in one. Always stamps
|
|
559
|
+
* the platform address + P5 disclosure + wake-id (this builder exists ONLY for the
|
|
560
|
+
* single-window path β the native dispatch renders via renderTurn/renderNameDropTurn,
|
|
561
|
+
* untouched). renderWake then emits the `{message, context}` every OE dispatch sends β the
|
|
562
|
+
* SAME renderer Slack/GitHub/Linear/Notion use, with NO platform branch. Pure β unit-testable.
|
|
563
|
+
*/
|
|
564
|
+
export declare function buildDiscordUnifiedWake(input: DiscordUnifiedWakeInput): NormalizedWake;
|
|
565
|
+
/** Everything {@link discordNativeWake} needs β extracted by the native dispatch, built directly
|
|
566
|
+
* by tests. The NON-unified sibling of {@link DiscordUnifiedWakeInput}: no wake-id (per-channel
|
|
567
|
+
* session; the reply posts back via message.completed, not a routed reply tool) and no P5
|
|
568
|
+
* disclosure (single-surface, not a cross-surface window). */
|
|
569
|
+
export interface DiscordNativeWakeInput {
|
|
570
|
+
/** The conversation the trigger arrived in (a thread's OWN channel id when in a thread). */
|
|
571
|
+
readonly channelId: string;
|
|
572
|
+
/** The thread's parent channel, when known β the locus CONTAINER (domain/spend grain). */
|
|
573
|
+
readonly parentChannelId?: string;
|
|
574
|
+
readonly isThread: boolean;
|
|
575
|
+
/** A DM (channel type 1/3) β an inherently-addressed `direct` wake whose reason is `dm`. */
|
|
576
|
+
readonly isDm: boolean;
|
|
577
|
+
readonly addressing: "direct" | "thread-follow" | "name-drop";
|
|
578
|
+
/** This conversation carries a PARTICIPATING, un-stopped engagement row β see
|
|
579
|
+
* {@link DiscordUnifiedWakeInput.threadEngaged}. π΄ NOT derivable from `thread-follow`: that tier
|
|
580
|
+
* is reached past an `isEngaged` gate a name-drop's WARM latch (`participating: false`) also
|
|
581
|
+
* satisfies, and a seat nobody invited must not be told it is enrolled. */
|
|
582
|
+
readonly threadEngaged?: boolean;
|
|
583
|
+
/** The bot's display name β name-drop wakes only (what the human actually said). */
|
|
584
|
+
readonly botName?: string;
|
|
585
|
+
/** RAW side-channel backscroll (mention/name-drop hydration) β renderWake wraps it (W3). */
|
|
586
|
+
readonly contextLines?: readonly ContextLine[];
|
|
587
|
+
readonly contextSource?: string;
|
|
588
|
+
readonly trigger: {
|
|
589
|
+
readonly id: string;
|
|
590
|
+
readonly author: string;
|
|
591
|
+
readonly authorId: string;
|
|
592
|
+
readonly sentAt: string;
|
|
593
|
+
readonly text: string;
|
|
594
|
+
/** Resolved per-wake trust tier (identity/wake-trust.ts). π΄ REQUIRED (OPE-596) β see
|
|
595
|
+
* {@link DiscordUnifiedWakeInput}'s trigger tier for the whole reason. */
|
|
596
|
+
readonly trust: WakeTrust;
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Build the NormalizedWake for one NATIVE (non-diverted) Discord wake β the per-channel-session
|
|
601
|
+
* path that renderTurn/renderNameDropTurn used to hand-render. It maps into the SAME shared type
|
|
602
|
+
* every other surface uses, so renderWake emits the one canonical dialect with NO platform branch:
|
|
603
|
+
* - `direct` (DM / @mention): the conversation driver β renderWake returns the body VERBATIM (no
|
|
604
|
+
* preamble, no envelope, unwrapped β W4), byte-identical to the old renderTurn(true).
|
|
605
|
+
* - `thread-follow` (engaged non-mention) / `name-drop`: JUDGED β the canonical Slack-parity
|
|
606
|
+
* preamble + the full wake envelope (the migration's deliberate convergence; the old Discord
|
|
607
|
+
* prose stub had DRIFTED away from the social-chatter / never-re-answer / PREFER_NO_REPLY_TOOL
|
|
608
|
+
* sentences, each of which encodes an observed judge failure).
|
|
609
|
+
* Locus: container = the (parent) channel; conversation = the thread when the trigger was in one.
|
|
610
|
+
* No wakeId is minted (a native wake answers through the base `reply`, which names no wake). Pure β
|
|
611
|
+
* unit-testable without a live Discord call.
|
|
612
|
+
*/
|
|
613
|
+
export declare function discordNativeWake(input: DiscordNativeWakeInput): NormalizedWake;
|
|
614
|
+
/** Everything {@link buildDiscordDmWake} needs β extracted by the DM divert, built directly by
|
|
615
|
+
* tests. Deliberately NARROWER than {@link DiscordUnifiedWakeInput}: a DM has no guild, no thread,
|
|
616
|
+
* no engagement tier and no name-drop, and every one of those absences is a property of the
|
|
617
|
+
* surface rather than a field a caller might or might not fill in. */
|
|
618
|
+
export interface DiscordDmWakeInput {
|
|
619
|
+
/** The DM (or group-DM) channel id β a DM's whole locus. */
|
|
620
|
+
readonly channelId: string;
|
|
621
|
+
readonly wakeId: string;
|
|
622
|
+
/** The OUTGOING generation's recorded TAIL (OPE-794), rendered as this first wake's backscroll.
|
|
623
|
+
* Only a FRESH generation's first wake carries one. */
|
|
624
|
+
readonly tail?: readonly WakeMessageInput[];
|
|
625
|
+
readonly trigger: {
|
|
626
|
+
readonly id: string;
|
|
627
|
+
readonly author: string;
|
|
628
|
+
readonly authorId: string;
|
|
629
|
+
readonly sentAt: string;
|
|
630
|
+
readonly text: string;
|
|
631
|
+
/** Resolved per-wake trust tier (identity/wake-trust.ts). π΄ REQUIRED (OPE-596) β the field
|
|
632
|
+
* used to default to `principal`, the highest tier, on the reasoning that a DM partner IS the
|
|
633
|
+
* seat's principal. That reasoning is about the ROOM, and the tier is about the AUTHOR. See
|
|
634
|
+
* {@link DiscordUnifiedWakeInput}'s trigger tier. */
|
|
635
|
+
readonly trust: WakeTrust;
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* THE 1:1 (and group) DM WAKE for a first-classed Discord DM window (OPE-356) β the Discord twin of
|
|
640
|
+
* slack-events.ts's `slackDmWake`, and it renders through the SAME core renderer with no platform
|
|
641
|
+
* branch, so the two surfaces' DM envelopes are one dialect.
|
|
642
|
+
*
|
|
643
|
+
* ADDRESSING IS `direct-mention`, ALWAYS, doing the same two jobs it does on Slack. Semantically it
|
|
644
|
+
* is simply TRUE: everything said in a DM is said to the agent, which is why this path bypasses the
|
|
645
|
+
* engagement gate, the name-drop judge and the channel-watch silence bias entirely β a DM IS ALWAYS
|
|
646
|
+
* ANSWERED. Mechanically it is LOAD-BEARING: `direct-mention` is what stamps `oe_addressing` through
|
|
647
|
+
* the divert, which marks the wake as OWED a reply and selects the `[direct]` preamble's reply
|
|
648
|
+
* mandate. π΄ IT IS NOT A DELIVERY PATH (OPE-1982): the addressed-prose bridge it used to arm is
|
|
649
|
+
* deleted, so a DM answer written as prose rather than a `reply` call is DROPPED on that channel
|
|
650
|
+
* like any other, and re-driven once towards the tool.
|
|
651
|
+
*
|
|
652
|
+
* `reason` is `dm`, not `mention`: the envelope's reason describes the SURFACE SHAPE. It is also the
|
|
653
|
+
* one reason that survives the OPE-744 collapse of `mention` into `channel-activity`, because no
|
|
654
|
+
* other attribute states "this is a direct-message room".
|
|
655
|
+
*
|
|
656
|
+
* NO `engagementTier`: that dial raises the bar for AMBIENT watching, and a DM is always answered.
|
|
657
|
+
*
|
|
658
|
+
* The `wakeId` is present because the unified channel is the delivery route: it is the id the model
|
|
659
|
+
* must echo to `reply`, and the only way a DM answer gets out. Pure β unit-testable.
|
|
660
|
+
*/
|
|
661
|
+
export declare function buildDiscordDmWake(input: DiscordDmWakeInput): NormalizedWake;
|
|
662
|
+
/**
|
|
663
|
+
* THE DOMAIN'S ADMISSION POLICY over a mention/name-drop hydration, for BOTH of this surface's wake roads (OPE-1800).
|
|
664
|
+
* π΄ THE DOMAIN'S POLICY, ON THIS READ TOO. A seated Discord wake takes TWO reads of the room: the since-last-wake catch-up, which has passed `seatAdmissionFor` since OPE-1726, and this hydration, which did not. So a `members-only` domain dropped a guest's message from the catch-up and then handed the seat the SAME message inside the context block. The policy is resolved by NAME from the caller's own domain key and applied by the shared core seam (channels/seat-admission.ts `admitContextLines`), which is the one mapping Slack's hydration takes β never a per-bundle copy.
|
|
665
|
+
* π΄ THE SEAT DECIDES, AND THE SEAT IS `grainUsesUnifiedWindow(grain)` ALONE β never "which road this wake took". That is the exact predicate Slack resolves `seated` from ({@link slackDomainUsesUnifiedWindow}, "the SAME fact β¦ without the async privacy floor"), and matching it is what gives one domain ONE admission answer. A wake whose room HAS a seat but which falls through to `dispatchTurn`'s native road β the guild vouch absent, or no `receive` β is a seat STOOD DOWN, not an unseated room: access/window-routing.ts calls that state `native-degraded` and hooks/window-routing.ts audits it `window.routing_degraded`. Reading it as "no seat" would hand that domain a second admission answer, selected by a runtime vouch.
|
|
666
|
+
* π΄ A TRULY UNSEATED DEPLOYMENT KEEPS ITS WHOLE HYDRATION: `window.topology` unarmed, or no seat domain reaches the room, so `grainUsesUnifiedWindow` is false. There is no seat for a seat policy to govern, that hydration is the only context such a wake carries, and the shipped `wide` default would strip the attributable-bot lines it keeps for coherence.
|
|
667
|
+
* π΄ GRADE FIRST, THEN ADMIT. `lines` must already carry the per-author regrade ({@link gradeDiscordContextLines}): the policy judges the RESOLVED tier, and an ungraded person floors at `unauth`, so admitting first would make `members-only` refuse every human line in the room.
|
|
668
|
+
* Nothing admitted β `undefined`, the same shape an absent fetch produces β never an empty slot, which renders as a quiet room.
|
|
669
|
+
*/
|
|
670
|
+
export declare function admitDiscordSeatLines(lines: readonly ContextLine[] | undefined, grain: SupervisorGrain, domainKey: string, isThread: boolean): readonly ContextLine[] | undefined;
|
|
671
|
+
/**
|
|
672
|
+
* The WINDOW road's use of {@link admitDiscordSeatLines}: admit, then derive the taint-join facts β both over ONE set.
|
|
673
|
+
* π΄ IT NO LONGER RENDERS (OPE-1749). It used to hand `divertDiscordWakeToUnifiedWindow` a PRE-RENDERED string, which took this road OUT of the core render β and with it out of the envelope de-duplication and the cap ORDER that `renderWake` applies through `hydrationSlices`. A seat whose carried TAIL overlapped this hydration then rendered each overlapping message TWICE and spent the ten-line cap doing it. The road now hands the structured lines down to the builder, exactly as every other surface does, and core decides what renders.
|
|
674
|
+
* π΄ ONE SET FEEDS BOTH OUTPUTS, so the join can never carry a line the policy REFUSED β which is the property that matters, because a refused line's bytes never reach the model. It is not an equality of counts: the core render applies its own bounds and the de-duplication drops the envelope's own copies, so FEWER lines render than the join marks. That direction over-taints, which is the safe one (OPE-475) β and a de-duplicated line's bytes did enter, through the envelope sibling that replaced it.
|
|
675
|
+
* Nothing admitted β EMPTY on both, the same shape an absent or empty fetch produces, never an empty context block.
|
|
676
|
+
*/
|
|
677
|
+
export declare function discordSeatContextForWindow(lines: readonly ContextLine[] | undefined, source: string | undefined, grain: SupervisorGrain, domainKey: string, isThread: boolean): {
|
|
678
|
+
readonly lines: readonly ContextLine[];
|
|
679
|
+
readonly source?: string;
|
|
680
|
+
readonly ingestion: ReadonlyArray<ReturnType<typeof contextLineIngestion>>;
|
|
681
|
+
};
|
|
682
|
+
/**
|
|
683
|
+
* Divert one Discord wake into the unified window: get-or-create the DOMAIN session (the SAME
|
|
684
|
+
* registry row + rotation policy every other surface's live deps drive β github.ts
|
|
685
|
+
* liveGitHubSupervisorDeps β so a Discord wake lands in whatever generation the domain is on),
|
|
686
|
+
* build + render the wake with a host-minted wake-id, and hand it to the shared
|
|
687
|
+
* divertToUnifiedWindow (which stamps the auth and registers the durable wakeIdβorigin map
|
|
688
|
+
* entry BEFORE the send, inside unified.ts's fail-closed receive).
|
|
689
|
+
*
|
|
690
|
+
* π΄ NOTHING HERE REACTS TO A SESSION THAT DIES AT TURN TIME, and nothing can: the dispatch awaits
|
|
691
|
+
* only the ENQUEUE, so a turn-time throw cannot reject it, and eve emits no userland event for the
|
|
692
|
+
* unreadable-World class (channels/session-resume.ts holds the measurement). This comment used to
|
|
693
|
+
* promise a force-rotate-and-re-deliver arm; OPE-174 removed that arm as provably inert. The
|
|
694
|
+
* remedy that does work is PRE-DISPATCH β the OPE-599 runtime-change claim below, which rotates
|
|
695
|
+
* BEFORE the wake goes out so the trigger lands in a session the running eve build can read.
|
|
696
|
+
*/
|
|
697
|
+
/**
|
|
698
|
+
* EXPORTED FOR THE INGRESS CONTRACT TEST (OPE-1719), not for another caller. `dispatchTurn` is the
|
|
699
|
+
* only production caller and it is one file away. `scripts/owned-thread-ingress-wiring-test.ts`
|
|
700
|
+
* drives THIS function β the real Discord ingress divert, over the real store β because a test that
|
|
701
|
+
* reimplemented the wake dep would stay green after somebody moved this leg back to the raw divert,
|
|
702
|
+
* which is the exact regression the ticket closes.
|
|
703
|
+
*/
|
|
704
|
+
export declare function divertDiscordWakeToUnifiedWindow(input: {
|
|
705
|
+
readonly msg: DiscordRelayMessage;
|
|
706
|
+
readonly auth: OpenEverywhereDiscordAuth;
|
|
707
|
+
readonly partition: Partition;
|
|
708
|
+
/** Guaranteed `unified` + `singleWindow` + `domainName` by the caller's gate. */
|
|
709
|
+
readonly grain: SupervisorGrain;
|
|
710
|
+
readonly addressing: "direct-mention" | "channel-watch" | "name-drop";
|
|
711
|
+
/**
|
|
712
|
+
* The caller's OWN `discordSingleWindowPrivacyOk` verdict β an operator-VOUCHED guild surface. It
|
|
713
|
+
* is the same fact the trigger's trust tier is resolved from, and the catch-up's per-author trust
|
|
714
|
+
* resolver reads it rather than hardcoding one (channels/seat-catch-up.ts `surfaceInternal` says
|
|
715
|
+
* why). Today the only production caller gates this whole divert on it, so it is always `true`
|
|
716
|
+
* there; passing it keeps that a STRUCTURAL fact rather than a convention a second caller could
|
|
717
|
+
* break, since this function is exported.
|
|
718
|
+
*/
|
|
719
|
+
readonly privacyOk: boolean;
|
|
720
|
+
/** The ENROLMENT fact for a judged wake β see {@link DiscordUnifiedWakeInput.threadEngaged}. The
|
|
721
|
+
* caller reads it off the engagement row it already fetched for the dispatch gate. */
|
|
722
|
+
readonly threadEngaged?: boolean;
|
|
723
|
+
readonly receive: UnifiedReceive;
|
|
724
|
+
/** The ADMITTED backscroll lines, STRUCTURED, possibly empty (OPE-1749). */
|
|
725
|
+
readonly contextLines: readonly ContextLine[];
|
|
726
|
+
/** `renderContext`'s provenance label for {@link contextLines}. */
|
|
727
|
+
readonly contextSource?: string;
|
|
728
|
+
/** OPE-475: the taint join's per-line facts for the SAME backscroll lines `contextLines` holds
|
|
729
|
+
* ({@link gradeDiscordContextLines} output, mapped via `contextLineIngestion`) β carried
|
|
730
|
+
* alongside them because the join is the CALLER's to build: the render drops the envelope's own
|
|
731
|
+
* copies, whose bytes still entered through those siblings. Possibly empty. */
|
|
732
|
+
readonly contextIngestion: ReadonlyArray<ReturnType<typeof contextLineIngestion>>;
|
|
733
|
+
}): Promise<void>;
|
|
734
|
+
/** The `send` shape this channel dispatches with (SendFn parametrized by our state). The first
|
|
735
|
+
* arg is a plain string OR eve's SendPayload `{ message, context }` β `context` strings are
|
|
736
|
+
* appended as user messages before the delivery turn (used for the wake/mention backscroll). */
|
|
737
|
+
export type DiscordGatewaySend = (message: string | {
|
|
738
|
+
readonly message: string;
|
|
739
|
+
readonly context?: readonly string[];
|
|
740
|
+
}, options: {
|
|
741
|
+
auth: OpenEverywhereDiscordAuth;
|
|
742
|
+
continuationToken: string;
|
|
743
|
+
state: DiscordGatewayState;
|
|
744
|
+
}) => Promise<unknown>;
|
|
745
|
+
/**
|
|
746
|
+
* Resolve the wake decision (consulting the engagement registry only when needed) and, if it
|
|
747
|
+
* passes, start/resume the durable session. A mention also ENGAGES the channel so later
|
|
748
|
+
* non-mention replies keep waking it (reply-without-re-tagging, exactly like Slack). Runs in
|
|
749
|
+
* the background (handed to waitUntil) so the route acks fast.
|
|
750
|
+
*/
|
|
751
|
+
export declare function dispatchTurn(msg: DiscordRelayMessage, auth: OpenEverywhereDiscordAuth, send: DiscordGatewaySend, createThread?: (channelId: string, messageId: string, name: string) => Promise<string | null>, resolveParent?: (channelId: string) => Promise<string | undefined>, receive?: UnifiedReceive): Promise<void>;
|
|
752
|
+
export declare function engagementCoordsFor(state: DiscordGatewayState): {
|
|
753
|
+
p: Partition;
|
|
754
|
+
key: string;
|
|
755
|
+
} | null;
|
|
756
|
+
/**
|
|
757
|
+
* Promote the surface to PARTICIPATING on a POSTED reply (incl. a card-only reply β the card IS
|
|
758
|
+
* the reply), mirroring slack.ts's post branch: a warm (name-drop) surface the agent actually
|
|
759
|
+
* answered goes sticky, its silent streak resets, and its failures go loud.
|
|
760
|
+
*
|
|
761
|
+
* THE DISCORD DIVERGENCE (why this reads-then-promotes instead of blindly engaging): Slack promotes
|
|
762
|
+
* unconditionally because its engagement grain is the THREAD, so a name-drop reply merely
|
|
763
|
+
* establishing THREAD engagement is harmless. Discord's BASE-channel engagement grain is the WHOLE
|
|
764
|
+
* CHANNEL, and a base-channel name-drop DELIBERATELY does not engage (dispatchTurn β else every
|
|
765
|
+
* subsequent channel message becomes a judged model turn, the "engaged-channel surprise" from live
|
|
766
|
+
* testing). So a base-channel name-drop reply must NOT create whole-channel engagement. We only
|
|
767
|
+
* PROMOTE an EXISTING warm record (a thread name-drop engaged at dispatch, or a mention/engaged
|
|
768
|
+
* surface); no record β a base-channel name-drop β leave it un-engaged. Best-effort: engagement
|
|
769
|
+
* must never fail a delivered reply.
|
|
770
|
+
*/
|
|
771
|
+
export declare function promoteParticipatingOnReply(state: DiscordGatewayState): Promise<void>;
|
|
772
|
+
/**
|
|
773
|
+
* Warm auto-disengage on a JUDGED-SILENT turn (mirrors slack.ts's silent branch): a WARM
|
|
774
|
+
* (participating:false) surface that keeps judging NO_REPLY is shed after WARM_SILENT_LIMIT turns,
|
|
775
|
+
* so a one-off name-drop that never earns a reply doesn't leave the bot quietly following forever.
|
|
776
|
+
* PARTICIPATING surfaces are EXEMPT β they're sticky, only an explicit stop disengages them β so a
|
|
777
|
+
* missing record OR participating:true is a no-op. Best-effort; engagement bookkeeping never throws
|
|
778
|
+
* out of the completion handler.
|
|
779
|
+
*/
|
|
780
|
+
export declare function maybeShedWarmSurface(state: DiscordGatewayState): Promise<void>;
|
|
781
|
+
declare const _default: import("eve/channels").Channel<DiscordGatewayState, Record<string, unknown>, Record<string, unknown>>;
|
|
782
|
+
export default _default;
|
|
783
|
+
//# sourceMappingURL=discord-gateway.d.ts.map
|