Skip to content

fix(sessions): reset stale per-channel origin fields on channel switch#95328

Merged
jalehman merged 10 commits into
openclaw:mainfrom
ZengWen-DT:fix/95325-reset-stale-origin-on-channel-switch
Jun 21, 2026
Merged

fix(sessions): reset stale per-channel origin fields on channel switch#95328
jalehman merged 10 commits into
openclaw:mainfrom
ZengWen-DT:fix/95325-reset-stale-origin-on-channel-switch

Conversation

@ZengWen-DT

@ZengWen-DT ZengWen-DT commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

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.

mergeOrigin in src/config/sessions/metadata.ts merges field-by-field, overwriting a field only when the new inbound supplies it. Telegram DMs carry no nativeChannelId (or threadId), 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 reads origin.nativeChannelId reference 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 (or surface) 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 in src/auto-reply/reply/session.ts) with realistic Slack-then-Telegram MsgContext inputs.

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:

FAIL  src/config/sessions/origin-channel-switch.test.ts > clears stale channel-keyed fields when provider changes and the new turn omits them
AssertionError: expected 'D111SLACK' to be undefined
   expect(afterTelegram.origin?.nativeChannelId).toBeUndefined();

FAIL  src/config/sessions/origin-channel-switch.test.ts > does not re-stamp the prior channel id on subsequent same-channel turns
AssertionError: expected 'D111SLACK' to be undefined
   expect(afterTelegramAgain.origin?.nativeChannelId).toBeUndefined();
  Tests  2 failed | 2 passed (4)

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/threadId they are adopted, and same-provider turns still retain sparse metadata. So the regression is exactly the cross-provider omit case.

After (with the fix):

$ node scripts/run-vitest.mjs src/config/sessions/origin-channel-switch.test.ts
 Test Files  1 passed (1)
      Tests  4 passed (4)

Test cases: (1) Slack→Telegram with Telegram omitting channel id → stale Slack nativeChannelId/nativeDirectUserId/threadId cleared, provider/surface/accountId become 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 --check on both touched files (clean). Full lint/typecheck/build run in CI.

Real behavior proof notes

  • Behavior addressed: cross-channel dmScope: "main" session keeps the prior channel's nativeChannelId/threadId after a Slack→Telegram switch.
  • Environment tested: local unit test against the actual deriveSessionMetaPatch/mergeOrigin derivation path (the per-turn runtime entry point), Node 24.
  • Root cause evidence: src/config/sessions/metadata.ts mergeOrigin only assigned each field when next supplied it; Telegram DM MsgContext carries no NativeChannelId/MessageThreadId, so the prior values survived.
  • What was not tested: a two-bot live round-trip (a configured Slack bot and a Telegram bot sharing one 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, where NativeChannelId = reply.nativeChannelId ?? conversation.nativeChannelId) rather than hand-setting ctx.NativeChannelId. That proves the premise the bug rests on — a real Slack DM context supplies NativeChannelId, a real Telegram DM context omits it — and then runs that real context through the exact per-inbound-turn runtime path deriveSessionMetaPatch → mergeOrigin.

Behavior addressed: After a dmScope:"main" session switches provider (Slack → Telegram), session origin drops the prior channel's nativeChannelId/nativeDirectUserId/threadId.

Real environment tested: Linux (WSL2), Node v24.14.0, branch fix/95325-reset-stale-origin-on-channel-switch at commit 1a59d76a86.

Exact command run:

node scripts/run-vitest.mjs src/config/sessions/origin-channel-switch.test.ts --reporter=verbose

Evidence after fix:

 ✓ (real inbound-event context builder) > confirms the premise: Slack DM context supplies NativeChannelId, Telegram DM context omits it
 ✓ (real inbound-event context builder) > resets the stale Slack channel id after a real-context Slack->Telegram switch
 Test Files  1 passed (1)
      Tests  6 passed (6)

Evidence before fix (the channelChanged clear block temporarily removed from mergeOrigin):

 × > clears stale channel-keyed fields when provider changes and the new turn omits them
   → expected 'D111SLACK' to be undefined
 × > does not re-stamp the prior channel id on subsequent same-channel turns
   → expected 'D111SLACK' to be undefined
 × (real inbound-event context builder) > resets the stale Slack channel id after a real-context Slack->Telegram switch
   → expected 'D111SLACK' to be undefined
      Tests  3 failed | 3 passed (6)

