@stratusagent/channel-slack 0.11.9 → 0.12.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/README.md CHANGED
@@ -4,7 +4,7 @@ Slack adapter for Stratus agents. **One Slack app per agent** — Slack has no w
4
4
 
5
5
  - **Resumable conversations**: session keys are `slack:<agent>:<team>:<channel>:<thread_ts ?? ts>` (DMs: the DM channel id) — a thread is a conversation, it survives daemon restarts, and two agents sharing a thread keep fully separate sessions. A turn parked on a human when the daemon died is re-asked after the restart, and a turn that was still running is continued from where it got to (unless the agent runs on a harness — `codex` or a Claude subscription — where it is failed instead, since the harness may already have acted on it); either way, when it finishes its reply is posted into the thread as a fresh message — the placeholder it would have edited belonged to the old process.
6
6
  - **Replies post when they are ready**: by default each message is posted once, finished: what the agent writes before starting a tool is posted as the tool starts, and the reply follows, with Slack's loading status (`assistant.threads.setStatus` — "is thinking…", "is running shell.run…") shown while the turn works, so the notification carries the answer. The `stream` reply mode (`slack.replies` in the [config](../../docs/guides/slack.md#how-replies-appear)) keeps the placeholder-and-edit rendering instead: post `…`, edit as deltas arrive (throttled for `chat.update` limits), show `⚙ tool…` status lines, finalize with the full reply. Either way a reply that outgrows one message is split across several.
