Skip to content

Slack thread replies after daily reset should seed thread history #97073

Description

@bek91

Slack thread replies after daily reset should seed thread history

Summary

When a Slack user replies in an existing thread after the daily session reset boundary, OpenClaw can start a fresh prompt without the prior Slack thread context. The previous OpenClaw transcript may still be manually recoverable through session history tooling, but that recovery path does not seed the next agent turn.

This appears to be a bug at the boundary between Slack thread-history seeding and lazy daily session rollover.

Observed Behavior

  • A user replied in an existing Slack thread roughly 12 hours after prior work in the same thread.
  • The reply crossed the default daily reset boundary.
  • The reported incident config had Slack thread history enabled with initialHistoryLimit: 10 and inheritParent: true.
  • The agent responded as if it had no previous thread context.
  • The previous persisted session history was still discoverable later through manual session history tooling, but that recovery path is outside prompt assembly.

Expected Behavior

For an existing Slack thread whose OpenClaw session has become stale because of the reset policy, the next inbound Slack reply should still seed bounded Slack API thread history into the prompt, subject to initialHistoryLimit.

For the reported initialHistoryLimit: 10 config, that means the post-reset turn should be eligible to fetch up to 10 prior Slack thread messages. The fix should preserve the same prompt-size guard for fresh existing sessions while avoiding a context-free first turn after reset rollover.

Current-Code Evidence

Daily reset behavior:

  • Daily reset is lazy on the next inbound turn, not scheduled in the background.
  • The default reset policy is daily at 4am local time: src/config/sessions/reset-policy.ts:22, src/config/sessions/reset-policy.ts:25.
  • Freshness evaluation uses sessionStartedAt, with legacy fallbacks: src/config/sessions/reset-policy.ts:78, src/config/sessions/lifecycle.ts:120.
  • Slack direct replies initialize session state through initSessionState, not the gateway HTTP agent method: src/auto-reply/reply/session.ts:295, src/auto-reply/reply/session.ts:504, src/auto-reply/reply/session.ts:540.
  • When the existing session is stale, initialization archives the old transcript and creates a new session id for the same session key: src/auto-reply/reply/session.ts:550, src/auto-reply/reply/session.ts:636, src/auto-reply/reply/session.ts:717, src/auto-reply/reply/session.ts:852.
  • Transcript archive writes are handled by src/config/sessions/session-accessor.ts:2733, src/gateway/session-transcript-files.fs.ts:357, src/gateway/session-transcript-files.fs.ts:391, and src/gateway/session-transcript-files.fs.ts:440.

Slack thread routing and history seeding:

  • Slack thread replies are recognized from Slack thread metadata: extensions/slack/src/threading.ts:13.
  • Slack thread session keys use a :thread:<threadTs> suffix: extensions/slack/src/monitor/message-handler/prepare-routing.ts:151, extensions/slack/src/monitor/message-handler/prepare-routing.ts:181, extensions/slack/src/monitor/message-handler/prepare-routing.ts:266, src/routing/session-key.ts:337.
  • The relevant Slack defaults are historyScope: "thread", inheritParent: false, and initialHistoryLimit: 20: extensions/slack/src/monitor/provider.ts:279, extensions/slack/src/monitor/message-handler/prepare-thread-context.ts:219.
  • A configured initialHistoryLimit: 10 changes the Slack API backfill cap, not the new/existing session gate. The fetch path still runs only when Slack prepare thinks the thread session is new, or when forced: extensions/slack/src/monitor/message-handler/prepare-thread-context.ts:219, extensions/slack/src/monitor/message-handler/prepare-thread-context.ts:221.
  • A configured inheritParent: true sets a parent channel session key for thread replies, but it does not fetch Slack thread replies or restore the old same-thread transcript: extensions/slack/src/monitor/message-handler/prepare-routing.ts:266, src/auto-reply/reply/session-parent-fork-prepare.ts:16.
  • resolveSlackThreadContextData decides whether a thread is new by checking only whether readSessionUpdatedAt() returns a value: extensions/slack/src/monitor/message-handler/prepare-thread-context.ts:98, extensions/slack/src/monitor/message-handler/prepare-thread-context.ts:128, extensions/slack/src/monitor/message-handler/prepare-thread-context.ts:198.
  • Slack thread history is fetched only when there is no previous session timestamp, or when forced for special cases: extensions/slack/src/monitor/message-handler/prepare-thread-context.ts:221.
  • The history fetch uses Slack conversations.replies, excludes the current message, and keeps the latest bounded set: extensions/slack/src/monitor/thread.ts:136, extensions/slack/src/monitor/thread.ts:160.

