Conversation
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟡 Slack mention seeding can create many new thread session keys (session store flooding / eviction DoS)
DescriptionTop-level Slack channel messages can be forced into new per-message thread sessions when
Although the session store has maintenance (default max entries), an attacker can still flood unique seeded thread keys to:
Vulnerable code (thread id seeding from top-level message ts): const seedCandidateThreadId = threadContext.incomingThreadTs ?? threadContext.messageTs;
const seededRoomThreadId =
!isThreadReply &&
isRoom &&
seedTopLevelRoomThread &&
replyToMode !== "off" &&
seedCandidateThreadId
? seedCandidateThreadId
: undefined;
...
const routedThreadId = canonicalThreadId ?? (isRoomish ? seededRoomThreadId : undefined);
...
: resolveThreadSessionKeys({
baseSessionKey: route.sessionKey,
threadId: routedThreadId,
});RecommendationMitigate session-store flooding from untrusted channel traffic:
Example approach (gate seeding behind allowlist or command): const shouldSeed =
isRoom &&
!isThreadReply &&
replyToMode !== "off" &&
isTrustedSender &&
isActionableCommand;
const seededRoomThreadId = shouldSeed ? threadContext.messageTs : undefined;2. 🔵 Unvalidated Slack thread_ts/ts used as conversationId and threadId for session routing
Description
Although Slack normally emits timestamps in
Vulnerable code: const seedCandidateThreadId = threadContext.incomingThreadTs ?? threadContext.messageTs;
...
const routedThreadId = canonicalThreadId ?? (isRoomish ? seededRoomThreadId : undefined);
...
conversationId: routedThreadId,
...
threadId: routedThreadId,RecommendationValidate/normalize Slack timestamps before using them for routing/binding keys.
Example fix: import { normalizeSlackThreadTsCandidate } from "../../thread-ts.js";
const incomingThreadTs = normalizeSlackThreadTsCandidate(threadContext.incomingThreadTs);
const messageTs = normalizeSlackThreadTsCandidate(threadContext.messageTs);
const seedCandidateThreadId = incomingThreadTs ?? messageTs;
...
const routedThreadId = normalizeSlackThreadTsCandidate(
canonicalThreadId ?? (isRoomish ? seededRoomThreadId : undefined),
);This constrains Analyzed PR: #72498 at commit Last updated on: 2026-04-27T06:16:35Z |
Greptile SummaryThis PR fixes Slack inbound thread session routing so that a top-level room app mention and its URL-only thread follow-up share one parent session key, eliminating duplicate subagent spawns. It does this by computing a mention check early (before Confidence Score: 4/5Safe to merge with awareness of the two P2 edge cases; no P0/P1 defects found on the primary fix path. All P2s. The core routing fix is well-targeted and covered by the new tests. Two edge cases worth monitoring: the
Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/slack/src/monitor/message-handler/prepare-routing.ts
Line: 108-115
Comment:
**`isRoomish` includes group DMs in the seed guard**
`seededRoomThreadId` is gated on `isRoomish`, which is `isRoom || isGroupDm`. The PR description targets "Slack room app mentions" and "top-level channel messages," but group DMs (`isGroupDm = true`) satisfy this guard too. A bot mention in a group DM with `replyToMode !== "off"` will now receive a thread-scoped session key (`agent:main:slack:group:<channel>:thread:<ts>`), whereas the old code kept group DMs on the channel session (since `canonicalThreadId` was always `undefined` for non-thread-reply room-ish messages without runtime binding). This is an undocumented behavior change for group DMs. Consider using `isRoom` instead of `isRoomish` if the intent is strictly channel/group-channel rooms.
```suggestion
const seededRoomThreadId =
!isThreadReply &&
isRoom &&
seedTopLevelRoomThread &&
replyToMode !== "off" &&
seedCandidateThreadId
? seedCandidateThreadId
: undefined;
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: extensions/slack/src/monitor/message-handler/prepare.ts
Line: 297-317
Comment:
**Routing seed decision and final mention check can diverge when runtime binding changes the agentId**
`wasMentionedForRouting` is computed against `initialRoute.agentId`'s regexes (`initialMentionRegexes`), but after `resolveSlackRoutingContext` the route may change via runtime conversation binding to a different agentId. `wasMentioned` (used for the final `mentionDecision`) is then computed against the bound agent's `mentionRegexes`.
If the bound agent's regexes match but the initial agent's don't (`wasMentionedForRouting = false`, `wasMentioned = true`), the message passes the mention gate but its session key is channel-scoped (not seeded), so a later thread follow-up still diverges. The inverse case (`wasMentionedForRouting = true`, `wasMentioned = false`) seeds the session but the message is then dropped by the mention check — a silent session-key orphan. This is an edge case confined to runtime-bound conversations with differing per-agent mention regexes, but it's worth documenting or guarding explicitly.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: extensions/slack/src/monitor/message-handler/prepare.thread-session-key.test.ts
Line: 136
Comment:
**`"batched"` missing from `replyToMode` iteration in existing channel-session test**
`buildCtx` and `buildAccount` now accept `"batched"` as a valid `replyToMode` value, but the existing "keeps top-level channel messages on the per-channel session regardless of replyToMode" test still iterates only over `["all", "first", "off"]`. Adding `"batched"` ensures the invariant is also pinned for that mode.
```suggestion
for (const mode of ["all", "first", "off", "batched"] as const) {
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "test(slack): cover bare thread ping prep..." | Re-trigger Greptile |
| const seededRoomThreadId = | ||
| !isThreadReply && | ||
| isRoomish && | ||
| seedTopLevelRoomThread && | ||
| replyToMode !== "off" && | ||
| seedCandidateThreadId | ||
| ? seedCandidateThreadId | ||
| : undefined; |
There was a problem hiding this comment.
isRoomish includes group DMs in the seed guard
seededRoomThreadId is gated on isRoomish, which is isRoom || isGroupDm. The PR description targets "Slack room app mentions" and "top-level channel messages," but group DMs (isGroupDm = true) satisfy this guard too. A bot mention in a group DM with replyToMode !== "off" will now receive a thread-scoped session key (agent:main:slack:group:<channel>:thread:<ts>), whereas the old code kept group DMs on the channel session (since canonicalThreadId was always undefined for non-thread-reply room-ish messages without runtime binding). This is an undocumented behavior change for group DMs. Consider using isRoom instead of isRoomish if the intent is strictly channel/group-channel rooms.
| const seededRoomThreadId = | |
| !isThreadReply && | |
| isRoomish && | |
| seedTopLevelRoomThread && | |
| replyToMode !== "off" && | |
| seedCandidateThreadId | |
| ? seedCandidateThreadId | |
| : undefined; | |
| const seededRoomThreadId = | |
| !isThreadReply && | |
| isRoom && | |
| seedTopLevelRoomThread && | |
| replyToMode !== "off" && | |
| seedCandidateThreadId | |
| ? seedCandidateThreadId | |
| : undefined; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/slack/src/monitor/message-handler/prepare-routing.ts
Line: 108-115
Comment:
**`isRoomish` includes group DMs in the seed guard**
`seededRoomThreadId` is gated on `isRoomish`, which is `isRoom || isGroupDm`. The PR description targets "Slack room app mentions" and "top-level channel messages," but group DMs (`isGroupDm = true`) satisfy this guard too. A bot mention in a group DM with `replyToMode !== "off"` will now receive a thread-scoped session key (`agent:main:slack:group:<channel>:thread:<ts>`), whereas the old code kept group DMs on the channel session (since `canonicalThreadId` was always `undefined` for non-thread-reply room-ish messages without runtime binding). This is an undocumented behavior change for group DMs. Consider using `isRoom` instead of `isRoomish` if the intent is strictly channel/group-channel rooms.
```suggestion
const seededRoomThreadId =
!isThreadReply &&
isRoom &&
seedTopLevelRoomThread &&
replyToMode !== "off" &&
seedCandidateThreadId
? seedCandidateThreadId
: undefined;
```
How can I resolve this? If you propose a fix, please make it concise.| @@ -290,6 +314,7 @@ export async function prepareSlackMessage(params: { | |||
| isGroupDm, | |||
| isRoom, | |||
| isRoomish, | |||
| seedTopLevelRoomThread: opts.source === "app_mention" || wasMentionedForRouting, | |||
There was a problem hiding this comment.
Routing seed decision and final mention check can diverge when runtime binding changes the agentId
wasMentionedForRouting is computed against initialRoute.agentId's regexes (initialMentionRegexes), but after resolveSlackRoutingContext the route may change via runtime conversation binding to a different agentId. wasMentioned (used for the final mentionDecision) is then computed against the bound agent's mentionRegexes.
If the bound agent's regexes match but the initial agent's don't (wasMentionedForRouting = false, wasMentioned = true), the message passes the mention gate but its session key is channel-scoped (not seeded), so a later thread follow-up still diverges. The inverse case (wasMentionedForRouting = true, wasMentioned = false) seeds the session but the message is then dropped by the mention check — a silent session-key orphan. This is an edge case confined to runtime-bound conversations with differing per-agent mention regexes, but it's worth documenting or guarding explicitly.
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/slack/src/monitor/message-handler/prepare.ts
Line: 297-317
Comment:
**Routing seed decision and final mention check can diverge when runtime binding changes the agentId**
`wasMentionedForRouting` is computed against `initialRoute.agentId`'s regexes (`initialMentionRegexes`), but after `resolveSlackRoutingContext` the route may change via runtime conversation binding to a different agentId. `wasMentioned` (used for the final `mentionDecision`) is then computed against the bound agent's `mentionRegexes`.
If the bound agent's regexes match but the initial agent's don't (`wasMentionedForRouting = false`, `wasMentioned = true`), the message passes the mention gate but its session key is channel-scoped (not seeded), so a later thread follow-up still diverges. The inverse case (`wasMentionedForRouting = true`, `wasMentioned = false`) seeds the session but the message is then dropped by the mention check — a silent session-key orphan. This is an edge case confined to runtime-bound conversations with differing per-agent mention regexes, but it's worth documenting or guarding explicitly.
How can I resolve this? If you propose a fix, please make it concise.|
WIP |
941725a to
faaecd3
Compare
This reverts commit faaecd3.
Normalize actionable Slack thread roots and follow-up replies onto the same thread parent session key.
Normalize actionable Slack thread roots and follow-up replies onto the same thread parent session key.
Summary
firstandbatchedreply metadata for seeded top-level roots: the session key is thread-scoped, butMessageThreadIdstays unset so single-use reply modes are not accidentally upgraded to all-replies behavior.1777244748.777299must not route by their own message timestamps.Root Cause
Duplicate subagent spawns were downstream of two parent sessions being created for one logical Slack thread. The inbound root mention had no reply-shaped thread context, so it used the channel parent session
agent:main:slack:channel:<channel>. The later URL-only follow-up carriedthread_ts, so it usedagent:main:slack:channel:<channel>:thread:<thread_ts>. This is session-key normalization: once the same logical Slack thread maps to one parent session, subagent spawn accounting stays attached to the same controller session.A related second incident showed actual follow-up replies in Slack thread
1777244748.777299persisting as separate parent sessions keyed by child message timestamps (1777245202.803289and1777245402.577379). The current routing fix prefers Slackthread_tsfor actual thread replies; regressions pin that invariant with the real channel/thread/child timestamp and the bare-ping text<@B1> ?. Slack prepare routing receives native Slackthread_ts; thereply_to_id/topic_idnames are derived runtime metadata, not inbound fields consumed by this routing layer.Notes
MessageThreadIdon seeded top-level roots. Downstream Slack reply planning treatsMessageThreadIdas an explicit existing thread target, which would breakfirstandbatchedby replying to every send.ReplyToIdremains available for those single-use modes.Tests
Testing level: targeted local validation. Full repo baseline/CI remains pending while this PR is in draft.
codex review --base origin/mainfound no discrete actionable regressions after the single-use reply-mode correction.AI-assisted: implementation and validation prepared with OpenAI Codex.