fix: isolate Slack thread sessions (#758)
This commit is contained in:
109
CHANGELOG.md
109
CHANGELOG.md
@@ -10,13 +10,11 @@
|
||||
|
||||
### Changes
|
||||
- Docs: clarify per-agent auth stores, sandboxed skill binaries, and elevated semantics.
|
||||
- Usage: add MiniMax coding plan usage tracking.
|
||||
- Daemon: support profile-aware service names for multi-gateway setups. (#671) — thanks @bjesuiter.
|
||||
- Docs: add FAQ entries for missing provider auth after adding agents and Gemini thinking signature errors.
|
||||
- Agents: add optional auth-profile copy prompt on `agents add` and improve auth error messaging.
|
||||
- Security: expand `clawdbot security audit` checks (model hygiene, config includes, plugin allowlists, exposure matrix) and extend `--fix` to tighten more sensitive state paths.
|
||||
- Security: add `SECURITY.md` reporting policy.
|
||||
- Voice Call: add Plivo provider for the voice-call plugin. (#846) — thanks @vrknetha.
|
||||
- Plugins: add Zalo channel plugin with gateway HTTP hooks and onboarding install prompt. (#854) — thanks @longmaba.
|
||||
- Onboarding: add a security checkpoint prompt (docs link + sandboxing hint); require `--accept-risk` for `--non-interactive`.
|
||||
- Docs: expand gateway security hardening guidance and incident response checklist.
|
||||
@@ -30,78 +28,61 @@
|
||||
- CLI/Docs: add per-command CLI doc pages and link them from `clawdbot <command> --help`.
|
||||
- Browser: copy the installed Chrome extension path to clipboard after `clawdbot browser extension install/path`.
|
||||
- WhatsApp: add `channels.whatsapp.sendReadReceipts` to disable auto read receipts. (#882) — thanks @chrisrodz.
|
||||
|
||||
### Fixes
|
||||
- Slack: isolate thread history and avoid inheriting channel transcripts for new threads by default. (#758)
|
||||
- Gateway: forward termination signals to respawned CLI child processes to avoid orphaned systemd runs. (#933) — thanks @roshanasingh4.
|
||||
- Browser: add tests for snapshot labels/efficient query params and labeled image responses.
|
||||
- macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4.
|
||||
- macOS: format ConnectionsStore config to satisfy SwiftFormat lint. (#852) — thanks @mneves75.
|
||||
- Packaging: run `pnpm build` on `prepack` so npm publishes include fresh `dist/` output.
|
||||
- Telegram: register dock native commands with underscores to avoid `BOT_COMMAND_INVALID` (#929, fixes #901) — thanks @grp06.
|
||||
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
|
||||
- Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams.
|
||||
- Agents: strip downgraded tool call text without eating adjacent replies and filter thinking-tag leaks. (#905) — thanks @erikpr1994.
|
||||
- Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli.
|
||||
- Doctor: avoid re-adding WhatsApp config when only legacy ack reactions are set. (#927, fixes #900) — thanks @grp06.
|
||||
- Agents: scrub tuple `items` schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.
|
||||
- Agents: stabilize sub-agent announce status from runtime outcomes and normalize Result/Notes. (#835) — thanks @roshanasingh4.
|
||||
- Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06.
|
||||
- Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight.
|
||||
- Daemon: clear persisted launchd disabled state before bootstrap (fixes `daemon install` after uninstall). (#849) — thanks @ndraiman.
|
||||
- Logging: tolerate `EIO` from console writes to avoid gateway crashes. (#925, fixes #878) — thanks @grp06.
|
||||
- Sandbox: restore `docker.binds` config validation for custom bind mounts. (#873) — thanks @akonyer.
|
||||
- Sandbox: preserve configured PATH for `docker exec` so custom tools remain available. (#873) — thanks @akonyer.
|
||||
- Slack: respect `channels.slack.requireMention` default when resolving channel mention gating. (#850) — thanks @evalexpr.
|
||||
- Telegram: aggregate split inbound messages into one prompt (reduces “one reply per fragment”).
|
||||
- Telegram: let control commands bypass per-chat sequentialization; always allow abort triggers.
|
||||
- Auto-reply: treat trailing `NO_REPLY` tokens as silent replies.
|
||||
- Config: prevent partial config writes from clobbering unrelated settings (base hash guard + merge patch for connection saves).
|
||||
|
||||
## 2026.1.14
|
||||
|
||||
### Changes
|
||||
- Usage: add MiniMax coding plan usage tracking.
|
||||
- Auth: label Claude Code CLI auth options. (#915) — thanks @SeanZoR.
|
||||
- Docs: standardize Claude Code CLI naming across docs and prompts. (follow-up to #915)
|
||||
- Telegram: add message delete action in the message tool. (#903) — thanks @sleontenko.
|
||||
- Config: add `channels.<provider>.configWrites` gating for channel-initiated config writes; migrate Slack channel IDs.
|
||||
|
||||
### Fixes
|
||||
|
||||
#### Agents
|
||||
- Agents: make user time zone and 24-hour time explicit in the system prompt. (#859) — thanks @CashWilliams.
|
||||
- Agents: strip downgraded tool call text without eating adjacent replies and filter thinking-tag leaks. (#905) — thanks @erikpr1994.
|
||||
- Agents: cap tool call IDs for OpenAI/OpenRouter to avoid request rejections. (#875) — thanks @j1philli.
|
||||
- Agents: scrub tuple `items` schemas for Gemini tool calls. (#926, fixes #746) — thanks @grp06.
|
||||
- Agents: stabilize sub-agent announce status from runtime outcomes and normalize Result/Notes. (#835) — thanks @roshanasingh4.
|
||||
- Agent: clear run context after CLI runs (`clearAgentRunContext`) to avoid runaway contexts. (#934) — thanks @ronak-guliani.
|
||||
|
||||
#### Browser
|
||||
- Browser: add tests for snapshot labels/efficient query params and labeled image responses.
|
||||
- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor.
|
||||
- UI: move Docs link into the left navigation menu.
|
||||
|
||||
#### macOS
|
||||
- macOS: ensure launchd log directory exists with a test-only override. (#909) — thanks @roshanasingh4.
|
||||
- macOS: format ConnectionsStore config to satisfy SwiftFormat lint. (#852) — thanks @mneves75.
|
||||
- macOS: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
|
||||
- macOS: fix cron preview/testing payload to use `channel` key. (#867) — thanks @wes-davis.
|
||||
|
||||
#### Messaging / Channels
|
||||
- Messaging: unify markdown formatting + format-first chunking for Slack/Telegram/Signal. (#920) — thanks @TheSethRose.
|
||||
- iMessage: prefer handle routing for direct-message replies; include imsg RPC error details. (#935)
|
||||
- Slack: respect `channels.slack.requireMention` default when resolving channel mention gating. (#850) — thanks @evalexpr.
|
||||
- Slack: drop Socket Mode events with mismatched `api_app_id`/`team_id`. (#889) — thanks @roshanasingh4.
|
||||
- Discord: isolate autoThread thread context. (#856) — thanks @davidguttman.
|
||||
- Telegram: register dock native commands with underscores to avoid `BOT_COMMAND_INVALID` (#929, fixes #901) — thanks @grp06.
|
||||
- Telegram: honor `channels.telegram.timeoutSeconds` for grammY API requests. (#863) — thanks @Snaver.
|
||||
- Telegram: split long captions into media + follow-up text messages. (#907) — thanks @jalehman.
|
||||
- Telegram: migrate group config when supergroups change chat IDs. (#906) — thanks @sleontenko.
|
||||
- Telegram: aggregate split inbound messages into one prompt (reduces “one reply per fragment”).
|
||||
- Telegram: let control commands bypass per-chat sequentialization; always allow abort triggers.
|
||||
- WhatsApp: fix context isolation using wrong ID (was bot's number, now conversation ID). (#911) — thanks @tristanmanchester.
|
||||
- WhatsApp: honor SenderE164 for owner command auth when SenderId is LID; let owner control commands bypass group mention gating.
|
||||
- WhatsApp: normalize user JIDs with device suffix for allowlist checks in groups. (#838) — thanks @peschee.
|
||||
|
||||
#### Gateway / Daemon / Sessions
|
||||
- Gateway/UI: ship session defaults in the hello snapshot so the Control UI canonicalizes main session keys (no bare `main` alias).
|
||||
- Gateway: forward termination signals to respawned CLI child processes to avoid orphaned systemd runs. (#933) — thanks @roshanasingh4.
|
||||
- Sessions: return deep clones (`structuredClone`) so cached session entries can't be mutated. (#934) — thanks @ronak-guliani.
|
||||
- Heartbeat: keep `updatedAt` monotonic when restoring heartbeat sessions. (#934) — thanks @ronak-guliani.
|
||||
- Daemon: clear persisted launchd disabled state before bootstrap (fixes `daemon install` after uninstall). (#849) — thanks @ndraiman.
|
||||
- Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`).
|
||||
- Agent: clear run context after CLI runs (`clearAgentRunContext`) to avoid runaway contexts. (#934) — thanks @ronak-guliani.
|
||||
- Mac: pass auth token/password to dashboard URL for authenticated access. (#918) — thanks @rahthakor.
|
||||
- UI: use application-defined WebSocket close code (browser compatibility). (#918) — thanks @rahthakor.
|
||||
- TUI: render picker overlays via the overlay stack so /models and /settings display. (#921) — thanks @grizzdank.
|
||||
- TUI: add a bright spinner + elapsed time in the status line for send/stream/run states.
|
||||
- Embedded runner: suppress raw API error payloads from replies. (#924) — thanks @grp06.
|
||||
- Logging: tolerate `EIO` from console writes to avoid gateway crashes. (#925, fixes #878) — thanks @grp06.
|
||||
|
||||
#### Tools
|
||||
- Tools: enable `web_fetch` by default (unless explicitly disabled in config).
|
||||
|
||||
#### Auth / Models
|
||||
- Auth: normalize Claude Code CLI profile mode to oauth and auto-migrate config. (#855) — thanks @sebslight.
|
||||
- Google: downgrade unsigned thinking blocks before send to avoid missing signature errors.
|
||||
|
||||
#### Config / Doctor
|
||||
- Doctor: avoid re-adding WhatsApp config when only legacy ack reactions are set. (#927, fixes #900) — thanks @grp06.
|
||||
- Config: prevent partial config writes from clobbering unrelated settings (base hash guard + merge patch for connection saves).
|
||||
|
||||
#### Packaging / Auto-reply
|
||||
- Packaging: run `pnpm build` on `prepack` so npm publishes include fresh `dist/` output.
|
||||
- Auto-reply: treat trailing `NO_REPLY` tokens as silent replies.
|
||||
|
||||
#### Sandbox
|
||||
- Sandbox: restore `docker.binds` config validation for custom bind mounts. (#873) — thanks @akonyer.
|
||||
- Sandbox: preserve configured PATH for `docker exec` so custom tools remain available. (#873) — thanks @akonyer.
|
||||
- Gateway/Dev: ensure `pnpm gateway:dev` always uses the dev profile config + state (`~/.clawdbot-dev`).
|
||||
- macOS: fix cron preview/testing payload to use `channel` key. (#867) — thanks @wes-davis.
|
||||
- Telegram: honor `channels.telegram.timeoutSeconds` for grammY API requests. (#863) — thanks @Snaver.
|
||||
- Telegram: split long captions into media + follow-up text messages. (#907) - thanks @jalehman.
|
||||
- Telegram: migrate group config when supergroups change chat IDs. (#906) — thanks @sleontenko.
|
||||
- Messaging: unify markdown formatting + format-first chunking for Slack/Telegram/Signal. (#920) — thanks @TheSethRose.
|
||||
- Slack: drop Socket Mode events with mismatched `api_app_id`/`team_id`. (#889) — thanks @roshanasingh4.
|
||||
- Discord: isolate autoThread thread context. (#856) — thanks @davidguttman.
|
||||
- WhatsApp: fix context isolation using wrong ID (was bot's number, now conversation ID). (#911) — thanks @tristanmanchester.
|
||||
- WhatsApp: normalize user JIDs with device suffix for allowlist checks in groups. (#838) — thanks @peschee.
|
||||
|
||||
## 2026.1.13
|
||||
|
||||
|
||||
@@ -269,6 +269,11 @@ By default, Clawdbot replies in the main channel. Use `channels.slack.replyToMod
|
||||
|
||||
The mode applies to both auto-replies and agent tool calls (`slack sendMessage`).
|
||||
|
||||
### Thread session isolation
|
||||
Slack thread sessions are isolated by default. Configure with:
|
||||
- `channels.slack.thread.historyScope`: `thread` (default) keeps per-thread history; `channel` shares history across the channel.
|
||||
- `channels.slack.thread.inheritParent`: `false` (default) starts a clean thread session; `true` copies the parent channel transcript into the thread session.
|
||||
|
||||
### Manual threading tags
|
||||
For fine-grained control, use these tags in agent responses:
|
||||
- `[[reply_to_current]]` — reply to the triggering message (start/continue thread).
|
||||
|
||||
@@ -1050,6 +1050,10 @@ Slack runs in Socket Mode and requires both a bot token and app token:
|
||||
reactionNotifications: "own", // off | own | all | allowlist
|
||||
reactionAllowlist: ["U123"],
|
||||
replyToMode: "off", // off | first | all
|
||||
thread: {
|
||||
historyScope: "thread", // thread | channel
|
||||
inheritParent: false
|
||||
},
|
||||
actions: {
|
||||
reactions: true,
|
||||
messages: true,
|
||||
@@ -1083,6 +1087,10 @@ Reaction notification modes:
|
||||
- `all`: all reactions on all messages.
|
||||
- `allowlist`: reactions from `channels.slack.reactionAllowlist` on all messages (empty list disables).
|
||||
|
||||
Thread session isolation:
|
||||
- `channels.slack.thread.historyScope` controls whether thread history is per-thread (`thread`, default) or shared across the channel (`channel`).
|
||||
- `channels.slack.thread.inheritParent` controls whether new thread sessions inherit the parent channel transcript (default: false).
|
||||
|
||||
Slack action groups (gate `slack` tool actions):
|
||||
| Action group | Default | Notes |
|
||||
| --- | --- | --- |
|
||||
|
||||
@@ -202,6 +202,8 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"channels.discord.token": "Discord Bot Token",
|
||||
"channels.slack.botToken": "Slack Bot Token",
|
||||
"channels.slack.appToken": "Slack App Token",
|
||||
"channels.slack.thread.historyScope": "Slack Thread History Scope",
|
||||
"channels.slack.thread.inheritParent": "Slack Thread Parent Inheritance",
|
||||
"channels.signal.account": "Signal Account",
|
||||
"channels.imessage.cliPath": "iMessage CLI Path",
|
||||
"plugins.enabled": "Enable Plugins",
|
||||
@@ -243,6 +245,10 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"tools.web.fetch.userAgent": "Override User-Agent header for web_fetch requests.",
|
||||
"channels.slack.allowBots":
|
||||
"Allow bot-authored messages to trigger Slack replies (default: false).",
|
||||
"channels.slack.thread.historyScope":
|
||||
'Scope for Slack thread history context ("thread" isolates per thread; "channel" reuses channel history).',
|
||||
"channels.slack.thread.inheritParent":
|
||||
"If true, Slack thread sessions inherit the parent channel transcript (default: false).",
|
||||
"auth.profiles": "Named auth profiles (provider + mode + optional email).",
|
||||
"auth.order": "Ordered auth profile IDs per provider (used for automatic failover).",
|
||||
"auth.cooldowns.billingBackoffHours":
|
||||
|
||||
@@ -60,6 +60,13 @@ export type SlackSlashCommandConfig = {
|
||||
ephemeral?: boolean;
|
||||
};
|
||||
|
||||
export type SlackThreadConfig = {
|
||||
/** Scope for thread history context (thread|channel). Default: thread. */
|
||||
historyScope?: "thread" | "channel";
|
||||
/** If true, thread sessions inherit the parent channel transcript. Default: false. */
|
||||
inheritParent?: boolean;
|
||||
};
|
||||
|
||||
export type SlackAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
name?: string;
|
||||
@@ -101,6 +108,8 @@ export type SlackAccountConfig = {
|
||||
reactionAllowlist?: Array<string | number>;
|
||||
/** Control reply threading when reply tags are present (off|first|all). */
|
||||
replyToMode?: ReplyToMode;
|
||||
/** Thread session behavior. */
|
||||
thread?: SlackThreadConfig;
|
||||
actions?: SlackActionConfig;
|
||||
slashCommand?: SlackSlashCommandConfig;
|
||||
dm?: SlackDmConfig;
|
||||
|
||||
@@ -203,6 +203,11 @@ export const SlackChannelSchema = z.object({
|
||||
systemPrompt: z.string().optional(),
|
||||
});
|
||||
|
||||
export const SlackThreadSchema = z.object({
|
||||
historyScope: z.enum(["thread", "channel"]).optional(),
|
||||
inheritParent: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const SlackAccountSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
capabilities: z.array(z.string()).optional(),
|
||||
@@ -224,6 +229,7 @@ export const SlackAccountSchema = z.object({
|
||||
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
||||
reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
replyToMode: ReplyToModeSchema.optional(),
|
||||
thread: SlackThreadSchema.optional(),
|
||||
actions: z
|
||||
.object({
|
||||
reactions: z.boolean().optional(),
|
||||
|
||||
@@ -327,6 +327,81 @@ describe("monitorSlackProvider tool results", () => {
|
||||
expect(capturedCtx.CommandBody).toBe("second");
|
||||
});
|
||||
|
||||
it("scopes thread history to the thread by default", async () => {
|
||||
config = {
|
||||
messages: { ackReactionScope: "group-mentions" },
|
||||
channels: {
|
||||
slack: {
|
||||
historyLimit: 5,
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
channels: { C1: { allow: true, requireMention: false } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const capturedCtx: Array<{ Body?: string }> = [];
|
||||
replyMock.mockImplementation(async (ctx) => {
|
||||
capturedCtx.push(ctx ?? {});
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const controller = new AbortController();
|
||||
const run = monitorSlackProvider({
|
||||
botToken: "bot-token",
|
||||
appToken: "app-token",
|
||||
abortSignal: controller.signal,
|
||||
});
|
||||
|
||||
await waitForEvent("message");
|
||||
const handler = getSlackHandlers()?.get("message");
|
||||
if (!handler) throw new Error("Slack message handler not registered");
|
||||
|
||||
await handler({
|
||||
event: {
|
||||
type: "message",
|
||||
user: "U1",
|
||||
text: "thread-a-one",
|
||||
ts: "200",
|
||||
thread_ts: "100",
|
||||
channel: "C1",
|
||||
channel_type: "channel",
|
||||
},
|
||||
});
|
||||
|
||||
await handler({
|
||||
event: {
|
||||
type: "message",
|
||||
user: "U1",
|
||||
text: "thread-a-two",
|
||||
ts: "201",
|
||||
thread_ts: "100",
|
||||
channel: "C1",
|
||||
channel_type: "channel",
|
||||
},
|
||||
});
|
||||
|
||||
await handler({
|
||||
event: {
|
||||
type: "message",
|
||||
user: "U2",
|
||||
text: "thread-b-one",
|
||||
ts: "301",
|
||||
thread_ts: "300",
|
||||
channel: "C1",
|
||||
channel_type: "channel",
|
||||
},
|
||||
});
|
||||
|
||||
await flush();
|
||||
controller.abort();
|
||||
await run;
|
||||
|
||||
expect(replyMock).toHaveBeenCalledTimes(3);
|
||||
expect(capturedCtx[1]?.Body).toContain("thread-a-one");
|
||||
expect(capturedCtx[2]?.Body).not.toContain("thread-a-one");
|
||||
expect(capturedCtx[2]?.Body).not.toContain("thread-a-two");
|
||||
});
|
||||
|
||||
it("updates assistant thread status when replies start", async () => {
|
||||
replyMock.mockImplementation(async (_ctx, opts) => {
|
||||
await opts?.onReplyStart?.();
|
||||
|
||||
@@ -202,10 +202,60 @@ describe("monitorSlackProvider tool results", () => {
|
||||
ParentSessionKey?: string;
|
||||
};
|
||||
expect(ctx.SessionKey).toBe("agent:main:main:thread:123");
|
||||
expect(ctx.ParentSessionKey).toBe("agent:main:main");
|
||||
expect(ctx.ParentSessionKey).toBeUndefined();
|
||||
});
|
||||
|
||||
it("forks thread sessions and injects starter context", async () => {
|
||||
it("keeps thread parent inheritance opt-in", async () => {
|
||||
replyMock.mockResolvedValue({ text: "thread reply" });
|
||||
|
||||
config = {
|
||||
messages: { responsePrefix: "PFX" },
|
||||
channels: {
|
||||
slack: {
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
channels: { C1: { allow: true, requireMention: false } },
|
||||
thread: { inheritParent: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const controller = new AbortController();
|
||||
const run = monitorSlackProvider({
|
||||
botToken: "bot-token",
|
||||
appToken: "app-token",
|
||||
abortSignal: controller.signal,
|
||||
});
|
||||
|
||||
await waitForEvent("message");
|
||||
const handler = getSlackHandlers()?.get("message");
|
||||
if (!handler) throw new Error("Slack message handler not registered");
|
||||
|
||||
await handler({
|
||||
event: {
|
||||
type: "message",
|
||||
user: "U1",
|
||||
text: "hello",
|
||||
ts: "123",
|
||||
thread_ts: "111.222",
|
||||
channel: "C1",
|
||||
channel_type: "channel",
|
||||
},
|
||||
});
|
||||
|
||||
await flush();
|
||||
controller.abort();
|
||||
await run;
|
||||
|
||||
expect(replyMock).toHaveBeenCalledTimes(1);
|
||||
const ctx = replyMock.mock.calls[0]?.[0] as {
|
||||
SessionKey?: string;
|
||||
ParentSessionKey?: string;
|
||||
};
|
||||
expect(ctx.SessionKey).toBe("agent:main:slack:channel:C1:thread:111.222");
|
||||
expect(ctx.ParentSessionKey).toBe("agent:main:slack:channel:C1");
|
||||
});
|
||||
|
||||
it("injects starter context for thread replies", async () => {
|
||||
replyMock.mockResolvedValue({ text: "ok" });
|
||||
|
||||
const client = getSlackClient();
|
||||
@@ -265,7 +315,7 @@ describe("monitorSlackProvider tool results", () => {
|
||||
ThreadLabel?: string;
|
||||
};
|
||||
expect(ctx.SessionKey).toBe("agent:main:slack:channel:C1:thread:111.222");
|
||||
expect(ctx.ParentSessionKey).toBe("agent:main:slack:channel:C1");
|
||||
expect(ctx.ParentSessionKey).toBeUndefined();
|
||||
expect(ctx.ThreadStarterBody).toContain("starter message");
|
||||
expect(ctx.ThreadLabel).toContain("Slack thread #general");
|
||||
});
|
||||
@@ -329,7 +379,7 @@ describe("monitorSlackProvider tool results", () => {
|
||||
ParentSessionKey?: string;
|
||||
};
|
||||
expect(ctx.SessionKey).toBe("agent:support:slack:channel:C1:thread:111.222");
|
||||
expect(ctx.ParentSessionKey).toBe("agent:support:slack:channel:C1");
|
||||
expect(ctx.ParentSessionKey).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps replies in channel root when message is not threaded (replyToMode off)", async () => {
|
||||
|
||||
@@ -74,6 +74,8 @@ export type SlackMonitorContext = {
|
||||
reactionMode: SlackReactionNotificationMode;
|
||||
reactionAllowlist: Array<string | number>;
|
||||
replyToMode: "off" | "first" | "all";
|
||||
threadHistoryScope: "thread" | "channel";
|
||||
threadInheritParent: boolean;
|
||||
slashCommand: Required<import("../../config/config.js").SlackSlashCommandConfig>;
|
||||
textLimit: number;
|
||||
ackReactionScope: string;
|
||||
@@ -133,6 +135,8 @@ export function createSlackMonitorContext(params: {
|
||||
reactionMode: SlackReactionNotificationMode;
|
||||
reactionAllowlist: Array<string | number>;
|
||||
replyToMode: SlackMonitorContext["replyToMode"];
|
||||
threadHistoryScope: SlackMonitorContext["threadHistoryScope"];
|
||||
threadInheritParent: SlackMonitorContext["threadInheritParent"];
|
||||
slashCommand: SlackMonitorContext["slashCommand"];
|
||||
textLimit: number;
|
||||
ackReactionScope: string;
|
||||
@@ -363,6 +367,8 @@ export function createSlackMonitorContext(params: {
|
||||
reactionMode: params.reactionMode,
|
||||
reactionAllowlist: params.reactionAllowlist,
|
||||
replyToMode: params.replyToMode,
|
||||
threadHistoryScope: params.threadHistoryScope,
|
||||
threadInheritParent: params.threadInheritParent,
|
||||
slashCommand: params.slashCommand,
|
||||
textLimit: params.textLimit,
|
||||
ackReactionScope: params.ackReactionScope,
|
||||
|
||||
@@ -263,7 +263,6 @@ export async function prepareSlackMessage(params: {
|
||||
: null;
|
||||
|
||||
const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`;
|
||||
const historyKey = message.channel;
|
||||
const historyEntry =
|
||||
isRoomish && ctx.historyLimit > 0
|
||||
? {
|
||||
@@ -291,9 +290,11 @@ export async function prepareSlackMessage(params: {
|
||||
const threadKeys = resolveThreadSessionKeys({
|
||||
baseSessionKey,
|
||||
threadId: isThreadReply ? threadTs : undefined,
|
||||
parentSessionKey: isThreadReply ? baseSessionKey : undefined,
|
||||
parentSessionKey: isThreadReply && ctx.threadInheritParent ? baseSessionKey : undefined,
|
||||
});
|
||||
const sessionKey = threadKeys.sessionKey;
|
||||
const historyKey =
|
||||
isThreadReply && ctx.threadHistoryScope === "thread" ? sessionKey : message.channel;
|
||||
enqueueSystemEvent(`${inboundLabel}: ${preview}`, {
|
||||
sessionKey,
|
||||
contextKey: `slack:message:${message.channel}:${message.ts ?? "unknown"}`,
|
||||
|
||||
@@ -74,6 +74,8 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
const reactionMode = slackCfg.reactionNotifications ?? "own";
|
||||
const reactionAllowlist = slackCfg.reactionAllowlist ?? [];
|
||||
const replyToMode = slackCfg.replyToMode ?? "off";
|
||||
const threadHistoryScope = slackCfg.thread?.historyScope ?? "thread";
|
||||
const threadInheritParent = slackCfg.thread?.inheritParent ?? false;
|
||||
const slashCommand = resolveSlackSlashCommandConfig(opts.slashCommand ?? slackCfg.slashCommand);
|
||||
const textLimit = resolveTextChunkLimit(cfg, "slack", account.accountId);
|
||||
const ackReactionScope = cfg.messages?.ackReactionScope ?? "group-mentions";
|
||||
@@ -129,6 +131,8 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
reactionMode,
|
||||
reactionAllowlist,
|
||||
replyToMode,
|
||||
threadHistoryScope,
|
||||
threadInheritParent,
|
||||
slashCommand,
|
||||
textLimit,
|
||||
ackReactionScope,
|
||||
|
||||
Reference in New Issue
Block a user