7
- - **Markdown is translated to Slack's mrkdwn** on the way out — `**bold**` to `*bold*`, `*italic*` to `_italic_`, `~~struck~~` to `~struck~`, `[text](https://…)` to `<https://…|text>`, and a `#` heading to a bold line, since mrkdwn has no headings. The reply is read once into tokens and written back from those, so what counts as code is decided one time rather than by each rule in turn. Three things follow from that. **A code run is one token**: nothing reads or rewrites inside it, an unclosed fence included, and the newlines in a fence are not line breaks — so a `#` on a line of somebody's shell script is not a heading. A run of N backticks opens code and only a run of exactly N closes it, which is what lets ``a span with a ` in it`` and a ````-fence holding a ```-fence work; one or two unmatched backticks are text, as they are in Markdown, but a pair is a span however far apart, because Slack pairs them too. **A delimiter is matched to its partner**, across whatever sits between — ``**the `fs.read` tool**`` is bold around a snippet, not four literal asterisks — though never across a line, so a marker still being streamed stays literal until its closing half arrives. A run is spent rather than matched whole, so the three asterisks ending `**bold and *italic***` close the italic with one and the bold with two; what no pair spends stays on the line as text, and what a run cannot buy — the third tilde of `~~~struck~~~`, a style that does not exist — is never charged for. **A wrapper is only written where its own character is not already loose** in what it wraps: Slack has one delimiter per style and no way to nest it, so a heading that already contains bold keeps the bold it has rather than gaining a second pair that would print instead of render, and `# glob *.ts` keeps an asterisk that was never a marker. A heading sheds its hashes and its trailing spaces from the prose that holds them, never from a snippet. Anything whose spelling already matches — lists, block quotes, inline code — is untouched. **A pipe table**, which mrkdwn has no syntax for, becomes a code block with its columns padded to line up (alignment colons respected, `**`/`` ` `` markers dropped from cells, since nothing renders inside code); a grid wider than 60 characters — past what a phone shows unwrapped — or one with a link in it (a link inside a code block is not clickable) becomes one line per row, `*Header*: value · *Header*: value`. A one-column table has nothing to line up and becomes a bulleted list under its bold header. A table inside a fence is somebody's text and stays as written, and a grid longer than one message is split like any code block, closed at the cut and reopened in the next, with its header and rule repeated so every part names its columns.
7
+ - **Markdown is translated to Slack's mrkdwn** on the way out — `**bold**` to `*bold*`, `*italic*` to `_italic_`, `~~struck~~` to `~struck~`, `[text](https://…)` to `<https://…|text>`, and a `#` heading to a bold line, since mrkdwn has no headings. A bare address is sent as `<https://…>`, Slack's own link markup, so emphasis around it (`**https://…**`) cannot be read as part of it, and a `*`, `_`, or `~` inside one is part of the address rather than a marker. Trailing punctuation (`.`, `,`, `**`) and a `)` the address never opened belong to the sentence, as in GFM autolinks, so `…/wiki/Function_(mathematics)` stays whole. A bare address is sent as `<https://…>`, Slack's own link markup, so emphasis around it (`**https://…**`) cannot be read as part of it, and the `*`, `_`, or `~` inside one is part of the address rather than a marker; trailing punctuation (`.`, `,`, `**`) is the sentence's, as in GFM autolinks. The reply is read once into tokens and written back from those, so what counts as code is decided one time rather than by each rule in turn. Three things follow from that. **A code run is one token**: nothing reads or rewrites inside it, an unclosed fence included, and the newlines in a fence are not line breaks — so a `#` on a line of somebody's shell script is not a heading. A run of N backticks opens code and only a run of exactly N closes it, which is what lets ``a span with a ` in it`` and a ````-fence holding a ```-fence work; one or two unmatched backticks are text, as they are in Markdown, but a pair is a span however far apart, because Slack pairs them too. **A delimiter is matched to its partner**, across whatever sits between — ``**the `fs.read` tool**`` is bold around a snippet, not four literal asterisks — though never across a line, so a marker still being streamed stays literal until its closing half arrives. A run is spent rather than matched whole, so the three asterisks ending `**bold and *italic***` close the italic with one and the bold with two; what no pair spends stays on the line as text, and what a run cannot buy — the third tilde of `~~~struck~~~`, a style that does not exist — is never charged for. **A wrapper is only written where its own character is not already loose** in what it wraps: Slack has one delimiter per style and no way to nest it, so a heading that already contains bold keeps the bold it has rather than gaining a second pair that would print instead of render, and `# glob *.ts` keeps an asterisk that was never a marker. A heading sheds its hashes and its trailing spaces from the prose that holds them, never from a snippet. Anything whose spelling already matches — lists, block quotes, inline code — is untouched. **A pipe table**, which mrkdwn has no syntax for, becomes a code block with its columns padded to line up (alignment colons respected, `**`/`` ` `` markers dropped from cells, since nothing renders inside code); a grid wider than 60 characters — past what a phone shows unwrapped — or one with a link in it (a link inside a code block is not clickable) becomes one line per row, `*Header*: value · *Header*: value`. A one-column table has nothing to line up and becomes a bulleted list under its bold header. A table inside a fence is somebody's text and stays as written, and a grid longer than one message is split like any code block, closed at the cut and reopened in the next, with its header and rule repeated so every part names its columns.
8
8
  - **Mention to start, then it stays in the thread.** A mention opens a conversation; every reply in that thread reaches the agent without tagging it again, and free-form DMs are unchanged. A mention partway into an existing thread opens the conversation with what was said before it: the thread's parent and its newest 39 earlier messages. A mention at the top of a channel opens with the channel's 20 most recent messages instead, which is where a channel per client or per project keeps its context. Either way each message keeps its own speaker and its own trust, so a principal's mention does not carry a stranger's earlier words in under the principal's label (other bots count as `unknown`, and under `admit: "principals"` unlisted senders are left out). This needs the history scopes below; without them the agent sees only the message that named it, and the adapter logs a warning. Who an untagged reply is for is decided by [Who a message is for](#who-a-message-is-for). Inbound mentions of other users are humanized to `@Display Name` for the model; redeliveries are deduped so a slow turn never runs twice — in memory while the process remembers them, and after a restart by the gateway: each message is dispatched with its own key (`idempotencyKey`), a repeat runs no second turn, and the adapter posts that message's own reply and files (a file changed since that turn is skipped, since the path may now hold another file), or its failure if the turn failed — once more, if the process died after posting it, which beats a reply never posted.
9
9
  - **Approval buttons** for the gateway's `remote` permission mode: a gated tool call parks the turn and asks in its thread with **Allow once** / **Always allow** / **Deny**. See [Approving tool calls](#approving-tool-calls) — clicks are authorized by *who clicked*, never by who can see the message, and **Always allow** is offered only where the daemon would actually remember it.
10
10
  - **Tokens are gateway infrastructure secrets**, stored in the `channels` namespace of `~/.stratus/credentials.json` — never in an agent's credential allowlist:
package/dist/index.d.ts CHANGED
@@ -45,6 +45,12 @@ export interface SlackAgentConfig {
45
45
  * a `…` placeholder at once and edits it as the reply is written.
46
46
  */
47
47
  replies?: 'final' | 'stream';
48
+ /**
49
+ * `false` posts this agent's messages without link and media previews
50
+ * (Slack's `unfurl_links`/`unfurl_media`). Absent or `true` leaves
51
+ * Slack's default, which previews them.
52
+ */
53
+ linkPreviews?: boolean;
48
54
  /**
49
55
  * Channel ids where a new top-level message from an admitted sender is
50
56
  * this agent's to answer without a mention: the channel people come to
@@ -244,6 +250,9 @@ export interface SlackWebLike {
244
250
  test(): Promise<{
245
251
  user_id?: string;
246
252
  team_id?: string;
253
+ response_metadata?: {
254
+ scopes?: string[];
255
+ };
247
256
  }>;
248
257
  };
249
258
  chat: {
@@ -252,6 +261,8 @@ export interface SlackWebLike {
252
261
  text: string;
253
262
  thread_ts?: string;
254
263
  blocks?: SlackBlock[];
264
+ unfurl_links?: boolean;
265
+ unfurl_media?: boolean;
255
266
  }): Promise<{
256
267
  ts?: string;
257
268
  channel?: string;
@@ -413,6 +424,13 @@ export interface SlackAdapterOptions {
413
424
  */
414
425
  now?: () => number;
415
426
  }
427
+ /**
428
+ * A client whose every post goes out without link or media previews, for
429
+ * an agent configured with `linkPreviews: false`. Applied once around the
430
+ * client, so no post site can forget it. `chat.update` takes no unfurl
431
+ * flags; an edited placeholder keeps the post's own setting.
432
+ */
433
+ export declare const withoutLinkPreviews: (web: SlackWebLike) => SlackWebLike;
416
434
  /**
417
435
  * The default fetcher over a `fetch`. Exported as a factory so the bounded
418
436
  * read can be tested against a fake `fetch` with a body that never ends;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
2
2
  import { lstat, open, readFile } from 'node:fs/promises';
3
3
  import { createRequire } from 'node:module';
4
4
  import path from 'node:path';
5
- import { filePathsOf, isLatestTurn, latestTurnReply, MAX_IDEMPOTENCY_KEY_LENGTH, turnFailureFor, turnFilesFor, turnReplyFor, CONVERSATION_METADATA_KEY, SENDER_TRUST_METADATA_KEY, sessionWriteTrust, imageDimensions, isImageAttachmentMediaType, IMAGE_ATTACHMENT_MAX_BYTES, IMAGE_ATTACHMENT_MAX_DIMENSION, IMAGE_ATTACHMENTS_MAX_TOTAL_BYTES, escapeControlCharacters, } from '@stratusagent/core';
5
+ import { filePathsOf, isLatestTurn, latestTurnReply, MAX_IDEMPOTENCY_KEY_LENGTH, turnFailureFor, turnFilesFor, turnReplyFor, CONVERSATION_METADATA_KEY, SENDER_TRUST_METADATA_KEY, sessionWriteTrust, imageDimensions, isImageAttachmentMediaType, IMAGE_ATTACHMENT_MAX_BYTES, IMAGE_ATTACHMENT_MAX_DIMENSION, IMAGE_ATTACHMENTS_MAX_TOTAL_BYTES, escapeControlCharacters, leastTrusted, sessionTrustOf, SESSION_TRUST_METADATA_KEY, } from '@stratusagent/core';
6
6
  import { admitsSender, channelSessionKey, senderTrustFor, } from '@stratusagent/channels';
7
7
  import { codeRunsOf, toSlackMrkdwn } from "./mrkdwn.js";
8
8
  /**
@@ -107,6 +107,52 @@ const defaultSocketClient = (appToken) => {
107
107
  const { SocketModeClient } = requireModule('@slack/socket-mode');
108
108
  return new SocketModeClient({ appToken });
109
109
  };
110
+ /**
111
+ * Scopes the manifest asks for. An app created before they shipped has
112
+ * fewer, and some features silently break — threads are answered without
113
+ * their context, images arrive as names. Checked once at connect, so the
114
+ * operator finds out at start, not at the first broken thread.
115
+ *
116
+ * Events (`message.channels`, `message.groups`, …) cannot be read from a
117
+ * bot token, so the warning says they are needed too.
118
+ */
119
+ const REQUIRED_SCOPES = [
120
+ 'app_mentions:read',
121
+ 'channels:history', 'channels:read',
122
+ 'chat:write',
123
+ 'files:read', 'files:write',
124
+ 'groups:history', 'groups:read',
125
+ 'im:history', 'im:read', 'im:write',
126
+ 'mpim:history', 'mpim:read',
127
+ 'users:read',
128
+ ];
129
+ const checkBotScopes = (agentId, granted, warn) => {
130
+ const have = new Set(granted);
131
+ const missing = REQUIRED_SCOPES.filter((scope) => !have.has(scope));
132
+ if (missing.length === 0) {
133
+ return;
134
+ }
135
+ warn(`slack: ${agentId}'s app is missing bot scopes: ${missing.join(', ')}. `
136
+ + 'Add them under OAuth & Permissions, add the matching message.* events '
137
+ + 'under Event Subscriptions, and reinstall the app to the workspace. '
138
+ + 'Without them, some features (thread follow-through, image viewing, '
139
+ + 'channel history) will silently fail.');
140
+ };
141
+ /**
142
+ * A client whose every post goes out without link or media previews, for
143
+ * an agent configured with `linkPreviews: false`. Applied once around the
144
+ * client, so no post site can forget it. `chat.update` takes no unfurl
145
+ * flags; an edited placeholder keeps the post's own setting.
146
+ */
147
+ export const withoutLinkPreviews = (web) => {
148
+ // Delegating objects, not copies: everything else on the client (and a
149
+ // method replaced on it later) is still reached through the original.
150
+ const chat = Object.create(web.chat);
151
+ chat.postMessage = (args) => web.chat.postMessage({ ...args, unfurl_links: false, unfurl_media: false });
152
+ const wrapped = Object.create(web);
153
+ wrapped.chat = chat;
154
+ return wrapped;
155
+ };
110
156
  const defaultWebClient = (botToken) => {
111
157
  const { WebClient } = requireModule('@slack/web-api');
112
158
  return new WebClient(botToken);
@@ -283,8 +329,16 @@ class ReplyRenderer {
283
329
  lastInterim;
284
330
  /** Whether any interim message landed — something said, whatever the reply's fate. */
285
331
  interimPosted = false;
286
- /** Every interim message that landed, in order, for the thread's other agents to hear. */
332
+ /** Every interim message that landed whole, in order: what a hosted-loop reply is stripped of. */
287
333
  interimSaid = [];
334
+ /**
335
+ * Told of each interim message as it is queued — before the reply, and
336
+ * before anything typed while its tool runs — with what of it landed,
337
+ * once that is known, so the thread's other agents hear it in its place.
338
+ */
339
+ onInterim;
340
+ /** The lowest trust this turn's session was lowered to while it ran (`session.tainted`). */
341
+ taintedTo;
288
342
  turnBreakPending = false;
289
343
  toolLine;
290
344
  ref;
@@ -407,6 +461,7 @@ class ReplyRenderer {
407
461
  this.statusWarned = options.statusWarned ?? { value: false };
408
462
  this.after = options.after ?? Promise.resolve();
409
463
  this.dmWith = options.dmWith;
464
+ this.onInterim = options.onInterim;
410
465
  }
411
466
  /**
412
467
  * Show Slack's loading status for this turn, and keep it up until the
@@ -525,6 +580,12 @@ class ReplyRenderer {
525
580
  if (this.finalized) {
526
581
  return;
527
582
  }
583
+ if (event.type === 'session.tainted') {
584
+ if (ofThisTurn && this.turnStarted) {
585
+ this.taintedTo = this.taintedTo === undefined ? event.trust : leastTrusted(this.taintedTo, event.trust);
586
+ }
587
+ return;
588
+ }
528
589
  if (event.type === 'provider.delta' && event.delta.type === 'text') {
529
590
  // Text from a new provider turn (after tools ran) must not fuse
530
591
  // with the previous turn's — "I'll check.The result is…" reads as
@@ -688,26 +749,49 @@ class ReplyRenderer {
688
749
  }
689
750
  const handover = this.handover;
690
751
  const after = this.after;
752
+ let heard = '';
753
+ let settleLanded;
754
+ const landedNow = new Promise((resolve) => {
755
+ settleLanded = resolve;
756
+ });
757
+ this.onInterim?.(() => heard, landedNow);
691
758
  this.uploadChain = this.uploadChain
692
759
  .then(() => handover)
693
760
  .then(() => after)
694
761
  .then(async () => {
695
- for (const chunk of messageChunks(text)) {
696
- const posted = await this.web.chat.postMessage({
697
- channel: this.channel,
698
- text: chunk,
699
- ...(this.threadTs ? { thread_ts: this.threadTs } : {}),
700
- });
701
- this.placeText(posted.ts);
762
+ const landed = [];
763
+ try {
764
+ for (const chunk of messageChunks(text)) {
765
+ const posted = await this.web.chat.postMessage({
766
+ channel: this.channel,
767
+ text: chunk,
768
+ ...(this.threadTs ? { thread_ts: this.threadTs } : {}),
769
+ });
770
+ this.placeText(posted.ts);
771
+ landed.push(chunk);
772
+ }
773
+ }
774
+ finally {
775
+ // Whatever landed was said, whole or not: it counts as speaking,
776
+ // and the others hear it. Only a message that landed whole is
777
+ // stripped from a hosted-loop reply; part of one is left in the
778
+ // reply, where repeating a few lines beats losing the rest.
779
+ if (landed.length > 0) {
780
+ this.interimPosted = true;
781
+ heard = landed.length === messageChunks(text).length ? text : landed.join('\n\n');
782
+ }
783
+ settleLanded(landed.length > 0);
702
784
  }
703
- this.interimPosted = true;
704
785
  this.lastInterim = text;
705
786
  this.interimSaid.push(text);
706
787
  // A post of the app's in this thread takes the status down while
707
788
  // the turn is still working; put it back.
708
789
  this.refreshLoading();
709
790
  })
710
- .catch((error) => this.warn(`chat.postMessage failed for an interim message: ${error instanceof Error ? error.message : String(error)}`));
791
+ .catch((error) => this.warn(`chat.postMessage failed for an interim message: ${error instanceof Error ? error.message : String(error)}`))
792
+ // Settled whatever happened above, or the other agents' intake would
793
+ // wait on it for good. A second settle is a no-op.
794
+ .finally(() => settleLanded(false));
711
795
  }
712
796
  /**
713
797
  * Everything this turn said in the thread, for its other agents to hear:
@@ -715,8 +799,7 @@ class ReplyRenderer {
715
799
  * repeats the last of them. Read once the turn's posts have landed.
716
800
  */
717
801
  spokenText(reply) {
718
- const rest = this.unsaid(reply);
719
- return [...this.interimSaid, ...(rest.trim().length > 0 ? [rest] : [])].join('\n\n');
802
+ return this.unsaid(reply);
720
803
  }
721
804
  /**
722
805
  * The part of a reply not already posted as interim messages. A provider
@@ -727,11 +810,19 @@ class ReplyRenderer {
727
810
  */
728
811
  unsaid(reply) {
729
812
  let rest = reply.trim();
813
+ // Whole messages only, at the separator the codex provider joins them
814
+ // with: a reply of its own that merely begins with the same words
815
+ // ("Sure" then "Sure, the build passes.") is not cut.
730
816
  for (const said of this.interimSaid) {
731
- if (!rest.startsWith(said)) {
817
+ if (rest === said) {
818
+ rest = '';
819
+ }
820
+ else if (rest.startsWith(`${said}\n\n`)) {
821
+ rest = rest.slice(said.length + 2).trimStart();
822
+ }
823
+ else {
732
824
  break;
733
825
  }
734
- rest = rest.slice(said.length).trimStart();
735
826
  }
736
827
  return rest === this.lastInterim ? '' : rest;
737
828
  }
@@ -1159,6 +1250,31 @@ const pastCodePoint = (text, index) => {
1159
1250
  const code = text.charCodeAt(index - 1);
1160
1251
  return code >= 0xd800 && code <= 0xdbff ? index + 1 : index;
1161
1252
  };
1253
+ /**
1254
+ * `cut` moved in front of any Slack markup it would land inside — a link
1255
+ * `<https://…>` or `<https://…|label>`, a mention `<@U…>` — when that still
1256
+ * leaves half a message. Cut through, the first message ends on an
1257
+ * unterminated `<https://…` and the next starts with the rest of the
1258
+ * address and a stray `>`, and neither half is a link. Markup is one line,
1259
+ * so only the cut's own line is looked at.
1260
+ */
1261
+ const outsideSlackMarkup = (text, cut) => {
1262
+ const open = text.lastIndexOf('<', cut - 1);
1263
+ if (open <= SLACK_MAX_MESSAGE_CHARS / 2) {
1264
+ return cut;
1265
+ }
1266
+ const closedBefore = text.lastIndexOf('>', cut - 1);
1267
+ const lineBreak = text.lastIndexOf('\n', cut - 1);
1268
+ if (closedBefore > open || lineBreak > open) {
1269
+ return cut;
1270
+ }
1271
+ const close = text.indexOf('>', cut);
1272
+ const nextLine = text.indexOf('\n', cut);
1273
+ if (close === -1 || (nextLine !== -1 && nextLine < close)) {
1274
+ return cut;
1275
+ }
1276
+ return open;
1277
+ };
1162
1278
  const truncateForSlack = (text) => text.length <= SLACK_MAX_MESSAGE_CHARS
1163
1279
  ? text
1164
1280
  : `${text.slice(0, safeCutIndex(text, SLACK_MAX_MESSAGE_CHARS - 1))}…`;
@@ -1215,8 +1331,9 @@ const splitForSlack = (text) => {
1215
1331
  // point, or the step that guarantees progress lands inside an emoji.
1216
1332
  const fence = run !== undefined && run.closer.startsWith('\n');
1217
1333
  if (run === undefined || !fence || reopen === undefined || reopen.length + run.closer.length > SLACK_MAX_MESSAGE_CHARS / 4 || floor + 2 > budget) {
1218
- chunks.push(rest.slice(0, cut));
1219
- rest = rest.slice(cut).replace(/^\n+/, '');
1334
+ const at = run === undefined ? outsideSlackMarkup(rest, cut) : cut;
1335
+ chunks.push(rest.slice(0, at));
1336
+ rest = rest.slice(at).replace(/^\n+/, '');
1220
1337
  continue;
1221
1338
  }
1222
1339
  const inner = rest.lastIndexOf('\n', budget - 1);
@@ -2592,7 +2709,6 @@ export const createSlackChannelAdapter = (options) => {
2592
2709
  slackChannel: channel,
2593
2710
  slackUser: speaker.botUserId,
2594
2711
  slackThread: thread,
2595
- [SENDER_TRUST_METADATA_KEY]: sessionWriteTrust(spoken),
2596
2712
  };
2597
2713
  // In the reply's own workspace only, because a session key carries a
2598
2714
  // team — the same reason `resolveFollowUpWinner` stays in-workspace.
@@ -2616,6 +2732,7 @@ export const createSlackChannelAdapter = (options) => {
2616
2732
  if (text.trim().length === 0) {
2617
2733
  return { observed: undefined };
2618
2734
  }
2735
+ const trust = sessionWriteTrust(typeof spoken === 'function' ? await spoken() : spoken);
2619
2736
  let member;
2620
2737
  try {
2621
2738
  member = (await hearer.web.conversations.info({ channel })).channel?.is_member;
@@ -2630,7 +2747,7 @@ export const createSlackChannelAdapter = (options) => {
2630
2747
  // spoke, by display name. A bot user's is the app's own, set by
2631
2748
  // whoever installed it.
2632
2749
  const author = await displayNameFor(hearer, speaker.botUserId);
2633
- const observed = gateway.observe?.({ sessionId, agentId: hearer.config.agentId, message: `${author}: ${text}`, metadata });
2750
+ const observed = gateway.observe?.({ sessionId, agentId: hearer.config.agentId, message: `${author}: ${text}`, metadata: { ...metadata, [SENDER_TRUST_METADATA_KEY]: trust } });
2634
2751
  return { observed: observed === undefined ? undefined : placeObserve(sessionId, observed) };
2635
2752
  });
2636
2753
  await observed;
@@ -2682,12 +2799,15 @@ export const createSlackChannelAdapter = (options) => {
2682
2799
  }
2683
2800
  const channelId = conversation.id;
2684
2801
  const web = connection.web;
2802
+ // A reply under a message rather than a post beside it. Every chunk of
2803
+ // a long one goes to the same thread, so a split reply stays together.
2804
+ const thread = address.thread?.trim() || undefined;
2685
2805
  return {
2686
2806
  async post(text) {
2687
2807
  const chunks = messageChunks(text.trim().length > 0 ? text : '(empty message)');
2688
2808
  let first;
2689
2809
  for (const chunk of chunks) {
2690
- const posted = await web.chat.postMessage({ channel: channelId, text: chunk });
2810
+ const posted = await web.chat.postMessage({ channel: channelId, text: chunk, ...(thread ? { thread_ts: thread } : {}) });
2691
2811
  if (!first) {
2692
2812
  first = { channel: posted.channel ?? channelId, ts: posted.ts ?? '' };
2693
2813
  }
@@ -2848,7 +2968,16 @@ export const createSlackChannelAdapter = (options) => {
2848
2968
  if (!gateway) {
2849
2969
  return;
2850
2970
  }
2851
- const connection = connectionFor(event.agentId);
2971
+ // A delegated call is asked where the work was asked for: the
2972
+ // delegating conversation, through the app and approvers of the agent
2973
+ // at the root of the chain. The agent doing the work may have no
2974
+ // connection here at all, and its sub-session has no conversation of
2975
+ // its own — asked the old way, nobody saw it and the turn sat out the
2976
+ // whole timeout. The gateway makes such a call one-shot, so routing it
2977
+ // through another agent's approvers never grants anything standing.
2978
+ const origin = event.delegatedFrom;
2979
+ const viaOrigin = origin ? connectionFor(origin.agentId) : undefined;
2980
+ const connection = viaOrigin ?? connectionFor(event.agentId);
2852
2981
  if (!connection) {
2853
2982
  // Two different situations, and only one of them is this adapter's
2854
2983
  // to answer.
@@ -2863,14 +2992,14 @@ export const createSlackChannelAdapter = (options) => {
2863
2992
  // agent — denying here would let Slack refuse a question somebody
2864
2993
  // else was going to ask. `stratus serve` reports agents that no
2865
2994
  // channel can ask for, at startup, where the whole roster is visible.
2866
- if (configuredAgents.has(event.agentId)) {
2867
- warn(`slack: ${event.agentId} has no live connection; denying ${event.call.toolName}`);
2995
+ if (configuredAgents.has(event.agentId) || (origin && configuredAgents.has(origin.agentId))) {
2996
+ warn(`slack: ${origin?.agentId ?? event.agentId} has no live connection; denying ${event.call.toolName}`);
2868
2997
  gateway.resolveApproval({ requestId: event.requestId, answer: 'deny', reason: 'undeliverable' });
2869
2998
  }
2870
2999
  return;
2871
3000
  }
2872
3001
  const approvers = new Set((connection.config.approvers ?? []).filter((id) => id.length > 0));
2873
- const metadata = event.metadata ?? {};
3002
+ const metadata = (viaOrigin ? origin?.metadata : event.metadata) ?? {};
2874
3003
  const sameChannel = metadata.channel === 'slack' && typeof metadata.slackChannel === 'string'
2875
3004
  ? metadata.slackChannel
2876
3005
  : undefined;
@@ -2884,14 +3013,19 @@ export const createSlackChannelAdapter = (options) => {
2884
3013
  gateway.resolveApproval({ requestId: event.requestId, answer: 'deny', reason: 'undeliverable' });
2885
3014
  };
2886
3015
  if (approvers.size === 0) {
2887
- decline(`no approvers configured for ${event.agentId}`);
3016
+ decline(`no approvers configured for ${connection.config.agentId}`);
2888
3017
  return;
2889
3018
  }
2890
3019
  if (!channel) {
2891
- decline(`no conversation to ask in for ${event.agentId}`);
3020
+ decline(`no conversation to ask in for ${connection.config.agentId}`);
2892
3021
  return;
2893
3022
  }
2894
- const agentName = gateway.agents().find((agent) => agent.id === event.agentId)?.name ?? event.agentId;
3023
+ const nameOf = (agentId) => gateway.agents().find((agent) => agent.id === agentId)?.name ?? agentId;
3024
+ // "Quinn (for Nova)": the approver is being asked about Quinn's call,
3025
+ // by Nova's app, in Nova's conversation, and should be told both.
3026
+ const agentName = viaOrigin && origin && origin.agentId !== event.agentId
3027
+ ? `${nameOf(event.agentId)} (for ${nameOf(origin.agentId)})`
3028
+ : nameOf(event.agentId);
2895
3029
  // The notification preview is all some approvers see before deciding
2896
3030
  // whether to open the thread, so it carries the arguments too — just
2897
3031
  // the short form.
@@ -4298,7 +4432,7 @@ export const createSlackChannelAdapter = (options) => {
4298
4432
  : {
4299
4433
  ...room,
4300
4434
  ...(room.kind === 'direct' && senderTrust === 'user' ? { with: author } : {}),
4301
- ...(thread !== undefined && room.kind !== 'direct' ? { thread: true } : {}),
4435
+ ...(thread !== undefined && room.kind !== 'direct' ? { thread: true, threadRoot: thread } : {}),
4302
4436
  // People outside this workspace read a Slack Connect channel, and
4303
4437
  // the event says so itself, even when the lookup failed.
4304
4438
  ...(room.kind !== 'direct' && args.body?.is_ext_shared_channel === true ? { shared: true } : {}),
@@ -4373,6 +4507,25 @@ export const createSlackChannelAdapter = (options) => {
4373
4507
  statusWarned: connection.statusWarned,
4374
4508
  ...(ahead ? { after: ahead } : {}),
4375
4509
  ...(isDm ? { dmWith: userId } : {}),
4510
+ // Each interim message, as it lands, takes its place in the
4511
+ // thread's other agents' sessions — ahead of a follow-up typed
4512
+ // while the tool it preceded is still running. Under the session's
4513
+ // label as stored, lowered by anything that tainted it during this
4514
+ // turn, and the least trusted label where it cannot be read.
4515
+ ...(thread !== undefined
4516
+ ? {
4517
+ onInterim: (heard, landed) => {
4518
+ // Tracked, so a shutdown drains it like any other reaction:
4519
+ // the text is already in the thread, whatever the turn does.
4520
+ track(overhearReply(connection, event.channel, thread, heard, async () => {
4521
+ const routed = await gateway.sessionRouting?.(sessionId).catch(() => undefined);
4522
+ const stored = routed ? sessionTrustOf(routed) : 'external';
4523
+ const live = renderer.taintedTo === undefined ? stored : leastTrusted(stored, renderer.taintedTo);
4524
+ return { metadata: { [SESSION_TRUST_METADATA_KEY]: live } };
4525
+ }, landed));
4526
+ },
4527
+ }
4528
+ : {}),
4376
4529
  });
4377
4530
  if (!streaming) {
4378
4531
  replyOrder.set(sessionId, renderer.posted);
@@ -4565,15 +4718,14 @@ export const createSlackChannelAdapter = (options) => {
4565
4718
  await heard;
4566
4719
  }
4567
4720
  else {
4568
- const { spoke, spokeAt } = await renderer.fail(failure instanceof Error ? failure.message : String(failure));
4569
- // What the turn said before it broke is in the thread, so the
4570
- // others hear it; the error note is the failure's, and is not. Under
4571
- // the session's own label where it can be read, and the least
4572
- // trusted one where it cannot.
4573
- if (thread !== undefined && renderer.spokenText('').length > 0) {
4574
- const routed = await gateway.sessionRouting?.(sessionId).catch(() => undefined);
4575
- await overhearReply(connection, event.channel, thread, renderer.spokenText(''), routed ?? { metadata: {} }, Promise.resolve(true));
4576
- }
4721
+ // A paused daemon's refusal is its answer, said as it is: the
4722
+ // operator wrote that sentence for the person reading it, and it is
4723
+ // not a failure of the agent. Matched by name, as this package does
4724
+ // not import the gateway. A turn nobody asked for stays silent.
4725
+ const pausedReply = failure instanceof Error && failure.name === 'IntakePausedError';
4726
+ const { spoke, spokeAt } = pausedReply
4727
+ ? await renderer.finalize(renderer.lazy ? '' : failure.message)
4728
+ : await renderer.fail(failure instanceof Error ? failure.message : String(failure));
4577
4729
  if (spoke && threadKey !== undefined) {
4578
4730
  // A file it posted before breaking is still the last thing said.
4579
4731
  rememberAddressee(threadKey, connection.config.agentId, spokeAt ?? event.ts);
@@ -4715,10 +4867,12 @@ export const createSlackChannelAdapter = (options) => {
4715
4867
  continue;
4716
4868
  }
4717
4869
  try {
4718
- const web = createWeb(config.botToken);
4870
+ const created = createWeb(config.botToken);
4871
+ const web = config.linkPreviews === false ? withoutLinkPreviews(created) : created;
4719
4872
  const auth = await web.auth.test();
4720
4873
  const botUserId = auth.user_id ?? '';
4721
4874
  const teamId = auth.team_id ?? '';
4875
+ checkBotScopes(config.agentId, auth.response_metadata?.scopes ?? [], warn);
4722
4876
  // Recorded before the socket is even built: an app that never
4723
4877
  // comes up must still be recognizable when somebody names it.
4724
4878
  botIdentities.set(config.agentId, { botUserId, teamId });