Observed result: With the fix removed, the context produced by the real builder keeps the stale D111SLACK on 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? No


AI-assisted.

@clawsweeper

clawsweeper Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 20, 2026, 9:06 PM ET / 01:06 UTC.

Summary
The branch clears stale session-origin identity fields on provider, surface, or account changes, adds channel-switch regression coverage, and also changes release-check signal cleanup plus test-project routing.

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.

  • Persisted Origin Fields Cleared: 4 cleared on identity change. The PR changes durable session-origin retention for nativeChannelId, nativeDirectUserId, accountId, and threadId.
  • Non-Session Automation Surface: 1 signal path and 1 test-routing list changed. The branch modifies release-check process handling and test selection outside the linked session bug, so proof scope matters before merge.

Stored data model
Persistent data-model change detected: serialized state: src/config/sessions/origin-channel-switch.test.ts, unknown-data-model-change: src/config/sessions/origin-channel-switch.test.ts, vector/embedding metadata: src/config/sessions/origin-channel-switch.test.ts. Confirm migration or upgrade compatibility proof before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #95325
Summary: This PR is one candidate fix for the canonical stale session-origin issue; one sibling candidate remains open and an earlier provider-only candidate is closed unmerged as superseded.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🦐 gold shrimp
Proof: 🦐 gold shrimp
Patch quality: 🐚 platinum hermit
Result: blocked until stronger real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

Proof guidance:

  • [P1] Needs stronger real behavior proof before merge: Insufficient: terminal proof covers the session metadata and inbound-builder path, but the branch also changes release-check/test routing without after-fix terminal or log proof; add redacted proof or split those changes, then update the PR body for re-review.