Prompt assembly:

  • Prepared Slack context includes inbound history, session keys, thread metadata, and supplemental thread bodies: extensions/slack/src/monitor/message-handler/prepare.ts:1215, extensions/slack/src/monitor/message-handler/prepare.ts:1251, extensions/slack/src/monitor/message-handler/prepare.ts:1264, extensions/slack/src/monitor/message-handler/prepare.ts:1319.
  • The thread starter body is currently included only when there is no previous thread timestamp: extensions/slack/src/monitor/message-handler/prepare.ts:1321.
  • Supplemental inbound context is converted into prompt context through src/auto-reply/reply/inbound-context.ts:47, src/auto-reply/reply/inbound-meta.ts:503, src/auto-reply/reply/inbound-meta.ts:548, src/auto-reply/reply/inbound-meta.ts:593, src/auto-reply/reply/inbound-meta.ts:656, and src/auto-reply/reply/inbound-meta.ts:723.
  • ThreadHistoryBody is rendered as [Thread history - for context]: src/auto-reply/reply/get-reply-run.ts:724, src/auto-reply/reply/get-reply-run.ts:805.

Existing test coverage:

  • New thread sessions load initial Slack thread history: extensions/slack/src/monitor/message-handler/prepare.test.ts:1597.
  • Fresh existing thread sessions intentionally skip loading Slack history to avoid prompt bloat: extensions/slack/src/monitor/message-handler/prepare.test.ts:1856.
  • There does not appear to be coverage for an existing thread session row that is stale according to the daily reset policy.

Likely Root Cause

Slack thread history seeding treats a persisted thread session row as proof that the thread is not new:

const isNewThreadSession = !threadSessionPreviousTimestamp;

That check is based on row existence/update time, not reset-policy freshness. When a Slack thread session row exists but is stale, Slack preparation can skip bounded Slack thread history. The same inbound turn is then initialized as a fresh OpenClaw session because the old session crossed the daily reset boundary. The resulting prompt is new from OpenClaw's perspective but was not seeded from Slack thread history.

Proposed Fix

Make Slack thread-history seeding freshness-aware.

In resolveSlackThreadContextData, or a helper it calls, replace the readSessionUpdatedAt()-only "new thread session" decision with a reset-aware decision:

  • Load enough session lifecycle state for the thread session key.
  • Apply resolveSessionResetPolicy and evaluateSessionFreshness with the relevant Slack/channel override.
  • Treat stale existing thread rows as "new for initial Slack history seeding".
  • A narrow implementation shape is to compute whether the existing thread session will roll over before thread context resolution, then pass forceInitialHistory: true only for that stale-row case.
  • Preserve the current skip behavior for fresh existing sessions.
  • Preserve initialHistoryLimit: 0 as an explicit disable.
  • Do not replay archived OpenClaw transcripts automatically; seed bounded Slack thread history only.
  • Keep inheritParent as the explicit switch for parent transcript forking.

Acceptance Criteria

  • A Slack reply in an existing thread after the daily reset boundary includes bounded Slack API thread history in the next prompt.
  • With initialHistoryLimit: 10, the stale post-reset case fetches up to 10 prior Slack thread messages.
  • A Slack reply in an existing fresh thread still skips Slack history loading by default.
  • initialHistoryLimit: 0 still disables Slack thread history seeding.
  • historyScope: "thread" and historyScope: "channel" keep their existing inbound history-key semantics; the fix should not redefine that option.
  • inheritParent: true remains parent-channel transcript fork behavior only; it does not replace Slack thread-history seeding for the same thread.
  • Existing Slack API failure fallback behavior remains unchanged.

