You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #954 fixes the proactive-post amnesia case (a user replying to a thread bootstrapped by an agent-initiated send_slack_message lands on a session with no transcript), but the implementation opened the gate too wide: history backfill now re-adopts every bot-authored message in a thread, including the bot's own prior replies that already exist in the session's persisted transcript. Result: adopted-context shows the bot's previous turns as if they were external speakers, and the model sees its own outputs replayed back as conversation context.
Repro
Live dogfood session on a regular (non-proactive-bootstrap) Slack thread. After a daemon restart triggered a recovery + history backfill:
Recovered from snapshot (turns=3, history=13) — session already had a full transcript
Fetched 9 thread history messages — backfill returned 9 entries instead of the 3 user-authored entries the previous filter would have returned
The next approval prompt's adopted-context block listed two speakers: the human user and the bot's own user ID (the workspace user the daemon authenticates as)
The watermark dedup the PR relied on is doing nothing here:
Pre-restart, the cursor was stuck at the thread root and not advancing across hydration calls (cursor=1778553106.206079 repeated across 02:33 and 02:43 fetches)
Even if the watermark did advance, it filters by Slack ts — it cannot tell a bot reply we wrote ourselves from one we didn't
Why this design was wrong
The PR conflated two different sets:
"Messages from bots" — what the change filters on
"Messages we couldn't have seen via our own producer-side transcripts" — what actually matters for backfill
Those overlap at exactly one entry: the proactive-post root that bootstrapped the thread. Every other bot message in the thread was produced by some session of ours, which wrote it to its own persisted transcript at the time. Server-side history backfill is only useful for content we never produced ourselves.
The PR's defense — "watermark dedups, caught-up sessions don't refetch" — is load-bearing on a primitive that (a) can lag advancement, and (b) doesn't filter on the right axis. The deleted Filters_out_bot_messages test was the boundary that prevented this exact failure mode.
Historical context
The original problem PR #954 was trying to solve is real and still needs a fix: when a DeliveryKind.None reminder fires and the daemon's ephemeral session calls send_slack_message and terminates, the new Slack thread that's created has no producing session. A user reply lands on a freshly-bootstrapped destination session whose transcript is empty, so the agent has amnesia about the message it just posted.
Before #954, SlackThreadHistoryFetcher and DiscordThreadHistoryFetcher both unconditionally skipped bot-authored entries in server-side thread history. That filter was correct for normal threads (where the bot's prior turns are already in the session transcript) but it dropped the proactive-post root in the bootstrap case, leaving the destination session with nothing to adopt.
#954 fixed amnesia by removing the bot-message filter outright. That fixes the bootstrap case but introduces the regression described above for every other thread.
OpenSpec change add-proactive-channel-sessions-conformance (delta on thread-history-backfill) captures the intent but with the wrong design constraint.
Proposed fix
Restrict bot-authored backfill to the thread root only:
if (message.IsBotAuthored && message.Ts != thread.ParentTs)
continue;
(Plus the analogous check in DiscordThreadHistoryFetcher for the page iterator and the raw-message-fetch site; the thread-root resolution site already operates on the root specifically.)
This satisfies both invariants with a one-line predicate per fetcher:
Proactive-post amnesia stays fixed — the proactive post IS the thread root, so it surfaces.
No re-adopting our own outputs — every other bot message in the thread is produced by one of our sessions and already in transcript; the root-check filters them out.
Tests that change:
Includes_bot_messages_with_bot_id_as_sender_when_user_is_missing — reshape so the bot reply at non-root is filtered, the bot post at root is included
New regression test: bot message at thread root is included; bot message later in the thread is excluded
New regression test: a session with prior transcript that recovers and backfills does not re-adopt its own bot turns
Out of scope
The watermark dedup behavior (cursor not advancing reliably) is a separate issue worth tracing, but it's no longer in the critical path once the root-only filter is in place — the watermark becomes a cost optimization, not a correctness primitive.
Summary
PR #954 fixes the proactive-post amnesia case (a user replying to a thread bootstrapped by an agent-initiated
send_slack_messagelands on a session with no transcript), but the implementation opened the gate too wide: history backfill now re-adopts every bot-authored message in a thread, including the bot's own prior replies that already exist in the session's persisted transcript. Result: adopted-context shows the bot's previous turns as if they were external speakers, and the model sees its own outputs replayed back as conversation context.Repro
Live dogfood session on a regular (non-proactive-bootstrap) Slack thread. After a daemon restart triggered a recovery + history backfill:
Recovered from snapshot (turns=3, history=13)— session already had a full transcriptFetched 9 thread history messages— backfill returned 9 entries instead of the 3 user-authored entries the previous filter would have returnedThe watermark dedup the PR relied on is doing nothing here:
cursor=1778553106.206079repeated across 02:33 and 02:43 fetches)ts— it cannot tell a bot reply we wrote ourselves from one we didn'tWhy this design was wrong
The PR conflated two different sets:
Those overlap at exactly one entry: the proactive-post root that bootstrapped the thread. Every other bot message in the thread was produced by some session of ours, which wrote it to its own persisted transcript at the time. Server-side history backfill is only useful for content we never produced ourselves.
The PR's defense — "watermark dedups, caught-up sessions don't refetch" — is load-bearing on a primitive that (a) can lag advancement, and (b) doesn't filter on the right axis. The deleted
Filters_out_bot_messagestest was the boundary that prevented this exact failure mode.Historical context
The original problem PR #954 was trying to solve is real and still needs a fix: when a
DeliveryKind.Nonereminder fires and the daemon's ephemeral session callssend_slack_messageand terminates, the new Slack thread that's created has no producing session. A user reply lands on a freshly-bootstrapped destination session whose transcript is empty, so the agent has amnesia about the message it just posted.Before #954,
SlackThreadHistoryFetcherandDiscordThreadHistoryFetcherboth unconditionally skipped bot-authored entries in server-side thread history. That filter was correct for normal threads (where the bot's prior turns are already in the session transcript) but it dropped the proactive-post root in the bootstrap case, leaving the destination session with nothing to adopt.#954 fixed amnesia by removing the bot-message filter outright. That fixes the bootstrap case but introduces the regression described above for every other thread.
OpenSpec change
add-proactive-channel-sessions-conformance(delta onthread-history-backfill) captures the intent but with the wrong design constraint.Proposed fix
Restrict bot-authored backfill to the thread root only:
(Plus the analogous check in
DiscordThreadHistoryFetcherfor the page iterator and the raw-message-fetch site; the thread-root resolution site already operates on the root specifically.)This satisfies both invariants with a one-line predicate per fetcher:
Tests that change:
Includes_bot_messages_with_bot_id_as_sender_when_user_is_missing— reshape so the bot reply at non-root is filtered, the bot post at root is includedOut of scope
References
fix(channels): include bot messages in thread history backfill) — the change being amendedsrc/Netclaw.Channels.Slack/SlackThreadHistoryFetcher.cssrc/Netclaw.Channels.Discord/Transport/DiscordThreadHistoryFetcher.cssrc/Netclaw.Actors.Tests/Channels/SlackThreadHistoryFetcherTests.cssrc/Netclaw.Actors.Tests/Channels/DiscordThreadHistoryFetcherTests.csopenspec/changes/add-proactive-channel-sessions-conformance/