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.
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
initialHistoryLimit: 10andinheritParent: true.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: 10config, 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:
src/config/sessions/reset-policy.ts:22,src/config/sessions/reset-policy.ts:25.sessionStartedAt, with legacy fallbacks:src/config/sessions/reset-policy.ts:78,src/config/sessions/lifecycle.ts:120.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.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.src/config/sessions/session-accessor.ts:2733,src/gateway/session-transcript-files.fs.ts:357,src/gateway/session-transcript-files.fs.ts:391, andsrc/gateway/session-transcript-files.fs.ts:440.Slack thread routing and history seeding:
extensions/slack/src/threading.ts:13.: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.historyScope: "thread",inheritParent: false, andinitialHistoryLimit: 20:extensions/slack/src/monitor/provider.ts:279,extensions/slack/src/monitor/message-handler/prepare-thread-context.ts:219.initialHistoryLimit: 10changes 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.inheritParent: truesets 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.resolveSlackThreadContextDatadecides whether a thread is new by checking only whetherreadSessionUpdatedAt()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.extensions/slack/src/monitor/message-handler/prepare-thread-context.ts:221.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:
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.extensions/slack/src/monitor/message-handler/prepare.ts:1321.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, andsrc/auto-reply/reply/inbound-meta.ts:723.ThreadHistoryBodyis 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:
extensions/slack/src/monitor/message-handler/prepare.test.ts:1597.extensions/slack/src/monitor/message-handler/prepare.test.ts:1856.Likely Root Cause
Slack thread history seeding treats a persisted thread session row as proof that the thread is not new:
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 thereadSessionUpdatedAt()-only "new thread session" decision with a reset-aware decision:resolveSessionResetPolicyandevaluateSessionFreshnesswith the relevant Slack/channel override.forceInitialHistory: trueonly for that stale-row case.initialHistoryLimit: 0as an explicit disable.inheritParentas the explicit switch for parent transcript forking.Acceptance Criteria
initialHistoryLimit: 10, the stale post-reset case fetches up to 10 prior Slack thread messages.initialHistoryLimit: 0still disables Slack thread history seeding.historyScope: "thread"andhistoryScope: "channel"keep their existing inbound history-key semantics; the fix should not redefine that option.inheritParent: trueremains parent-channel transcript fork behavior only; it does not replace Slack thread-history seeding for the same thread.Suggested Tests
initialHistoryLimit: 10. Assert that Slackconversations.repliesis used,ThreadHistoryBodyis populated, and any reset-aware first-turn metadata is asserted only if the implementation introduces or reuses such a signal.inheritParent: trueremains parent-fork behavior and does not itself restore same-thread Slack replies.Edge Cases To Preserve
initialHistoryLimitmessages.conversations.replieserrors should still degrade to no thread history rather than failing the reply.inheritParent: trueshould not duplicate the same context through both parent transcript fork and Slack thread history.Notes
This is not caused by a low
initialHistoryLimitor byinheritParentsemantics. In the reported incident,initialHistoryLimit: 10should 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: truecan attach the parent channel session, but it does not fetch Slack thread replies and does not restore the old same-thread transcript.The
sessions_historytool 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