Suggested Tests

  • Add a Slack prepare test where a thread session row exists but is stale by daily reset policy and initialHistoryLimit: 10. Assert that Slack conversations.replies is used, ThreadHistoryBody is populated, and any reset-aware first-turn metadata is asserted only if the implementation introduces or reuses such a signal.
  • Keep the existing fresh-row bloat guard test that skips thread history for active thread sessions.
  • Keep the new-thread initial history test.
  • Add or preserve coverage showing inheritParent: true remains parent-fork behavior and does not itself restore same-thread Slack replies.
  • Optionally add an integration-level test through reply session initialization to assert that rollover archives the old OpenClaw transcript while the reset turn's prompt receives bounded Slack thread history.

Edge Cases To Preserve

  • Very long Slack threads should still retain only the latest initialHistoryLimit messages.
  • The current inbound Slack message should still be excluded from fetched thread history.
  • Bot/current assistant reply filtering should not change.
  • Slack conversations.replies errors should still degrade to no thread history rather than failing the reply.
  • inheritParent: true should not duplicate the same context through both parent transcript fork and Slack thread history.
  • Runtime/configured conversation bindings that bypass the normal thread suffix should not seed unrelated Slack history.
  • Existing DM-thread special-case behavior that forces initial history should remain intact.

Notes

This is not caused by a low initialHistoryLimit or by inheritParent semantics. In the reported incident, initialHistoryLimit: 10 should have been enough to seed bounded Slack thread history if Slack prepare had treated the post-reset row as a first turn for history seeding. inheritParent: true can attach the parent channel session, but it does not fetch Slack thread replies and does not restore the old same-thread transcript.

The sessions_history tool may help a user recover prior transcript content after the fact, but that is not part of the automatic prompt path and should not be required for the first post-reset Slack reply to carry thread context.

Agent Transcript

Redacted Codex session transcript
source: local Codex session
redaction: raw tool outputs, system/developer prompts, absolute local paths, secrets, browser/session/auth details

[user]
Requested an isolated worktree named slack-thread-seeding, based on latest main, with setup run before edits.

[assistant]
Created the dedicated worktree on branch fix/slack-thread-seeding, ran pnpm install, reported status, and waited for confirmation before editing.

[user]
Requested a source-backed investigation of Slack thread context after daily reset, including the reported Slack thread config with initialHistoryLimit=10 and inheritParent=true.

[assistant]
Performed a read-only trace through Slack routing, Slack thread-history seeding, session reset policy, reply session initialization, prompt assembly, transcript history tooling, docs, and adjacent tests. The investigation found that Slack thread-history seeding uses readSessionUpdatedAt() as the new/existing gate, while daily reset freshness is evaluated later during reply session initialization.

[assistant]
Concluded that the behavior is a current-code semantics mismatch: the stale Slack thread row is treated as existing for Slack history seeding, then rolled to a fresh OpenClaw session for the model turn. initialHistoryLimit=10 caps the Slack API backfill only after the fetch path runs, and inheritParent=true attaches/forks parent channel context without restoring same-thread Slack replies.

[user]
Requested a temporary Markdown GitHub issue draft based on the findings.

[assistant]
Created tmp/slack-thread-seeding-issue.md with observed behavior, expected behavior, code evidence, root cause, proposed fix, acceptance criteria, tests, and edge cases.

[user]
Requested a review of the draft for documentation quality, logical flow, contradictions, and errors.

[assistant]
Reviewed and tightened the draft, then revised it again after comparison with another Codex review so the issue uses the reported initialHistoryLimit=10 and inheritParent=true incident config.

[user]
Approved posting the issue with a redacted transcript.

Metadata

Metadata

Assignees

Labels

P2Normal backlog priority with limited blast radius.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.maintainerMaintainer-authored PR

Type

No type

Fields

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions