Skip to content

[Context] Avoid replaying current Slack bot thread context#68402

Open
bek91 wants to merge 6 commits intoopenclaw:mainfrom
bek91:codex/fix-slack-thread-context-injection
Open

[Context] Avoid replaying current Slack bot thread context#68402
bek91 wants to merge 6 commits intoopenclaw:mainfrom
bek91:codex/fix-slack-thread-context-injection

Conversation

@bek91
Copy link
Copy Markdown

@bek91 bek91 commented Apr 18, 2026

Summary

Describe the problem and fix in 2–5 bullets:

  • Problem: fresh Slack thread bootstrap could replay OpenClaw's own prior Slack bot turns back into a new session as assistant context, and runPreparedReply() could duplicate ThreadStarterBody by emitting both the structured untrusted block and a plain-text [Thread starter - for context] prelude.
  • Why it matters: a restarted or newly-created thread session could inherit low-value self-authored prompt text and duplicate starter content, both of which add noise to the agent prompt.
  • What changed: first-turn Slack thread seeding now excludes only the current Slack bot's starter/history entries, preserves human and third-party bot context, and removes the plain-text ThreadStarterBody fallback when no ThreadHistoryBody exists.
  • What did NOT change (scope boundary): allowlist/context-visibility behavior for non-bot senders is unchanged, and third-party bot/integration thread context is still retained.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

  • Root cause: Slack first-turn bootstrap treated prior thread context too broadly, so OpenClaw-authored Slack bot turns could be replayed into the next session as assistant context, and get-reply-run.ts independently added a second plain-text starter prelude when only ThreadStarterBody existed.
  • Missing detection / guardrail: there was no regression coverage distinguishing current-bot thread content from third-party bot context, and no test asserting that starter-only context should appear exactly once.
  • Contributing context (if known): the branch originally removed all bot-authored thread history; codex review --base origin/main caught that as over-broad, so this update narrows the filter to the current Slack bot identity only.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts, extensions/slack/src/monitor/message-handler/prepare.thread-context-allowlist.test.ts, extensions/slack/src/monitor/message-handler/prepare.test.ts, src/auto-reply/reply/get-reply-run.media-only.test.ts
  • Scenario the test should lock in: fresh Slack thread bootstrap excludes the current bot's prior starter/history, retains third-party bot context, and does not duplicate starter-only prompt context.
  • Why this is the smallest reliable guardrail: the bug lives at the Slack thread-context seam plus reply prompt construction, and these tests exercise those exact boundaries without needing a live Slack environment.
  • Existing test that already covers this (if any): the Slack extension lane also covers the touched Slack surface via pnpm test:extension slack.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

  • Fresh Slack thread sessions no longer replay OpenClaw's own earlier Slack bot turns into ThreadStarterBody / ThreadHistoryBody.
  • Third-party bot/integration messages in Slack threads are still preserved as context.
  • Starter-only context is no longer duplicated by an extra plain-text [Thread starter - for context] prelude.

Diagram (if applicable)

Before:
[new Slack thread session] -> [seed all prior thread content] -> [self-bot turns and starter-only plain-text fallback can enter prompt]

After:
[new Slack thread session] -> [drop only current Slack bot starter/history] -> [keep human/third-party context] -> [single starter-context representation]

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation:

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: local pnpm / Vitest checkout
  • Model/provider: Codex local review + edit workflow
  • Integration/channel (if any): Slack
  • Relevant config (redacted): channels.slack.replyToMode=all, channels.slack.groupPolicy=open, thread bootstrap enabled with initialHistoryLimit=20

Steps

  1. Start a fresh Slack thread session where the thread already contains earlier replies from OpenClaw.
  2. Send a new follow-up in that thread after session creation or rehydration.
  3. Inspect the seeded prompt context for ThreadStarterBody / ThreadHistoryBody.

Expected

  • OpenClaw's own earlier Slack bot turns are not replayed into the new session bootstrap.
  • Third-party bot or human context remains available when relevant.
  • Starter-only context appears once.

Actual

  • With this patch, the above expected behavior is observed in the focused regression suite and the Slack extension test lane.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

What you personally verified (not just CI), and how:

  • Verified scenarios: current-bot starter/history filtering, third-party bot starter retention, and the duplicate ThreadStarterBody prelude regression.
  • Edge cases checked: allowlist mode, first-turn thread bootstrap, and media-only reply prompt construction.
  • What you did not verify: a live Slack workspace/manual end-to-end run.

AI Assistance

  • AI-assisted: Codex was used to implement and review this PR.
  • Testing degree: fully tested for the touched surface; full-repo pnpm build / pnpm check still fail on unrelated existing issues noted below.
  • Prompts/session logs: local Codex session history is available on request.
  • I understand what the code does and reviewed the final changes before pushing.
  • codex review --base origin/main was run locally and its actionable finding was addressed before this update.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps:

