Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/slack-history-whoami.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sapiom/tools": minor
---

`connectors.slack` gains `history()` (Slack `conversations.history`: a channel's top-level messages, paginated, with `oldest` / `latest` bounds) and `whoAmI()` (Slack `auth.test`: the bot's own `user_id` and `bot_id`, to recognise its own posts). Agents that read a channel no longer need their own Slack bot token.
22 changes: 11 additions & 11 deletions examples/support-desk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -690,14 +690,14 @@ The deployed agents always share the fleet database.

## Known limitations

| Limitation | Why |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A channel without the bot is invisible.** A customer posting there produces no event, no run and no alert. | Slack sends nothing for a channel the bot is not in. The setup agent probes the channels listed in `channels.customer`; it cannot find unlisted ones. |
| **Post, then record (top-level posts).** A threaded post that is recorded after it is sent is stamped with a block id derived from its key, so a retry finds it in the thread and records it instead of posting again (intake's team-reply mirror records first, so it cannot duplicate). A top-level post (the intake triage card, the urgent-pager DM, the controller's on-call escalation DM, the daily digest, an On Hold notice on an issue with no triage thread) still posts again if Slack accepted it and the next database write failed. | Slack's Web API has no idempotency key, and the Slack connector exposes neither `conversations.history` nor message `metadata`, so a top-level post cannot be found again. |
| **Linear adoption window.** A retry more than 7 days after a crash between creating the Linear issue and recording it creates a second one. | `save_issue` has no idempotency key; escalation adopts by a `<fleetId>:<issueId>` marker over the last 7 days. |
| **Thread replies under a card left in an old triage channel are not notes.** After a desk's `triageChannel` moves, a teammate's reply under one of its existing cards is not stored on the issue. | Intake routes a message by its channel, and the old channel is no longer a desk's triage channel. |
| **Intake links by content.** A new top-level message joins any open issue Jev judges to be the same problem (p ≥ 0.8). Leftover open issues capture new messages. | Run `pnpm run reset-demo` before a demo. |
| **Latency.** A customer message reaches its draft card in about 35 s. A click shows a working state in about 2 s. | The time goes to the steps: intake makes a Jev call and several Slack calls, and each step starts a fresh sandbox call. |
| **Deploy detection is local.** `pnpm run setup` skips a deploy when the bundle hash in `.sapiom/fleet-state.json` matches the live build; a fresh clone redeploys once. | The server does not expose a content hash for a build. |
| **Customers are recognised by Slack workspace.** Anyone outside `team.slack_team_ids` posting in a channel the bot is in is a customer. Shared channels cannot be detected. A team member's message is kept only in a channel that already has an account. A customer who posts from your workspace needs `customers.test_user_ids`. | The connector has no `conversations.info`. |
| **The docs source is fleet-wide.** Every desk drafts from the same `knowledge.docs_url`. | Desks share it until a per-desk override is needed. |
| Limitation | Why |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A channel without the bot is invisible.** A customer posting there produces no event, no run and no alert. | Slack sends nothing for a channel the bot is not in. The setup agent probes the channels listed in `channels.customer`; it cannot find unlisted ones. |
| **Post, then record (top-level posts).** A threaded post that is recorded after it is sent is stamped with a block id derived from its key, so a retry finds it in the thread and records it instead of posting again (intake's team-reply mirror records first, so it cannot duplicate). A top-level post (the intake triage card, the urgent-pager DM, the controller's on-call escalation DM, the daily digest, an On Hold notice on an issue with no triage thread) still posts again if Slack accepted it and the next database write failed. | Slack's Web API has no idempotency key, and the desk does not yet search channel history (`connectors.slack.history`) for a top-level post's block id, so it cannot find one again. |
| **Linear adoption window.** A retry more than 7 days after a crash between creating the Linear issue and recording it creates a second one. | `save_issue` has no idempotency key; escalation adopts by a `<fleetId>:<issueId>` marker over the last 7 days. |
| **Thread replies under a card left in an old triage channel are not notes.** After a desk's `triageChannel` moves, a teammate's reply under one of its existing cards is not stored on the issue. | Intake routes a message by its channel, and the old channel is no longer a desk's triage channel. |
| **Intake links by content.** A new top-level message joins any open issue Jev judges to be the same problem (p ≥ 0.8). Leftover open issues capture new messages. | Run `pnpm run reset-demo` before a demo. |
| **Latency.** A customer message reaches its draft card in about 35 s. A click shows a working state in about 2 s. | The time goes to the steps: intake makes a Jev call and several Slack calls, and each step starts a fresh sandbox call. |
| **Deploy detection is local.** `pnpm run setup` skips a deploy when the bundle hash in `.sapiom/fleet-state.json` matches the live build; a fresh clone redeploys once. | The server does not expose a content hash for a build. |
| **Customers are recognised by Slack workspace.** Anyone outside `team.slack_team_ids` posting in a channel the bot is in is a customer. Shared channels cannot be detected. A team member's message is kept only in a channel that already has an account. A customer who posts from your workspace needs `customers.test_user_ids`. | The connector has no `conversations.info`. |
| **The docs source is fleet-wide.** Every desk drafts from the same `knowledge.docs_url`. | Desks share it until a per-desk override is needed. |
9 changes: 9 additions & 0 deletions packages/tools/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ import * as github from "./connectors/github/index.js";
import type { ListReposArgs, GitHubRepo } from "./connectors/github/index.js";
import * as slack from "./connectors/slack/index.js";
import type {
SlackHistoryArgs,
SlackHistoryResult,
SlackPostEphemeralArgs,
SlackPostEphemeralResult,
SlackPostMessageArgs,
Expand All @@ -214,6 +216,7 @@ import type {
SlackUpdateResult,
SlackUserInfoArgs,
SlackUserInfoResult,
SlackWhoAmIResult,
} from "./connectors/slack/index.js";
import { mcp } from "./connectors/mcp/index.js";
import type { McpConnector } from "./connectors/mcp/index.js";
Expand Down Expand Up @@ -632,6 +635,10 @@ export interface Sapiom {
removeReaction(args: SlackReactionArgs): Promise<SlackReactionResult>;
/** `conversations.replies`: a thread's parent message and replies, paginated. */
replies(args: SlackRepliesArgs): Promise<SlackRepliesResult>;
/** `conversations.history`: a channel's top-level messages, paginated. */
history(args: SlackHistoryArgs): Promise<SlackHistoryResult>;
/** `auth.test`: the bot's own `user_id` and `bot_id`. */
whoAmI(): Promise<SlackWhoAmIResult>;
/** `users.info`: look up a user by id. */
userInfo(args: SlackUserInfoArgs): Promise<SlackUserInfoResult>;
};
Expand Down Expand Up @@ -921,6 +928,8 @@ function bind(transport: Transport): Sapiom {
addReaction: (args) => slack.addReaction(args, transport),
removeReaction: (args) => slack.removeReaction(args, transport),
replies: (args) => slack.replies(args, transport),
history: (args) => slack.history(args, transport),
whoAmI: () => slack.whoAmI(transport),
userInfo: (args) => slack.userInfo(args, transport),
},
linear: mcp("linear", transport),
Expand Down
Loading
Loading