Risk before merge

  • [P1] The PR intentionally changes durable session-origin semantics by clearing accountId as well as native channel fields on provider, surface, or account changes; maintainers should confirm that exact stored-field contract.
  • [P1] The branch changes cross-OS release-check child-process signal handling and test-project routing without after-fix terminal/log proof for that automation path.
  • [P1] The open sibling fix(sessions): reset stale per-channel origin fields on channel switch (#95325) #95332 targets the same root bug with positive proof, so maintainers should choose one canonical branch before landing either implementation.

Maintainer options:

  1. Narrow And Prove The Selected Branch (recommended)
    Pick the canonical session-origin PR, keep the stored-origin contract explicit, and either split or add redacted terminal/log proof for the release-check and test-routing changes before merge.
  2. Accept The Broader Scope
    Maintainers can intentionally land the session fix plus automation changes together if they accept the compatibility and automation risk after owner review.
  3. Pause For The Sibling
    If fix(sessions): reset stale per-channel origin fields on channel switch (#95325) #95332 is the preferred canonical branch, preserve any useful tests from this PR and close or pause this branch as superseded.

Next step before merge

  • [P1] Needs maintainer choice between same-root PRs plus a proof or scope decision for the non-session automation changes; there is no narrow autonomous repair lane.

Security
Cleared: Cleared: no dependency, lockfile, secret, workflow, or package-resolution change was found; the script child-process change was reviewed as a code-execution surface with no concrete security concern.

Review details

Best 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 changes

Label justifications:

  • P2: The PR addresses a real session metadata bug with limited blast radius and no evidence that primary message delivery is broken.
  • merge-risk: 🚨 compatibility: Clearing persisted origin account and native channel fields can change existing shared-session upgrade behavior.
  • merge-risk: 🚨 session-state: The diff changes how shared-session origin identity is retained or cleared when a DM session moves between channel identities.
  • merge-risk: 🚨 automation: The branch also changes cross-OS release-check child-process handling and test-project routing, which affects maintainer automation outside the session fix.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦐 gold shrimp and patch quality is 🐚 platinum hermit.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: Insufficient: terminal proof covers the session metadata and inbound-builder path, but the branch also changes release-check/test routing without after-fix terminal or log proof; add redacted proof or split those changes, then update the PR body for re-review.
Evidence reviewed

PR surface:

Source +13, Tests +227, Other +10. Total +250 across 10 files.

View PR surface stats
Area Files Added Removed Net
Source 5 18 5 +13
Tests 3 227 0 +227
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 2 10 0 +10
Total 10 255 5 +250

What I checked:

  • Repository policy read and applied: Root AGENTS.md and scoped scripts/extensions/test/channels guidance were read; session-state compatibility, script-runner proof, and Telegram proof standards affected this review. (AGENTS.md:14, 8b4eedf1bc6f)
  • Current main still preserves omitted origin fields: mergeOrigin copies the existing origin and only overwrites nativeChannelId, nativeDirectUserId, accountId, and threadId when the next inbound supplies them, so stale omitted fields can persist. (src/config/sessions/metadata.ts:14, 8b4eedf1bc6f)
  • Latest release still has the same sparse merge: v2026.6.8 contains the same mergeOrigin behavior, so the central bug is not already shipped as fixed. (src/config/sessions/metadata.ts:14, 844f405ac1be)
  • PR head changes the durable merge boundary: The PR deletes nativeChannelId, nativeDirectUserId, accountId, and threadId before applying new values when provider, surface, or account changes. (src/config/sessions/metadata.ts:23, 3a946cb078d8)
  • Runtime callers persist the metadata patch: The reply path and session store call deriveSessionMetaPatch with the existing session entry before applying or persisting session metadata. (src/auto-reply/reply/session.ts:710, 8b4eedf1bc6f)
  • Channel premise matches source: The shared inbound builder copies native channel identity only when supplied; Slack seeds nativeChannelId from the message channel while Telegram direct-message construction omits nativeChannelId. (src/channels/inbound-event/context.ts:515, 8b4eedf1bc6f)

Likely related people:

  • steipete: Commit 5f22b68 introduced the session origin metadata helper that owns mergeOrigin. (role: introduced behavior; confidence: high; commits: 5f22b68268b9; files: src/config/sessions/metadata.ts)
  • gumadeiras: Commit dcd0cf9 added native channel identity semantics across session metadata, types, and templating. (role: adjacent routing contributor; confidence: medium; commits: dcd0cf9f98a5; files: src/config/sessions/metadata.ts, src/config/sessions/types.ts, src/auto-reply/templating.ts)
  • mbelinky: Merged PR Gateway/sessions: preserve shared session route on system events #66073 touched shared session route/origin preservation near this durable-session behavior. (role: adjacent session-route contributor; confidence: medium; commits: 527895f0362f; files: src/config/sessions/metadata.ts, src/auto-reply/reply/session.ts)
  • vincentkoc: Current blame attributes the present mergeOrigin and release-check script lines to recent main carry-forward commits, so this is a useful routing signal but not the original feature provenance. (role: recent line owner; confidence: low; commits: 8ff1d3e67bbc, b43eedbb1818; files: src/config/sessions/metadata.ts, scripts/openclaw-cross-os-release-checks.ts)
  • jalehman: The PR timeline shows recent force-pushes and branch commits by this handle, including account-clearing and script follow-up changes now under maintainer review. (role: recent follow-up owner; confidence: medium; commits: 4d115ef1adc0, ab9d13db4bc5, 8d31b40a87fc; files: src/config/sessions/metadata.ts, src/config/sessions/origin-channel-switch.test.ts, scripts/openclaw-cross-os-release-checks.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

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
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. labels Jun 20, 2026
@jalehman jalehman self-assigned this Jun 20, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Jun 20, 2026
@jalehman jalehman requested review from a team as code owners June 20, 2026 22:24
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation gateway Gateway runtime cli CLI command changes scripts Repository scripts commands Command implementations docker Docker and sandbox tooling agents Agent runtime and tooling channel: feishu Channel integration: feishu extensions: qa-lab labels Jun 20, 2026
@openclaw-barnacle openclaw-barnacle Bot added size: S and removed docs Improvements or additions to documentation app: web-ui App: web-ui scripts Repository scripts agents Agent runtime and tooling extensions: codex extensions: copilot size: XL labels Jun 20, 2026
ZengWen-DT and others added 10 commits June 20, 2026 17:49
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.
@jalehman

Copy link
Copy Markdown
Contributor

Merged via squash.

Thanks @ZengWen-DT!

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

Labels

extensions: vercel-ai-gateway extensions: xai extensions: zai merge-risk: 🚨 automation 🚨 May affect CI, automerge, proof capture, label sync, or maintainer automation. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 session-state 🚨 May lose, corrupt, stale, or mis-associate session, agent, or context state. P2 Normal backlog priority with limited blast radius. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. scripts Repository scripts size: M status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Session origin retains stale per-channel fields (nativeChannelId, threadId) across a channel switch in a dmScope:"main" session

2 participants