Risks and Mitigations

  • Risk: filtering by the current Slack bot identity could miss future self-authored payload shapes if Slack changes the relevant author fields.
    • Mitigation: added regression coverage for current-bot vs third-party-bot starter/history behavior; retained existing sender formatting path for all surviving context.
  • Risk: full-repo validation is currently noisy due unrelated red checks.
    • Mitigation: ran focused regression coverage plus pnpm test:extension slack, and documented the unrelated failures below.

Validation Notes

Passed locally:

  • codex review --base origin/main
  • pnpm exec vitest run extensions/slack/src/monitor/message-handler/prepare-thread-context.test.ts extensions/slack/src/monitor/message-handler/prepare.thread-context-allowlist.test.ts extensions/slack/src/monitor/message-handler/prepare.test.ts src/auto-reply/reply/get-reply-run.media-only.test.ts
  • pnpm test:extension slack

Attempted but currently failing outside this diff:

  • pnpm build
    • extensions/lobster/src/lobster-runner.ts: could not resolve @clawdbot/lobster/core
  • pnpm check
    • extensions/discord/src/monitor/gateway-plugin*.ts: firstHeartbeatTimeout typing
    • extensions/qa-lab/src/providers/aimock/server.ts: @copilotkit/aimock / implicit any
    • tracked at #69006 for the Discord / qa-lab failures

@openclaw-barnacle openclaw-barnacle bot added channel: slack Channel integration: slack size: S labels Apr 18, 2026
@bek91 bek91 changed the title [codex] Avoid replaying Slack bot thread context [Context] Avoid replaying Slack bot thread context Apr 18, 2026
@bek91 bek91 marked this pull request as ready for review April 18, 2026 04:31
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 18, 2026

Greptile Summary

This PR fixes two related Slack thread-context issues: (1) bot-authored messages were being replayed into new thread sessions via ThreadHistoryBody, and (2) when only ThreadStarterBody existed, the same starter text could appear twice in the prompt — once as a structured untrusted-context block and once as a plain-text [Thread starter - for context] prelude.

The bot-message fix pre-filters threadHistory before the allowlist step in resolveSlackThreadContextData, and the duplicate-prelude fix removes the ThreadStarterBody fallback from threadContextNote in get-reply-run.ts. Both changes are well-tested with targeted regression coverage.

Confidence Score: 5/5

Safe to merge — changes are focused, correct, and fully covered by new/updated tests.

Both fixes are logically sound. The only remaining findings are P2 cleanup suggestions (dead code branches after the bot pre-filter, and uniqueUserIds being derived from the pre-filter list). Neither affects correctness.

No files require special attention.

Comments Outside Diff (2)

  1. extensions/slack/src/monitor/message-handler/prepare-thread-context.ts, line 145-158 (link)

    P2 uniqueUserIds built before the bot filter is applied

    uniqueUserIds is derived from threadHistory (the full list), then threadHistoryWithoutBots is computed three lines later. Since Slack bot messages typically don't carry a user field this is harmless in practice, but semantically the lookup should operate on the already-filtered list so we never call resolveUserName for entries that will be discarded.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/slack/src/monitor/message-handler/prepare-thread-context.ts
    Line: 145-158
    
    Comment:
    **`uniqueUserIds` built before the bot filter is applied**
    
    `uniqueUserIds` is derived from `threadHistory` (the full list), then `threadHistoryWithoutBots` is computed three lines later. Since Slack bot messages typically don't carry a `user` field this is harmless in practice, but semantically the lookup should operate on the already-filtered list so we never call `resolveUserName` for entries that will be discarded.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. extensions/slack/src/monitor/message-handler/prepare-thread-context.ts, line 186-203 (link)

    P2 Dead branches in the post-filter formatting loop

    filteredThreadHistory is derived from threadHistoryWithoutBots, so historyMsg.botId is always undefined here. That makes isBot always false, role always "user", and the historyMsg.botId ? \Bot (${historyMsg.botId})` : "Unknown"` ternary unreachable. These can be simplified:

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/slack/src/monitor/message-handler/prepare-thread-context.ts
    Line: 186-203
    
    Comment:
    **Dead branches in the post-filter formatting loop**
    
    `filteredThreadHistory` is derived from `threadHistoryWithoutBots`, so `historyMsg.botId` is always `undefined` here. That makes `isBot` always `false`, `role` always `"user"`, and the `historyMsg.botId ? \`Bot (${historyMsg.botId})\` : "Unknown"` ternary unreachable. These can be simplified:
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/slack/src/monitor/message-handler/prepare-thread-context.ts
Line: 145-158

Comment:
**`uniqueUserIds` built before the bot filter is applied**

`uniqueUserIds` is derived from `threadHistory` (the full list), then `threadHistoryWithoutBots` is computed three lines later. Since Slack bot messages typically don't carry a `user` field this is harmless in practice, but semantically the lookup should operate on the already-filtered list so we never call `resolveUserName` for entries that will be discarded.

