fix(sessions): reset stale per-channel origin fields on channel switch#95328
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed June 20, 2026, 9:06 PM ET / 01:06 UTC. Summary PR surface: Source +13, Tests +227, Other +10. Total +250 across 10 files. Reproducibility: yes. at source level. Current mergeOrigin preserves omitted native origin fields, Slack can supply NativeChannelId, and Telegram direct-message context can omit it; I did not run a live two-transport repro in this read-only review. Review metrics: 2 noteworthy metrics.
Stored data model Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land one canonical session-origin fix after confirming the field-clearing contract, and split or explicitly prove the cross-OS script/test-routing changes before merging this branch. Do we have a high-confidence way to reproduce the issue? Yes, at source level. Current mergeOrigin preserves omitted native origin fields, Slack can supply NativeChannelId, and Telegram direct-message context can omit it; I did not run a live two-transport repro in this read-only review. Is this the best way to solve the issue? Yes for the core code location: mergeOrigin is the shared metadata boundary and the patch preserves same-channel sparse enrichment. The whole branch still needs maintainer selection against the sibling PR and proof or scoping for the unrelated script changes. AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 8b4eedf1bc6f. Label changesLabel justifications:
Evidence reviewedPR surface: Source +13, Tests +227, Other +10. Total +250 across 10 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
In a dmScope:"main" session shared across channels, mergeOrigin only overwrote a field when the new inbound supplied it. Telegram DMs carry no nativeChannelId/threadId, so a Slack -> Telegram switch left the prior Slack channel id (and threadId/nativeDirectUserId) stamped on a now-Telegram session, keeping reactions, native threading, and status reads pointed at the previous channel until the user messaged the original channel again. Treat a provider/surface change as a fresh channel identity and drop the channel-keyed fields so an inbound that omits them no longer inherits the previous channel's values. Same-provider turns still preserve sparse metadata as before.
…ontext builder Drive the production buildChannelInboundEventContext so the bug premise is proven rather than assumed: a Slack DM turn populates ctx.NativeChannelId (from conversation.nativeChannelId) while a Telegram DM turn omits it, then run that real context through deriveSessionMetaPatch to show the stale id is reset.
|
Merged via squash.
Thanks @ZengWen-DT! |
What Problem This Solves
Fixes #95325. With
session.dmScope: "main", a user's DMs to an agent across channels (Slack, Telegram, webchat) share one session. On a channel switch (e.g. Slack → Telegram), the session origin's per-channel identity fields are not reset.mergeOrigininsrc/config/sessions/metadata.tsmerges field-by-field, overwriting a field only when the new inbound supplies it. Telegram DMs carry nonativeChannelId(orthreadId), so the previous channel's Slack channel id persists in a now-Telegram session. It keeps persisting on every subsequent Telegram turn until the user messages on the original channel again or the session resets.route.target.to(delivery) updates correctly, so message delivery is unaffected. But native-channel-keyed operations (reactions, native threading, native message references) and any status/inspection that readsorigin.nativeChannelIdreference the previous channel for the entire duration of the new-channel session — e.g. an agent on Telegram reports the prior Slack account/channel when asked about its current context.Why This Change Was Made
The sparse merge exists so consecutive turns on the same channel keep previously known fields when an inbound legitimately omits them. That intent is correct within a channel, but a provider/surface change is a fresh channel identity: the channel-keyed fields (
nativeChannelId,nativeDirectUserId,threadId) belong to the prior channel and must not survive into the new one.The fix treats a
provider(orsurface) change as a channel switch and drops those three channel-keyed fields before the merge, so an inbound that omits them no longer inherits the previous channel's values. Provider/surface-agnostic and always-supplied fields (label,from,to,accountId,chatType) are untouched and still update as before. Same-provider turns keep the existing sparse-merge behavior. This matches the root cause and suggested fix in the issue and keeps the change bounded to the one helper that owns origin merging.User Impact
Native-channel-keyed operations and context/status reads now reflect the channel the session is actually on after a cross-channel switch in a
dmScope: "main"session, instead of pointing at the previous channel. No config, migration, or delivery-path change; same-channel sessions behave exactly as before.Evidence
Added
src/config/sessions/origin-channel-switch.test.ts, exercising the real runtime path (deriveSessionMetaPatch, the function called per inbound turn insrc/auto-reply/reply/session.ts) with realistic Slack-then-TelegramMsgContextinputs.Before (bug reproduced on the pre-fix
mergeOrigin) — stashing only the fix and running the test shows the stale Slack channel id persisting on the Telegram session, exactly as reported:Root cause / premise — the two passing tests above confirm the other half of the contract is preserved both with and without the fix: when the new channel does supply
nativeChannelId/threadIdthey are adopted, and same-provider turns still retain sparse metadata. So the regression is exactly the cross-provider omit case.After (with the fix):
Test cases: (1) Slack→Telegram with Telegram omitting channel id → stale Slack
nativeChannelId/nativeDirectUserId/threadIdcleared,provider/surface/accountIdbecome Telegram; (2) no re-stamp of the prior channel id on subsequent Telegram turns; (3) new channel's identity adopted when the inbound supplies it; (4) same-provider sparse turns still preserve the established channel id and thread.Formatting verified with
oxfmt --checkon both touched files (clean). Full lint/typecheck/build run in CI.Real behavior proof notes
dmScope: "main"session keeps the prior channel'snativeChannelId/threadIdafter a Slack→Telegram switch.deriveSessionMetaPatch/mergeOriginderivation path (the per-turn runtime entry point), Node 24.src/config/sessions/metadata.tsmergeOriginonly assigned each field whennextsupplied it; Telegram DMMsgContextcarries noNativeChannelId/MessageThreadId, so the prior values survived.dmScope:"main"session) was not run; the bug lives entirely in pure metadata derivation, so the unit test exercises the exact failing code path deterministically.Real behavior proof — production context builder (premise closed)
This proof drives the production shared inbound-event context builder
buildChannelInboundEventContext(src/channels/inbound-event/context.ts:515, whereNativeChannelId = reply.nativeChannelId ?? conversation.nativeChannelId) rather than hand-settingctx.NativeChannelId. That proves the premise the bug rests on — a real Slack DM context suppliesNativeChannelId, a real Telegram DM context omits it — and then runs that real context through the exact per-inbound-turn runtime pathderiveSessionMetaPatch → mergeOrigin.Behavior addressed: After a
dmScope:"main"session switches provider (Slack → Telegram), session origin drops the prior channel'snativeChannelId/nativeDirectUserId/threadId.Real environment tested: Linux (WSL2), Node v24.14.0, branch
fix/95325-reset-stale-origin-on-channel-switchat commit1a59d76a86.Exact command run:
Evidence after fix:
Evidence before fix (the
channelChangedclear block temporarily removed frommergeOrigin):Observed result: With the fix removed, the context produced by the real builder keeps the stale
D111SLACKon the Telegram turn (3 failures, including the real-context case). With the fix, the stale id is reset on the provider switch (6/6 pass). The premise case passes in both states, confirming the supply/omit asymmetry is produced by the real builder, not assumed by the test.What was not tested: a live two-bot Slack↔Telegram round-trip over real transports (requires live Slack + Telegram credentials). The bug lives entirely in pure metadata derivation; this proof exercises the real production context builder and the real merge function — the exact code each inbound turn runs.
Risk checklist
Did user-visible behavior change?
Yes(post-channel-switch origin now reflects the current channel; same-channel behavior unchanged)Did config, environment, or migration behavior change?
NoAI-assisted.