openclaw - ✅(Solved) Fix Provider plugin createStreamFn context lacks per-conversation identity (sessionId / sessionKey) [3 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#73488Fetched 2026-04-29 06:19:14
View on GitHub
Comments
0
Participants
1
Timeline
7
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×4referenced ×3

The ProviderCreateStreamFnContext passed to plugin createStreamFn (declared in src/plugins/types.ts) only includes { config, agentDir, workspaceDir, provider, modelId, model }. It does NOT include sessionId or sessionKey. As a result, any provider plugin that needs to scope per-call state by conversation (session resumption, billing/usage, MCP routing, prompt caching, audit logs, conversation-aware retries) is forced to either (a) collapse all conversations of a given agent into one bucket, or (b) resort to brittle workarounds (hashing the first user message, watching .lock files, parsing the system prompt for owner/channel hints).

Root Cause

The ProviderCreateStreamFnContext passed to plugin createStreamFn (declared in src/plugins/types.ts) only includes { config, agentDir, workspaceDir, provider, modelId, model }. It does NOT include sessionId or sessionKey. As a result, any provider plugin that needs to scope per-call state by conversation (session resumption, billing/usage, MCP routing, prompt caching, audit logs, conversation-aware retries) is forced to either (a) collapse all conversations of a given agent into one bucket, or (b) resort to brittle workarounds (hashing the first user message, watching .lock files, parsing the system prompt for owner/channel hints).

Fix Action

Fix / Workaround

Summary

The ProviderCreateStreamFnContext passed to plugin createStreamFn (declared in src/plugins/types.ts) only includes { config, agentDir, workspaceDir, provider, modelId, model }. It does NOT include sessionId or sessionKey. As a result, any provider plugin that needs to scope per-call state by conversation (session resumption, billing/usage, MCP routing, prompt caching, audit logs, conversation-aware retries) is forced to either (a) collapse all conversations of a given agent into one bucket, or (b) resort to brittle workarounds (hashing the first user message, watching .lock files, parsing the system prompt for owner/channel hints).

Why workarounds don't work

  • First user message hash → broken by limitHistoryTurns (drops the earliest user turns once the cap is hit). Also collides on common openings ("hi", "hola", a sticker reaction).
  • System prompt content match → only gives (agent, channel, owner) granularity; multiple threads on the same (channel, owner) collide. No conversation-stable thread id is encoded in the prompt.
  • Filesystem-watch on .jsonl.lock → racy under concurrency.
  • process.cwd() → resolves to effectiveWorkspace, which is per-agent.
  • StreamOptions → only signal, temperature, apiKey, transport, maxTokens (in pi-ai's StreamOptions); no place for conversation id without an arbitrary Record<string, unknown> extension.

PR fix notes

PR #73492: feat: expose sessionId and sessionKey to provider createStreamFn plugins

Description (problem / solution / changelog)

Fixes #73488.

Summary

Adds two optional fields, sessionId and sessionKey, to ProviderCreateStreamFnContext so provider plugins that need per-conversation state (CLI session resumption, billing buckets, MCP routing, prompt-cache scoping, audit trails) can finally distinguish concurrent conversations of the same agent.

The information already exists at runtime — attempt.ts:1581 has params.sessionId and params.sessionKey in scope when it calls registerProviderStreamForModel. This PR just plumbs them through.

Concrete failure mode (the trigger for this work)

A Claude-CLI-backed provider plugin (zeulewan/glueclaw) keys its session map by agentDir, because that's the only identity-bearing field the runtime exposes. The result: an agent in three concurrent conversations (e.g. two DMs and a group) resumes against a single shared Claude CLI session, leaking transcripts between conversations.

The full investigation, including why every plugin-side workaround fails (compaction drops the first-turn anchor, "hi"/"hola" collide, system prompt only encodes (agent, channel, owner) granularity, process.cwd() resolves to effectiveWorkspace per-agent, StreamOptions has no conversation slot, no per-call env vars), is in #73488.

Changes

  • src/plugins/types.ts: add sessionId?: string and sessionKey?: string to ProviderCreateStreamFnContext. Documented when each is populated and which to prefer (sessionKey when present — stable across session resets for the same logical conversation).
  • src/agents/provider-stream.ts: accept sessionId and sessionKey on registerProviderStreamForModel, propagate to the plugin context.
  • src/agents/pi-embedded-runner/run/attempt.ts: pass params.sessionId and params.sessionKey (already in scope) through.
  • src/agents/pi-embedded-runner/compact.ts: same for the compaction codepath; threaded through resolveCompactionProviderStream.
  • src/agents/btw.ts: same for the side-question codepath; uses the local sessionId and params.sessionKey.
  • The two one-off paths (src/media-understanding/image.ts, src/agents/simple-completion-transport.ts) intentionally leave the new fields undefined — they do not run inside an established conversation.

Backward compatibility

Fully compatible. Both fields are optional. Existing plugins that don't read them are unaffected. No public API surface is removed or renamed. Existing tests pass without modification (they use expect.objectContaining, tolerant to additional keys).

Test plan

  • pnpm tsgo:core — clean.
  • pnpm exec vitest run src/agents/btw.test.ts src/agents/pi-embedded-runner/compact.hooks.test.ts src/media-understanding/image.test.ts — 127/127 passing.
  • CI on this PR.
  • Downstream verification: paired PR in zeulewan/glueclaw will consume ctx.sessionKey (falling back to ctx.agentDir when absent) and exercise multi-conversation isolation against a real Claude CLI.

Changed files

  • src/agents/btw.ts (modified, +2/-0)
  • src/agents/pi-embedded-runner/compact.ts (modified, +6/-0)
  • src/agents/pi-embedded-runner/run/attempt.ts (modified, +2/-0)
  • src/agents/provider-stream.ts (modified, +4/-0)
  • src/plugins/types.ts (modified, +23/-0)

PR #4: fix: scope Claude CLI sessions per OpenClaw conversation, not per agent

Description (problem / solution / changelog)

Summary

  • The plugin's session map was keyed by agentDir, which collapsed all of an agent's concurrent conversations into a single Claude CLI session. Two DMs and a group on the same agent would resume against the same underlying CLI process, leaking transcripts between conversations.
  • OpenClaw owns the per-conversation identity (sessionKey, sessionId) but historically did not propagate it to provider plugins. The companion change in openclaw exposes both fields in ProviderCreateStreamFnContext (openclaw/openclaw#73488). This PR consumes them with a clear precedence.

Precedence

  1. ctx.sessionKey — semantic, stable across session resets for the same logical conversation.
  2. ctx.sessionId — UUID fallback, rotates on reset.
  3. ctx.agentDir — historical behavior, used when openclaw is too old to expose the new fields.
  4. "default" — final safety net; should never hit in practice.

Changes

  • src/session-key.ts: new pure helper resolveSessionKey.
  • index.ts: read the new optional sessionId/sessionKey fields from ctx and feed them through resolveSessionKey.
  • src/__tests__/unit/pure-functions.test.ts: 7 new tests — precedence, distinctness across conversations, stability across turns, empty-string guard, fallback chains.

Forward / backward compatibility

When running against an OpenClaw build that has not yet shipped openclaw#73488, ctx.sessionKey and ctx.sessionId are undefined and the helper collapses back to per-agent — the historical behavior. The fix auto-activates the moment the upstream openclaw change lands.

Test plan

  • npm run typecheck
  • npm run test (91 / 7 skipped — 7 new unit tests)

Changed files

  • index.ts (modified, +8/-2)
  • src/__tests__/unit/pure-functions.test.ts (modified, +67/-0)
  • src/session-key.ts (added, +27/-0)

PR #29: fix: scope Claude CLI sessions per OpenClaw conversation, not per agent

Description (problem / solution / changelog)

Fixes #28. Pairs with openclaw/openclaw#73488 / openclaw/openclaw#73492.

Summary

  • The plugin's session map was keyed by agentDir, which collapsed all of an agent's concurrent conversations into a single Claude CLI session. Two DMs and a group on the same agent resume against the same underlying CLI process, leaking transcripts between conversations.
  • OpenClaw owns the per-conversation identity (sessionKey, sessionId) but historically did not propagate it to provider plugins. The companion change in openclaw exposes both fields in ProviderCreateStreamFnContext. This PR consumes them with a clear precedence.

Precedence

  1. ctx.sessionKey — semantic, stable across session resets for the same logical conversation.
  2. ctx.sessionId — UUID fallback, rotates on reset.
  3. ctx.agentDir — historical behavior, used when running against an OpenClaw build older than openclaw#73492.
  4. "default" — final safety net.

Changes

  • src/session-key.ts: new pure helper resolveSessionKey.
  • index.ts: read the new optional sessionId/sessionKey fields from ctx and feed them through resolveSessionKey.
  • src/__tests__/unit/pure-functions.test.ts: 7 new tests — precedence, distinctness across conversations, stability across turns, empty-string guard, fallback chains.

Forward / backward compatibility

Against current OpenClaw, ctx.sessionKey and ctx.sessionId are undefined and behavior collapses to historical per-agent (the bug). The fix is a no-op until openclaw ships the upstream context change, then auto-activates. Safe to merge independently of openclaw#73492.

Test plan

  • npm run typecheck
  • npm run test (91 / 7 skipped — 7 new unit tests)
  • End-to-end verification: requires a local OpenClaw built from openclaw#73492.

Changed files

  • README.md (modified, +12/-0)
  • index.ts (modified, +23/-2)
  • src/__tests__/integration/mock-claude.mjs (modified, +14/-0)
  • src/__tests__/integration/stream.test.ts (modified, +190/-0)
  • src/__tests__/unit/pure-functions.test.ts (modified, +67/-0)
  • src/session-key.ts (added, +27/-0)
  • src/stream.ts (modified, +8/-4)

Code Example

export type ProviderCreateStreamFnContext = {
  config?: OpenClawConfig;
  agentDir?: string;
  workspaceDir?: string;
  provider: string;
  modelId: string;
  model: ProviderRuntimeModel;
  /** OpenClaw conversation session id (the `<uuid>.jsonl` filename). */
  sessionId?: string;
  /** OpenClaw semantic session key (channel/group/sender-encoded). Use this in preference to sessionId for routing decisions when present. */
  sessionKey?: string;
};
RAW_BUFFERClick to expand / collapse

Summary

The ProviderCreateStreamFnContext passed to plugin createStreamFn (declared in src/plugins/types.ts) only includes { config, agentDir, workspaceDir, provider, modelId, model }. It does NOT include sessionId or sessionKey. As a result, any provider plugin that needs to scope per-call state by conversation (session resumption, billing/usage, MCP routing, prompt caching, audit logs, conversation-aware retries) is forced to either (a) collapse all conversations of a given agent into one bucket, or (b) resort to brittle workarounds (hashing the first user message, watching .lock files, parsing the system prompt for owner/channel hints).

Concrete failure mode

Real-world example from a Claude-CLI-backed provider plugin (zeulewan/glueclaw):

  • The plugin spawns the claude CLI per turn and persists the CLI's session UUID in a map keyed by sessionKey (the only identity-bearing input the plugin's wrapper receives).
  • The map ends up keyed by agentDir (since that's all the runtime exposes).
  • For an agent with multiple concurrent conversations (e.g. an agent in two DMs and a group), every conversation resumes against the same Claude CLI session, producing transcript bleed between conversations and a single shared identity.

The information needed to fix this exists at runtime in src/agents/pi-embedded-runner/run/attempt.ts:1530-1547: activeSession.sessionId, params.sessionKey, params.sessionFile are all in scope when registerProviderStreamForModel is called at attempt.ts:1581. They simply aren't propagated to the plugin context.

Why workarounds don't work

  • First user message hash → broken by limitHistoryTurns (drops the earliest user turns once the cap is hit). Also collides on common openings ("hi", "hola", a sticker reaction).
  • System prompt content match → only gives (agent, channel, owner) granularity; multiple threads on the same (channel, owner) collide. No conversation-stable thread id is encoded in the prompt.
  • Filesystem-watch on .jsonl.lock → racy under concurrency.
  • process.cwd() → resolves to effectiveWorkspace, which is per-agent.
  • StreamOptions → only signal, temperature, apiKey, transport, maxTokens (in pi-ai's StreamOptions); no place for conversation id without an arbitrary Record<string, unknown> extension.

Proposal

Add two optional fields to ProviderCreateStreamFnContext:

export type ProviderCreateStreamFnContext = {
  config?: OpenClawConfig;
  agentDir?: string;
  workspaceDir?: string;
  provider: string;
  modelId: string;
  model: ProviderRuntimeModel;
  /** OpenClaw conversation session id (the `<uuid>.jsonl` filename). */
  sessionId?: string;
  /** OpenClaw semantic session key (channel/group/sender-encoded). Use this in preference to sessionId for routing decisions when present. */
  sessionKey?: string;
};

Plumb them through registerProviderStreamForModel and into the few call sites that already have them in scope:

  • src/agents/pi-embedded-runner/run/attempt.ts (the main per-turn path) — has params.sessionId, params.sessionKey.
  • src/agents/pi-embedded-runner/compact.ts — has both via params.
  • src/agents/btw.ts — has params.sessionId, params.sessionKey.
  • src/media-understanding/image.ts, src/agents/simple-completion-transport.ts — one-off completions, pass undefined (already optional).

Fully backward compatible: existing plugins that don't read the new fields are unaffected.

Repro

  1. Run an agent in two distinct DMs concurrently using the glueclaw provider plugin.
  2. Observe .glueclaw/sessions.json: only one entry per agent, regardless of conversation count.
  3. Send messages in DM A; resume in DM B and the agent has DM A's transcript visible to it.

A PR implementing the proposal will be opened shortly.

extent analysis

TL;DR

Add sessionId and sessionKey fields to ProviderCreateStreamFnContext to enable conversation-scoped state management in provider plugins.

Guidance

  • Update the ProviderCreateStreamFnContext type to include optional sessionId and sessionKey fields.
  • Modify registerProviderStreamForModel to pass sessionId and sessionKey to the plugin context when available.
  • Verify that the updated context is correctly propagated to provider plugins by checking the sessionId and sessionKey values in the plugin's createStreamFn.
  • Test the updated implementation using the repro steps provided to ensure conversation-scoped state management works as expected.

Example

export type ProviderCreateStreamFnContext = {
  // ...
  sessionId?: string;
  sessionKey?: string;
};

Notes

The proposed change is fully backward compatible, and existing plugins that don't use the new fields will remain unaffected.

Recommendation

Apply the proposed workaround by adding the sessionId and sessionKey fields to ProviderCreateStreamFnContext and updating the relevant code paths to propagate these values to the plugin context. This change will enable conversation-scoped state management in provider plugins, resolving the issue.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix Provider plugin createStreamFn context lacks per-conversation identity (sessionId / sessionKey) [3 pull requests, 1 participants]