```suggestion
      const threadHistoryWithoutBots = threadHistory.filter((historyMsg) => !historyMsg.botId);
      const omittedBotHistoryCount = threadHistory.length - threadHistoryWithoutBots.length;
      const uniqueUserIds = [
        ...new Set(
          threadHistoryWithoutBots.map((item) => item.userId).filter((id): id is string => Boolean(id)),
        ),
      ];
      const userMap = new Map<string, { name?: string }>();
      await Promise.all(
        uniqueUserIds.map(async (id) => {
          const user = await params.ctx.resolveUserName(id);
          if (user) {
            userMap.set(id, user);
          }
        }),
      );
```

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-context.ts
Line: 186-203

Comment:
**Dead branches in the post-filter formatting loop**

`filteredThreadHistory` is derived from `threadHistoryWithoutBots`, so `historyMsg.botId` is always `undefined` here. That makes `isBot` always `false`, `role` always `"user"`, and the `historyMsg.botId ? \`Bot (${historyMsg.botId})\` : "Unknown"` ternary unreachable. These can be simplified:

```suggestion
      for (const historyMsg of filteredThreadHistory) {
        const msgUser = historyMsg.userId ? userMap.get(historyMsg.userId) : null;
        const msgSenderName = msgUser?.name ?? "Unknown";
        const msgWithId = `${historyMsg.text}\n[slack message id: ${historyMsg.ts ?? "unknown"} channel: ${params.message.channel}]`;
        historyParts.push(
          formatInboundEnvelope({
            channel: "Slack",
            from: `${msgSenderName} (user)`,
            timestamp: historyMsg.ts ? Math.round(Number(historyMsg.ts) * 1000) : undefined,
            body: msgWithId,
            chatType: "channel",
            envelope: params.envelopeOptions,
          }),
        );
      }
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Avoid replaying Slack bot thread context" | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@martingarramon martingarramon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM on the core change — filtering bot-authored history out of first-turn seeding is the right move, and the combined omittedHistoryCount + omittedBotHistoryCount log math is correct (the two sets are non-overlapping since threadHistoryWithoutBots feeds filterSupplementalContextItems).

Two small questions:

  1. ThreadStarterBody parity? The filter targets threadHistory but doesn't touch ThreadStarterBody. If a bot kicks off a thread that the gateway later joins, the starter itself would still seed. Intentional scope limit, or a follow-up?

  2. Fallback if the filter ever leaks. Removing the role branch means any bot message that slips through !historyMsg.botId (e.g., a future Slack SDK shape where botId is absent but the author is effectively a bot) would now be labeled (user) rather than (assistant). Low-probability, but a one-line defense-in-depth (const role = historyMsg.botId ? "assistant" : "user" kept) or a comment noting the filter is now the single source of truth would make the intent explicit.

Non-blocking. Test updates match the behavior change.

One housekeeping note: the pre-commit hook failures you called out in the PR body (Discord + qa-lab) are tracked now at #69006 — you can reference it if you want a canonical link.

Copy link
Copy Markdown
Author

bek91 commented Apr 19, 2026

Follow-up after review:

  • narrowed the Slack thread filter so it excludes only the current Slack bot's own prior starter/history entries
  • kept third-party bot/integration thread context intact
  • added regression coverage for current-bot vs third-party-bot starter handling
  • reran codex review --base origin/main, the focused Vitest regression set, and pnpm test:extension slack

I also rewrote the PR body to match the repo template and to document the current validation state plus the unrelated full-repo build / check failures.

@bek91 bek91 changed the title [Context] Avoid replaying Slack bot thread context [Context] Avoid replaying current Slack bot thread context Apr 19, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b937a41df7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread extensions/slack/src/monitor/message-handler/prepare-thread-context.ts Outdated
Copy link
Copy Markdown
Author

bek91 commented Apr 19, 2026

Also related to #68383. This latest follow-up commit fixes the auth.test().user_id vs Slack bot_id mismatch in the self-bot thread-context filter and reruns the Slack validation lanes.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22ba3df0cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/auto-reply/reply/get-reply-run.ts Outdated
Comment thread extensions/slack/src/monitor/message-handler/prepare-thread-context.ts Outdated
@martingarramon
Copy link
Copy Markdown
Contributor

LGTM. The new "omits bot-authored starter text and history" test covers both surface points I flagged — ThreadStarterBody parity (via threadStarterBody.toBeUndefined()) and third-party-bot preservation (scoping to bot_id: "B1" leaves other bots intact). Optional botId?: string on the context type is the right shape for backward compat. Ship it.

@martingarramon
Copy link
Copy Markdown
Contributor

Follow-on 6611a86 also LGTM — bot/assistant role labeling + !isNewSession thread-starter fallback are natural extensions of the filter: aggressive filtering can leave empty history on follow-up turns, starter fallback is the right second-order fix. New tests cover both behaviors. Still ship it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: slack Channel integration: slack size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants