@open-everywhere/provider-slack 0.8.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/channels/slack-agent-session.ts +100 -63
- package/agent/channels/slack-events.ts +1 -1
- package/agent/channels/slack-front.ts +15 -8
- package/agent/channels/slack-identity.ts +8 -0
- package/agent/channels/slack-web-api.ts +3 -3
- package/agent/channels/slack-working-signs.ts +71 -0
- package/agent/channels/slack.ts +36 -50
- package/agent/docs-generated.ts +36 -16
- package/agent/ops.ts +4 -0
- package/agent/register.ts +4 -2
- package/agent/setup/claim.ts +9 -8
- package/agent/setup/credentials.ts +82 -79
- package/agent/setup/eve-credentials.ts +2 -2
- package/agent/setup/page.ts +3 -3
- package/agent/status.ts +12 -4
- package/dist/agent/channels/slack-agent-session.d.ts +19 -3
- package/dist/agent/channels/slack-agent-session.d.ts.map +1 -1
- package/dist/agent/channels/slack-agent-session.js +102 -65
- package/dist/agent/channels/slack-agent-session.js.map +1 -1
- package/dist/agent/channels/slack-events.js +1 -1
- package/dist/agent/channels/slack-events.js.map +1 -1
- package/dist/agent/channels/slack-front.d.ts.map +1 -1
- package/dist/agent/channels/slack-front.js +16 -9
- package/dist/agent/channels/slack-front.js.map +1 -1
- package/dist/agent/channels/slack-identity.d.ts +7 -0
- package/dist/agent/channels/slack-identity.d.ts.map +1 -1
- package/dist/agent/channels/slack-identity.js +2 -0
- package/dist/agent/channels/slack-identity.js.map +1 -1
- package/dist/agent/channels/slack-web-api.js +3 -3
- package/dist/agent/channels/slack-web-api.js.map +1 -1
- package/dist/agent/channels/slack-working-signs.d.ts +34 -0
- package/dist/agent/channels/slack-working-signs.d.ts.map +1 -0
- package/dist/agent/channels/slack-working-signs.js +63 -0
- package/dist/agent/channels/slack-working-signs.js.map +1 -0
- package/dist/agent/channels/slack.d.ts.map +1 -1
- package/dist/agent/channels/slack.js +28 -46
- package/dist/agent/channels/slack.js.map +1 -1
- package/dist/agent/docs-generated.d.ts.map +1 -1
- package/dist/agent/docs-generated.js +36 -16
- package/dist/agent/docs-generated.js.map +1 -1
- package/dist/agent/ops.d.ts.map +1 -1
- package/dist/agent/ops.js +4 -1
- package/dist/agent/ops.js.map +1 -1
- package/dist/agent/register.d.ts.map +1 -1
- package/dist/agent/register.js +4 -2
- package/dist/agent/register.js.map +1 -1
- package/dist/agent/setup/claim.d.ts.map +1 -1
- package/dist/agent/setup/claim.js +9 -8
- package/dist/agent/setup/claim.js.map +1 -1
- package/dist/agent/setup/credentials.d.ts +21 -20
- package/dist/agent/setup/credentials.d.ts.map +1 -1
- package/dist/agent/setup/credentials.js +71 -75
- package/dist/agent/setup/credentials.js.map +1 -1
- package/dist/agent/setup/eve-credentials.js +2 -2
- package/dist/agent/setup/eve-credentials.js.map +1 -1
- package/dist/agent/setup/page.js +3 -3
- package/dist/agent/setup/page.js.map +1 -1
- package/dist/agent/status.d.ts +10 -2
- package/dist/agent/status.d.ts.map +1 -1
- package/dist/agent/status.js +12 -4
- package/dist/agent/status.js.map +1 -1
- package/package.json +6 -2
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
// registers the Slack setter: it names the session from the delivery's auth attributes. The
|
|
22
22
|
// person's next message sets `processing`, then parks `active` unless it reports again that it
|
|
23
23
|
// waits on the person. A job's result wake names no session, so its turn sets no state.
|
|
24
|
-
// • THE WORKING SIGNS (OPE-2629). The same setter
|
|
25
|
-
//
|
|
24
|
+
// • THE WORKING SIGNS (OPE-2629, OPE-2632). The same setter, and the stepper, show the mention door's
|
|
25
|
+
// signs on a DM-door turn — a 1:1 DM or a group DM: 👀, the status line with its step labels, and
|
|
26
|
+
// at the end no sign, or 🛑 when the turn failed for good. See "the working signs" below.
|
|
26
27
|
// • A FORWARDED JOB's WAIT (OPE-2536). A job the DM seat forwards to the work session can end waiting
|
|
27
28
|
// on the person: it asked a question in the room (`ask_room`), or it needs their own sign-in. The
|
|
28
29
|
// work session has no `update_status`, so the host sets the session `suspended` at the job's
|
|
@@ -40,7 +41,6 @@
|
|
|
40
41
|
import {
|
|
41
42
|
cancelAnchoredTurn,
|
|
42
43
|
resolvePartition,
|
|
43
|
-
STATUS_REACTION,
|
|
44
44
|
swallowRoomOp,
|
|
45
45
|
turnAnchorKey,
|
|
46
46
|
turnChannelKey,
|
|
@@ -48,8 +48,13 @@ import {
|
|
|
48
48
|
type CancelResult,
|
|
49
49
|
type SurfaceSessionPinner,
|
|
50
50
|
type SurfaceSessionSetter,
|
|
51
|
+
type SurfaceSessionStatus,
|
|
52
|
+
type SurfaceSessionStepper,
|
|
53
|
+
type SurfaceTurnEnd,
|
|
51
54
|
} from "@open-everywhere/core/agent/adapter-api.js";
|
|
52
55
|
import { bindableForSlackIds, slackForm, type SlackEventEnvelope } from "./slack-events.js";
|
|
56
|
+
import { type StepAction } from "./slack-step-label.js";
|
|
57
|
+
import { setSlackStatusReaction, setSlackThreadStatus, SLACK_WORKING_STATUS, slackStepStatus, type SlackSignRequest } from "./slack-working-signs.js";
|
|
53
58
|
|
|
54
59
|
/** Slack's event for a press of the session's Stop button. */
|
|
55
60
|
export const AGENT_SESSION_STOPPED_EVENT = "agent_session_stopped";
|
|
@@ -63,13 +68,6 @@ export type SlackSessionStatus = "processing" | "active" | "suspended";
|
|
|
63
68
|
/** The auth attribute the DM door stamps with the session's thread root (slack-front.ts `scheduleDmWake`). */
|
|
64
69
|
const AGENT_SESSION_TS_ATTRIBUTE = "agent_session_ts";
|
|
65
70
|
|
|
66
|
-
/**
|
|
67
|
-
* The mark a PINNED bag carries (OPE-2629). A forwarded job's settlement sets the session's state from
|
|
68
|
-
* its pin, not from a turn, so the setter must not touch the turn's working signs for it: a turn of the
|
|
69
|
-
* same session can be running at that moment, and its 👀 must stay until its own park.
|
|
70
|
-
*/
|
|
71
|
-
const SESSION_PIN_ATTRIBUTE = "oe_session_pin";
|
|
72
|
-
|
|
73
71
|
/** The form-call seam: `slackForm` in production, a recorder in the tests. */
|
|
74
72
|
export type SlackFormCall = (method: string, body: string) => Promise<Record<string, unknown> | undefined>;
|
|
75
73
|
|
|
@@ -166,32 +164,34 @@ export const slackSurfaceSessionPin: SurfaceSessionPinner = (attributes) => {
|
|
|
166
164
|
if (!session) return undefined;
|
|
167
165
|
return {
|
|
168
166
|
id: `${session.channelId}:${session.threadTs}`,
|
|
169
|
-
attributes: { channel_id: session.channelId, channel_type: "im", [AGENT_SESSION_TS_ATTRIBUTE]: session.threadTs
|
|
167
|
+
attributes: { channel_id: session.channelId, channel_type: "im", [AGENT_SESSION_TS_ATTRIBUTE]: session.threadTs },
|
|
170
168
|
};
|
|
171
169
|
};
|
|
172
170
|
|
|
173
171
|
// --- the working signs ------------------------------------------------------------------------------
|
|
174
172
|
//
|
|
175
|
-
// 🔴 THE DM
|
|
176
|
-
// none of the Slack channel's own handlers reach
|
|
177
|
-
//
|
|
178
|
-
// the
|
|
179
|
-
// start (`processing`)
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
// •
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
173
|
+
// 🔴 THE DM DOOR's WORKING SIGNS (OPE-2629, OPE-2632). A 1:1 DM and a group DM run on the `unified`
|
|
174
|
+
// channel, so none of the Slack channel's own handlers reach them: `onInbound` (the 👀), the
|
|
175
|
+
// `turn.started` and `actions.requested` renderers (the status line and its step labels) and the
|
|
176
|
+
// `session.failed` arm (the 🛑) serve only eve's native mention door. Core's turn-anchor hook calls
|
|
177
|
+
// the setter below at a DM turn's start (`processing`), at its park (`active` or `suspended`) and at
|
|
178
|
+
// `session.failed` (the park's state with `{ failed: true }`), and the stepper at each step. The signs
|
|
179
|
+
// ride those calls, through the SAME functions the mention door calls (slack-working-signs.ts):
|
|
180
|
+
// • the status reaction on the message the DM door stamped (`working_signs_ts`: the message itself
|
|
181
|
+
// when top-level, its thread root for a threaded reply): 👀 at the start, none at the park, 🛑
|
|
182
|
+
// when the turn failed for good;
|
|
183
|
+
// • the status line on that root: `Working...` at the start, each step's code-owned label (or a
|
|
184
|
+
// status verb) at each step, and cleared at every end. The flat DM reply does not clear it.
|
|
185
|
+
// Only a delivery the DM door stamped shows a sign: a person's 1:1 or group-DM message, and a follow-up
|
|
186
|
+
// press inside a thread (on its thread root). A channel turn carries no stamp (the mention door draws
|
|
187
|
+
// its own), and neither does a shortcut or a top-level press (a minted ts names no message) or a
|
|
188
|
+
// forwarded job's PIN (its settlement must not clear a running turn's 👀).
|
|
189
189
|
|
|
190
|
-
/** The status line a DM turn shows
|
|
191
|
-
export const SLACK_DM_WORKING_STATUS =
|
|
190
|
+
/** The status line a DM turn shows at its start — the mention door's own words (slack-working-signs.ts). */
|
|
191
|
+
export const SLACK_DM_WORKING_STATUS = SLACK_WORKING_STATUS;
|
|
192
192
|
|
|
193
|
-
/** The
|
|
194
|
-
const
|
|
193
|
+
/** The auth attribute the DM door stamps with the message the signs ride (slack-front.ts `scheduleDmWake`). */
|
|
194
|
+
const WORKING_SIGNS_TS_ATTRIBUTE = "working_signs_ts";
|
|
195
195
|
|
|
196
196
|
/**
|
|
197
197
|
* What an operator does when one sign call fails with a setup error (`SIGN_SETUP_ERRORS`). The sign
|
|
@@ -200,8 +200,8 @@ const WORKING_REACTION = STATUS_REACTION.working;
|
|
|
200
200
|
* every turn.
|
|
201
201
|
*/
|
|
202
202
|
const SIGN_SETUP_FIX: Readonly<Record<string, string>> = {
|
|
203
|
-
"reactions.add": "the 👀
|
|
204
|
-
"reactions.remove": "the 👀
|
|
203
|
+
"reactions.add": "the 👀 and the 🛑 need the reactions:write bot scope",
|
|
204
|
+
"reactions.remove": "the 👀 and the 🛑 need the reactions:write bot scope",
|
|
205
205
|
"assistant.threads.setStatus": "the DM status line needs the assistant:write bot scope and the agent view (features.agent_view)",
|
|
206
206
|
};
|
|
207
207
|
/** Slack's answers that mean the app lacks a scope or a feature: a setup gap, not a passing fault. */
|
|
@@ -210,24 +210,26 @@ const SIGN_SETUP_ERRORS = new Set(["missing_scope", "feature_disabled", "not_all
|
|
|
210
210
|
const SIGN_NO_OP_ERRORS = new Set(["already_reacted", "no_reaction"]);
|
|
211
211
|
const warnedSignErrors = new Set<string>();
|
|
212
212
|
|
|
213
|
-
/**
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
213
|
+
/** The DM door's sign seam over a form call. Never throws; a failure is logged once per method and error, with the setup fix. */
|
|
214
|
+
function formSignRequest(form: SlackFormCall): SlackSignRequest {
|
|
215
|
+
return async (method, params) => {
|
|
216
|
+
let error: string;
|
|
217
|
+
try {
|
|
218
|
+
const res = await form(method, new URLSearchParams({ ...params }).toString());
|
|
219
|
+
if (res?.ok === true) return;
|
|
220
|
+
error = String(res?.error ?? "no answer");
|
|
221
|
+
} catch (err) {
|
|
222
|
+
error = String(err);
|
|
223
|
+
}
|
|
224
|
+
if (SIGN_NO_OP_ERRORS.has(error)) return;
|
|
225
|
+
const key = `${method}:${error}`;
|
|
226
|
+
if (warnedSignErrors.has(key)) return;
|
|
227
|
+
warnedSignErrors.add(key);
|
|
228
|
+
const fix = SIGN_SETUP_ERRORS.has(error)
|
|
229
|
+
? `${SIGN_SETUP_FIX[method] ?? "this sign needs a manifest scope"}. Fix: paste the app manifest again (App Manifest → Save Changes) and reinstall if Slack asks`
|
|
230
|
+
: "the sign is best-effort and the turn goes on";
|
|
231
|
+
console.warn(`[open-everywhere:slack-session] ${method} failed (${error}) — ${fix}. Logged once per process.`);
|
|
232
|
+
};
|
|
231
233
|
}
|
|
232
234
|
|
|
233
235
|
/** Test-only: forget which sign failures were already logged. */
|
|
@@ -235,26 +237,61 @@ export function _resetSignWarningsForTest(): void {
|
|
|
235
237
|
warnedSignErrors.clear();
|
|
236
238
|
}
|
|
237
239
|
|
|
240
|
+
/** The message a DM-door delivery's signs ride, or undefined when the door stamped none. Pure. */
|
|
241
|
+
export function slackWorkingSignsOf(attributes: Readonly<Record<string, unknown>>): { readonly channelId: string; readonly threadTs: string; readonly teamId?: string } | undefined {
|
|
242
|
+
if (attributes.channel_type !== "im" && attributes.channel_type !== "mpim") return undefined;
|
|
243
|
+
const channelId = attributes.channel_id;
|
|
244
|
+
const stamped = attributes[WORKING_SIGNS_TS_ATTRIBUTE];
|
|
245
|
+
if (typeof channelId !== "string" || channelId.length === 0 || typeof stamped !== "string" || stamped.length === 0) return undefined;
|
|
246
|
+
return { channelId, threadTs: stamped, ...(typeof attributes.team_id === "string" ? { teamId: attributes.team_id } : {}) };
|
|
247
|
+
}
|
|
248
|
+
|
|
238
249
|
/**
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* spoof) sends neither.
|
|
250
|
+
* Set the DM turn's signs for one state: 👀 and `Working...` at `processing`; at a settle, no
|
|
251
|
+
* reaction (or 🛑 when the turn failed) and a cleared status line. The two calls run in parallel,
|
|
252
|
+
* because the turn waits on this setter. A swallowed room op (the egress spoof) sends neither.
|
|
242
253
|
*/
|
|
243
|
-
async function setSlackDmWorkingSigns(
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
254
|
+
async function setSlackDmWorkingSigns(root: { readonly channelId: string; readonly threadTs: string }, status: SurfaceSessionStatus, end: SurfaceTurnEnd | undefined, request: SlackSignRequest): Promise<void> {
|
|
255
|
+
const working = status === "processing";
|
|
256
|
+
if (swallowRoomOp(`a slack DM working sign (${working ? "start" : end?.failed === true ? "failed" : "end"})`)) return;
|
|
257
|
+
await Promise.all([
|
|
258
|
+
setSlackStatusReaction(request, { channel: root.channelId, timestamp: root.threadTs }, working ? "working" : end?.failed === true ? "blocked" : "none"),
|
|
259
|
+
setSlackThreadStatus(request, root, working ? SLACK_WORKING_STATUS : ""),
|
|
260
|
+
]);
|
|
248
261
|
}
|
|
249
262
|
|
|
250
|
-
/**
|
|
263
|
+
/**
|
|
264
|
+
* The Slack setter core's turn-anchor hook calls (registered in register.ts). It sets the agent
|
|
265
|
+
* session's state for a 1:1 DM, and the working signs for every DM-door delivery. It answers the id
|
|
266
|
+
* of the conversation it named: the agent session, else the message the signs ride.
|
|
267
|
+
*/
|
|
251
268
|
export function slackSurfaceSessionSetter(form: SlackFormCall = slackForm): SurfaceSessionSetter {
|
|
252
|
-
|
|
269
|
+
const request = formSignRequest(form);
|
|
270
|
+
return async (attributes, status, end) => {
|
|
253
271
|
const session = slackAgentSessionOf(attributes);
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
272
|
+
const signs = slackWorkingSignsOf(attributes);
|
|
273
|
+
const named = session ?? signs;
|
|
274
|
+
if (!named) return undefined;
|
|
275
|
+
await Promise.all([
|
|
276
|
+
...(session ? [setSlackSessionStatus({ ...session, status }, form)] : []),
|
|
277
|
+
...(signs ? [setSlackDmWorkingSigns(signs, status, end, request)] : []),
|
|
278
|
+
]);
|
|
279
|
+
return `${named.channelId}:${named.threadTs}`;
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* The Slack stepper (OPE-2632): each step of a DM-door turn shows its code-owned label on the status
|
|
285
|
+
* line, or a status verb — the mention door's own `actions.requested` text (slack-working-signs.ts
|
|
286
|
+
* `slackStepStatus`). A delivery with no sign stamp shows nothing here.
|
|
287
|
+
*/
|
|
288
|
+
export function slackSurfaceSessionStepper(form: SlackFormCall = slackForm): SurfaceSessionStepper {
|
|
289
|
+
const request = formSignRequest(form);
|
|
290
|
+
return async (attributes, actions) => {
|
|
291
|
+
const signs = slackWorkingSignsOf(attributes);
|
|
292
|
+
if (!signs) return;
|
|
293
|
+
if (swallowRoomOp("a slack DM step label")) return;
|
|
294
|
+
await setSlackThreadStatus(request, signs, slackStepStatus(actions as readonly StepAction[], { channelId: signs.channelId, teamId: signs.teamId ?? null }));
|
|
258
295
|
};
|
|
259
296
|
}
|
|
260
297
|
|
|
@@ -3533,7 +3533,7 @@ const TRANSCRIPT_TOTAL_CHARS = 4000; // whole-block cap — the wake prompt stay
|
|
|
3533
3533
|
// instead of a hunt through every caller.
|
|
3534
3534
|
//
|
|
3535
3535
|
// 🔴 THE DEFAULT IS RESOLVED, NOT AN ENV READ (OPE-2231). It used to default to
|
|
3536
|
-
//
|
|
3536
|
+
// the SLACK_BOT_TOKEN env var in the parameter list, which a default cannot be any more: the token
|
|
3537
3537
|
// may live in the credential store, so resolving it is async. The resolution therefore moved into the
|
|
3538
3538
|
// body. An explicitly-passed token (the multi-bot seam) still wins untouched; no caller in the tree
|
|
3539
3539
|
// passes one today, which is why this is a default-shape change and not a behaviour change for any
|
|
@@ -1114,9 +1114,9 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
1114
1114
|
// instead, so the resolver may touch the vault. It is awaited ONCE at the top of
|
|
1115
1115
|
// `handleFrontRequest` and the VALUE is used by both readers below (the inbound verify and the
|
|
1116
1116
|
// native forward's re-sign) — one resolution per request, and the two can never disagree about
|
|
1117
|
-
// which secret this request is being held to, which is what the re-sign depends on.
|
|
1118
|
-
//
|
|
1119
|
-
// the
|
|
1117
|
+
// which secret this request is being held to, which is what the re-sign depends on. The
|
|
1118
|
+
// `SLACK_SIGNING_SECRET` operator secret comes first (OPE-2650), and its store lookup is remembered
|
|
1119
|
+
// for thirty seconds, so the steady state costs no store round trip per request.
|
|
1120
1120
|
const resolveSigningSecret = async (): Promise<string | undefined> => opts.signingSecret ?? (await slackSigningSecret());
|
|
1121
1121
|
const markEventSeen = opts.markEventSeen ?? liveMarkEventSeen;
|
|
1122
1122
|
const stampEventVerified = opts.stampEventVerified ?? (() => stampSurfaceEventVerified("slack"));
|
|
@@ -1229,7 +1229,7 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
1229
1229
|
* the SAME seam (OPE-2231: `channels/slack.ts` hands `slackChannel` a `webhookVerifier` built on
|
|
1230
1230
|
* `setup/credentials.ts`), so there is no path on which the two can disagree about which secret
|
|
1231
1231
|
* this request is held to. Before that wiring there WAS one — eve fell back to
|
|
1232
|
-
*
|
|
1232
|
+
* the SLACK_SIGNING_SECRET env var while this route read the credential store — and a
|
|
1233
1233
|
* store-configured deployment 401'd in the inner handler after passing here.
|
|
1234
1234
|
*
|
|
1235
1235
|
* FAIL-SOFT TO TODAY'S BEHAVIOUR. If the body carried nothing to strip (the common path), or the
|
|
@@ -1501,6 +1501,10 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
1501
1501
|
* rides `authoredText` behind a `[voice clip]` marker, and a clip OE could not hear adds a
|
|
1502
1502
|
* host-written line, so neither is the person's typed text. */
|
|
1503
1503
|
agentSessionTypedText?: string,
|
|
1504
|
+
/** OPE-2632: the wake names a real message for the turn's working signs to ride: a `message.im`,
|
|
1505
|
+
* a `message.mpim`, a group-DM mention, or a follow-up press inside a thread (its thread root).
|
|
1506
|
+
* A shortcut and a top-level press leave it false: a minted ts names no message. */
|
|
1507
|
+
showsWorkingSigns = false,
|
|
1504
1508
|
): Promise<void> => {
|
|
1505
1509
|
const opensAgentSession = agentSessionTypedText !== undefined;
|
|
1506
1510
|
// 🔴 THE TENANT FENCE, AND THIS IS ITS CHOKEPOINT FOR THE WHOLE DM FAMILY (OPE-1644). Before
|
|
@@ -1659,6 +1663,8 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
1659
1663
|
...(partnerTrust ? { dmPartnerTrust: partnerTrust } : {}),
|
|
1660
1664
|
// OPE-2519: the 1:1 message's Slack agent session — its thread, or itself when top-level.
|
|
1661
1665
|
...(opensAgentSession && !isGroup ? { agentSessionTs: ev.threadTs ?? ev.ts } : {}),
|
|
1666
|
+
// OPE-2632: the message the turn's 👀, status line and 🛑 ride — the same root, on a 1:1 and a group DM.
|
|
1667
|
+
...(showsWorkingSigns && (ev.threadTs ?? ev.ts) ? { workingSignsTs: ev.threadTs ?? ev.ts } : {}),
|
|
1662
1668
|
// 🔴 THE GROUP DM CARRIES THE PLATFORM'S OWN SHARING VERDICT (OPE-1486), which is what a
|
|
1663
1669
|
// channel wake has always stamped (`authForEvent`) and an mpim wake never did. `bindSlack`
|
|
1664
1670
|
// reads the pair through `slackTrust`, so this is the binding's only route to the fact
|
|
@@ -1980,7 +1986,7 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
1980
1986
|
// A voice clip is heard ONCE, before the retry wrapper, so a strand retry never pays for a second
|
|
1981
1987
|
// transcription (OPE-2524, slack-voice-clips.ts). A DM is always addressed, so its clips are always heard.
|
|
1982
1988
|
const heard = await hearVoice(dev);
|
|
1983
|
-
await deliverWithStrandRetry(() => scheduleDmWake(heard, decision.dmKind, driverAdmitted, decision.dmKind === "im" ? dev.authoredText : undefined));
|
|
1989
|
+
await deliverWithStrandRetry(() => scheduleDmWake(heard, decision.dmKind, driverAdmitted, decision.dmKind === "im" ? dev.authoredText : undefined, true));
|
|
1984
1990
|
console.log(`[open-everywhere:slack-front] ${decision.dmKind} ${dev.channelId}/${dev.ts}: woke — diverted to the DM window seat`);
|
|
1985
1991
|
}),
|
|
1986
1992
|
);
|
|
@@ -2195,7 +2201,7 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
2195
2201
|
fifo.enqueue(dmFifoKey(mev.channelId), async () => {
|
|
2196
2202
|
// A bot's group-DM mention is not heard: `scheduleDmWake` drops a bot author after this step.
|
|
2197
2203
|
const heard = mev.botId ? mev : await hearVoice(mev);
|
|
2198
|
-
await deliverWithStrandRetry(() => scheduleDmWake({ ...heard, channelType: "mpim" }, "mpim"));
|
|
2204
|
+
await deliverWithStrandRetry(() => scheduleDmWake({ ...heard, channelType: "mpim" }, "mpim", false, undefined, true));
|
|
2199
2205
|
}),
|
|
2200
2206
|
);
|
|
2201
2207
|
return OK();
|
|
@@ -2384,7 +2390,8 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
2384
2390
|
// The rewrite removes the buttons and shows what the person sent. The wake takes the SAME road a
|
|
2385
2391
|
// typed DM takes, so the seat, the trust probe, the external-participant policy and the spend
|
|
2386
2392
|
// gate all decide it. The agent session is touched only when the answer sits in a thread: a
|
|
2387
|
-
// top-level answer has no session thread for a minted ts to name.
|
|
2393
|
+
// top-level answer has no session thread for a minted ts to name. The working signs (OPE-2632)
|
|
2394
|
+
// follow the same rule: a threaded press shows them on its thread root, a real message.
|
|
2388
2395
|
args.waitUntil(
|
|
2389
2396
|
markFollowUpPressed(click)
|
|
2390
2397
|
.then((rewritten) => {
|
|
@@ -2394,7 +2401,7 @@ export function buildFrontHandler(innerHandler: RouteHandler, opts: FrontOptions
|
|
|
2394
2401
|
);
|
|
2395
2402
|
args.waitUntil(
|
|
2396
2403
|
fifo.enqueue(dmFifoKey(click.channelId), async () => {
|
|
2397
|
-
await deliverWithStrandRetry(() => scheduleDmWake(sev, "im", false, click.threadTs === undefined ? undefined : click.text));
|
|
2404
|
+
await deliverWithStrandRetry(() => scheduleDmWake(sev, "im", false, click.threadTs === undefined ? undefined : click.text, click.threadTs !== undefined));
|
|
2398
2405
|
console.log(`[open-everywhere:slack-front] follow-up press in ${click.channelId}: woke the DM window`);
|
|
2399
2406
|
}),
|
|
2400
2407
|
);
|
|
@@ -62,6 +62,13 @@ export interface SlackIdentityInput {
|
|
|
62
62
|
* reply still reads `thread_ts`.
|
|
63
63
|
*/
|
|
64
64
|
readonly agentSessionTs?: string;
|
|
65
|
+
/**
|
|
66
|
+
* For a DM-family message (a 1:1 or a group DM) the person sent (OPE-2632): the message the turn's
|
|
67
|
+
* working signs ride — its `thread_ts`, or its own `ts` when top-level, the root the mention door's
|
|
68
|
+
* 👀 uses too. channels/slack-agent-session.ts reads it. A shortcut stamps none: its ts names no
|
|
69
|
+
* message. It changes no reply target.
|
|
70
|
+
*/
|
|
71
|
+
readonly workingSignsTs?: string;
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
/** Build the auth context OpenEverywhere dispatches with — raw facts as string attributes. */
|
|
@@ -79,6 +86,7 @@ export function buildSlackAuth(input: SlackIdentityInput): OpenEverywhereSlackAu
|
|
|
79
86
|
if (typeof input.trustChecked === "boolean") attributes.trust_checked = String(input.trustChecked);
|
|
80
87
|
if (input.dmPartnerTrust) attributes.dm_partner_trust = input.dmPartnerTrust;
|
|
81
88
|
if (input.agentSessionTs) attributes.agent_session_ts = input.agentSessionTs;
|
|
89
|
+
if (input.workingSignsTs) attributes.working_signs_ts = input.workingSignsTs;
|
|
82
90
|
// Root-DM grain stamp (window-tool-gating §D-DM, widened to group DMs): a 1:1 DM OR an mpim (group
|
|
83
91
|
// DM) is a STANDING seat ⇒ narrowed hands — both are closed multi-party conversations with no
|
|
84
92
|
// channel identity. A DM is unambiguously channelType "im" (the DM path always sets it); the
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
// so form is the only safe default. A null or undefined value is dropped; a string, number or
|
|
25
25
|
// boolean is stringified; anything else is JSON-encoded.
|
|
26
26
|
// • Token resolution. An explicit token wins, otherwise the deployment's resolved bot token
|
|
27
|
-
// (`SLACK_BOT_TOKEN
|
|
27
|
+
// (the `SLACK_BOT_TOKEN` operator secret, then the `/setup` value — OPE-2231, OPE-2650). Neither ⇒ throw
|
|
28
28
|
// `SLACK_BOT_TOKEN is required.` — the same message and the same fail-loud posture. ONE
|
|
29
29
|
// DELIBERATE NARROWING: eve's `SlackBotToken` also accepts a function (the secret-manager and
|
|
30
30
|
// rotation form). No caller in this repo passes one, so this seam takes a string. Widening it
|
|
@@ -137,8 +137,8 @@ const PROBE_CAP_MS = 2_000;
|
|
|
137
137
|
*
|
|
138
138
|
* 🔴 ASYNC SINCE OPE-2231, and the awaits are the whole change at the call site. The token used to be
|
|
139
139
|
* a property read; it can now come from the encrypted vault, because `/setup` captures it there. The
|
|
140
|
-
* resolver
|
|
141
|
-
*
|
|
140
|
+
* resolver remembers each store lookup for thirty seconds (the `SLACK_BOT_TOKEN` operator secret and
|
|
141
|
+
* the `/setup` value alike, OPE-2650), so the steady state costs no store round trip per call.
|
|
142
142
|
*/
|
|
143
143
|
async function resolveBotToken(botToken: string | undefined): Promise<string> {
|
|
144
144
|
const token = botToken ?? (await slackBotToken());
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// THE WORKING SIGNS (OPE-2632) — the ONE implementation of what a person sees while the bot works.
|
|
2
|
+
//
|
|
3
|
+
// Two doors start a Slack turn, and before OPE-2632 each drew the signs its own way:
|
|
4
|
+
// • the MENTION DOOR (slack.ts): eve's Slack channel, for a mention in a channel or a thread;
|
|
5
|
+
// • the DM DOOR (slack-agent-session.ts): a 1:1 DM or a group DM, diverted to the DM seat on the
|
|
6
|
+
// `unified` channel, where the Slack channel's own handlers never run. Core's turn-anchor hook
|
|
7
|
+
// drives its signs through the Slack setter and stepper instead.
|
|
8
|
+
// Both now call the functions in this file, so a person sees the same signs at either door:
|
|
9
|
+
// • ONE status reaction on the message: 👀 while the turn works, 🛑 when it fails, and none when it
|
|
10
|
+
// ends (absence is the done signal; ✅ stays reserved for people). See `setSlackStatusReaction`.
|
|
11
|
+
// • the LIVE STATUS LINE (`assistant.threads.setStatus`): `Working...` at the start, then each step's
|
|
12
|
+
// code-owned label ("Searching Notion…", slack-step-label.ts) or a status verb, and cleared at
|
|
13
|
+
// the end. See `slackStepStatus` and `setSlackThreadStatus`.
|
|
14
|
+
// The transport differs (eve's `slack.request` and `startTyping` at the mention door, the host's
|
|
15
|
+
// `slackForm` at the DM door), so each function takes a request seam that never throws.
|
|
16
|
+
|
|
17
|
+
import { getConfig, resolvePartition, STATUS_REACTION, statusVerbsFor, swallowRoomOp, type ReactionState } from "@open-everywhere/core/agent/adapter-api.js";
|
|
18
|
+
import { stepStatusFor, type StepAction } from "./slack-step-label.js";
|
|
19
|
+
|
|
20
|
+
/** One Slack Web API call for a sign. It must never throw: a sign is best-effort and never fails a turn. */
|
|
21
|
+
export type SlackSignRequest = (method: string, params: Readonly<Record<string, string>>) => Promise<void>;
|
|
22
|
+
|
|
23
|
+
/** The status line at a turn's start, at both doors. */
|
|
24
|
+
export const SLACK_WORKING_STATUS = "Working...";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Set the ONE status reaction on a message: add `state`'s emoji and remove every other status emoji.
|
|
28
|
+
* `"none"` removes them all. The calls run in parallel, because a DM turn waits on this. A swallowed
|
|
29
|
+
* room op (the egress spoof, core channels/egress-spoof.ts) sends nothing.
|
|
30
|
+
*/
|
|
31
|
+
export async function setSlackStatusReaction(request: SlackSignRequest, target: { readonly channel: string; readonly timestamp: string }, state: ReactionState | "none"): Promise<void> {
|
|
32
|
+
if (swallowRoomOp("a slack status reaction")) return;
|
|
33
|
+
await Promise.all(
|
|
34
|
+
Object.entries(STATUS_REACTION).map(([s, name]) => request(s === state ? "reactions.add" : "reactions.remove", { channel: target.channel, name, timestamp: target.timestamp })),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Set the live status line on a thread. An empty `status` clears it. */
|
|
39
|
+
export async function setSlackThreadStatus(request: SlackSignRequest, target: { readonly channelId: string; readonly threadTs: string }, status: string): Promise<void> {
|
|
40
|
+
await request("assistant.threads.setStatus", { channel_id: target.channelId, thread_ts: target.threadTs, status });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- status verbs (the fun bit) ----------------------------------------------
|
|
44
|
+
// "Tinkering…" / "Untangling…" instead of a flat "Thinking…"/"Working…" — a random
|
|
45
|
+
// pick per status update from the channel's effective vocabulary (framework defaults
|
|
46
|
+
// ← org ← role ← domain, extend-or-replace; memory/domain.ts statusVerbsFor — a
|
|
47
|
+
// domain's ROLE preset contributes its themed verbs as its own rung). Orgs add house
|
|
48
|
+
// verbs, a team adds its in-jokes. Cosmetic only, so ANY failure falls back to a
|
|
49
|
+
// plain verb — the status line must never take a turn down.
|
|
50
|
+
export function slackStatusVerb(state: { channelId?: string | null; teamId?: string | null }): string {
|
|
51
|
+
try {
|
|
52
|
+
const p = resolvePartition({
|
|
53
|
+
channel: { kind: "slack" },
|
|
54
|
+
session: { auth: { current: { attributes: { team_id: state.teamId ?? "", channel_id: state.channelId ?? "" }, principalId: "", principalType: "slack-user" } } },
|
|
55
|
+
});
|
|
56
|
+
const cfg = getConfig();
|
|
57
|
+
const verbs = statusVerbsFor(p.surfaceKey, cfg.loadDomains(), cfg.loadRoles?.() ?? []);
|
|
58
|
+
return `${verbs[Math.floor(Math.random() * verbs.length)]}…`;
|
|
59
|
+
} catch {
|
|
60
|
+
return "Working…";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The status line for one step: the first labelled action's code-owned label (OPE-2449), else a
|
|
66
|
+
* status verb. Never the model's own text: slack-step-label.ts reads only the tool's name and, for a
|
|
67
|
+
* connector, the vendor through its own table. `actions` undefined (a reasoning refresh) shows a verb.
|
|
68
|
+
*/
|
|
69
|
+
export function slackStepStatus(actions: readonly StepAction[] | undefined, state: { channelId?: string | null; teamId?: string | null }): string {
|
|
70
|
+
return stepStatusFor(actions) ?? slackStatusVerb(state);
|
|
71
|
+
}
|