Skip to content

fix: persist outbound sends and skip stale cron deliveries#50092

Merged
tyler6204 merged 4 commits intomainfrom
fix/sessions-send-persist
Mar 19, 2026
Merged

fix: persist outbound sends and skip stale cron deliveries#50092
tyler6204 merged 4 commits intomainfrom
fix/sessions-send-persist

Conversation

@tyler6204
Copy link
Copy Markdown
Member

Summary

This branch bundles a few related delivery and session tracking fixes:

  1. Persist outbound sends into session transcripts and session routing state so follow up sends keep the right destination context.
  2. Improve BlueBubbles delivery by auto creating chats for new phone numbers before sending attachments or messages.
  3. Add a stale cron delivery guard so an isolated cron run that finishes hours later does not announce an out of date briefing.

What changed

Outbound session persistence

  • persist outbound sends to session transcripts
  • keep outbound session routing state in sync for new target sends
  • add regression coverage around outbound session tracking
  • add Slack coverage for new target sends

BlueBubbles

  • auto create chats for new numbers before message sends
  • reuse that flow for attachment sends too
  • add coverage for the new send paths

Cron stale delivery protection

  • treat a cron delivery as stale when it is more than 3 hours past the run's scheduled slot
  • skip the final direct announce delivery for those stale runs while still returning a successful cron result
  • mark delivery as intentionally handled so the fallback main summary path does not replay the stale content
  • add regression coverage for the stale delivery case

Testing

  • pnpm vitest run src/cron/isolated-agent/delivery-dispatch.double-announce.test.ts

Notes

This should prevent cases where a failed or delayed morning briefing finally completes the next day and gets delivered alongside the current briefing.

@openclaw-barnacle openclaw-barnacle bot added channel: bluebubbles Channel integration: bluebubbles channel: slack Channel integration: slack size: L maintainer Maintainer-authored PR labels Mar 19, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 19, 2026

Greptile Summary

This PR bundles three related delivery fixes: (1) outbound sends are now persisted into session transcripts via a recordSessionMetaFromInbound fallback path so follow-up sends retain the correct routing context; (2) BlueBubbles attachments and message sends auto-create a DM chat for new phone-number handles before attempting the send, with the creation logic extracted into the new exported createChatForHandle helper; (3) a stale-delivery guard is added to cron runs — if the job's scheduled slot is more than 3 hours in the past, the final direct-announce delivery is skipped while deliveryAttempted is set so the fallback main-summary path is also suppressed.

Key changes:

  • createChatForHandle in send.ts is exported and handles multiple BlueBubbles response shapes (chatGuid, guid, nested chats array) for robust GUID extraction
  • attachments.ts auto-creates a DM chat and retries GUID resolution before throwing
  • transcript.ts now normalises session keys to lowercase before store lookup, matching how normalizeStoreSessionKey writes them
  • Stale-delivery check in delivery-dispatch.ts uses job.state.nextRunAtMs (falling back to runStartedAt) as the scheduled-at reference
  • Session-store fallback in outbound-session.ts: the return null; // Already exists comment is misleading — updateSessionStore unconditionally saves the store after the mutator, so the early-return only avoids mutating the entry, not the write itself (harmless but confusing)

Confidence Score: 4/5

  • This PR is safe to merge — all changes are functionally correct and well-tested; the two flagged items are minor style/documentation issues.
  • All three feature areas (session persistence, BlueBubbles auto-chat-creation, stale cron guard) have targeted regression tests. The logic is sound. The only issues found are: (1) a misleading // Already exists comment in the updateSessionStore callback that implies the write is skipped when it is not, and (2) two independent Date.now() calls in the stale-delivery guard that could diverge by milliseconds in production. Neither is a functional bug.
  • No files require special attention beyond the two style notes in src/infra/outbound/outbound-session.ts and src/cron/isolated-agent/delivery-dispatch.ts.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/infra/outbound/outbound-session.ts
Line: 1002-1004

Comment:
**Misleading "Already exists" comment on `return null`**

The comment `// Already exists` on the early-return `null` implies that returning `null` from the mutator will skip the store write. However, `updateSessionStore` always calls `saveSessionStoreUnlocked` after the mutator returns — the return value has no effect on whether the store is persisted. In the "already exists" path, the store is loaded and then written back unchanged (an unnecessary disk write), but the store is always saved.

The comment should clarify this to avoid future maintainers assuming a "bail out" contract:

```suggestion
        if (store[key]?.sessionId) {
          return null; // Entry exists; store will still be saved but unchanged
        }
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/cron/isolated-agent/delivery-dispatch.ts
Line: 360-367

Comment:
**`Date.now()` in log diverges from the timestamp used in the staleness check**

`isStaleCronDelivery` captures its own `nowMs = Date.now()` snapshot. The log message then calls `Date.now()` again independently, so the "age" printed in the log can differ slightly from the age that triggered the guard. In fast-running tests with fake timers this is consistent, but in production both calls are live reads so they can diverge (albeit by milliseconds).

A cleaner approach is to capture `nowMs` once before the `isStaleCronDelivery` call and pass it through:

```typescript
const nowMs = Date.now();
if (
  params.deliveryRequested &&
  isStaleCronDelivery({ job: params.job, runStartedAt: params.runStartedAt, nowMs })
) {
  deliveryAttempted = true;
  const scheduledAtMs = resolveCronDeliveryScheduledAtMs({
    job: params.job,
    runStartedAt: params.runStartedAt,
  });
  logWarn(
    `[cron:${params.job.id}] skipping stale delivery scheduled at ${new Date(scheduledAtMs).toISOString()}, age ${Math.round((nowMs - scheduledAtMs) / 60_000)}m`,
  );
  ...
}
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "fix(cron): skip stal..."

Comment on lines +1002 to +1004
if (store[key]?.sessionId) {
return null; // Already exists
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Misleading "Already exists" comment on return null

The comment // Already exists on the early-return null implies that returning null from the mutator will skip the store write. However, updateSessionStore always calls saveSessionStoreUnlocked after the mutator returns — the return value has no effect on whether the store is persisted. In the "already exists" path, the store is loaded and then written back unchanged (an unnecessary disk write), but the store is always saved.

The comment should clarify this to avoid future maintainers assuming a "bail out" contract:

Suggested change
if (store[key]?.sessionId) {
return null; // Already exists
}
if (store[key]?.sessionId) {
return null; // Entry exists; store will still be saved but unchanged
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/outbound/outbound-session.ts
Line: 1002-1004

Comment:
**Misleading "Already exists" comment on `return null`**

The comment `// Already exists` on the early-return `null` implies that returning `null` from the mutator will skip the store write. However, `updateSessionStore` always calls `saveSessionStoreUnlocked` after the mutator returns — the return value has no effect on whether the store is persisted. In the "already exists" path, the store is loaded and then written back unchanged (an unnecessary disk write), but the store is always saved.

The comment should clarify this to avoid future maintainers assuming a "bail out" contract:

```suggestion
        if (store[key]?.sessionId) {
          return null; // Entry exists; store will still be saved but unchanged
        }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +360 to +367
deliveryAttempted = true;
const scheduledAtMs = resolveCronDeliveryScheduledAtMs({
job: params.job,
runStartedAt: params.runStartedAt,
});
logWarn(
`[cron:${params.job.id}] skipping stale delivery scheduled at ${new Date(scheduledAtMs).toISOString()}, age ${Math.round((Date.now() - scheduledAtMs) / 60_000)}m`,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Date.now() in log diverges from the timestamp used in the staleness check

isStaleCronDelivery captures its own nowMs = Date.now() snapshot. The log message then calls Date.now() again independently, so the "age" printed in the log can differ slightly from the age that triggered the guard. In fast-running tests with fake timers this is consistent, but in production both calls are live reads so they can diverge (albeit by milliseconds).

A cleaner approach is to capture nowMs once before the isStaleCronDelivery call and pass it through:

const nowMs = Date.now();
if (
  params.deliveryRequested &&
  isStaleCronDelivery({ job: params.job, runStartedAt: params.runStartedAt, nowMs })
) {
  deliveryAttempted = true;
  const scheduledAtMs = resolveCronDeliveryScheduledAtMs({
    job: params.job,
    runStartedAt: params.runStartedAt,
  });
  logWarn(
    `[cron:${params.job.id}] skipping stale delivery scheduled at ${new Date(scheduledAtMs).toISOString()}, age ${Math.round((nowMs - scheduledAtMs) / 60_000)}m`,
  );
  ...
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cron/isolated-agent/delivery-dispatch.ts
Line: 360-367

Comment:
**`Date.now()` in log diverges from the timestamp used in the staleness check**

`isStaleCronDelivery` captures its own `nowMs = Date.now()` snapshot. The log message then calls `Date.now()` again independently, so the "age" printed in the log can differ slightly from the age that triggered the guard. In fast-running tests with fake timers this is consistent, but in production both calls are live reads so they can diverge (albeit by milliseconds).

A cleaner approach is to capture `nowMs` once before the `isStaleCronDelivery` call and pass it through:

```typescript
const nowMs = Date.now();
if (
  params.deliveryRequested &&
  isStaleCronDelivery({ job: params.job, runStartedAt: params.runStartedAt, nowMs })
) {
  deliveryAttempted = true;
  const scheduledAtMs = resolveCronDeliveryScheduledAtMs({
    job: params.job,
    runStartedAt: params.runStartedAt,
  });
  logWarn(
    `[cron:${params.job.id}] skipping stale delivery scheduled at ${new Date(scheduledAtMs).toISOString()}, age ${Math.round((nowMs - scheduledAtMs) / 60_000)}m`,
  );
  ...
}
```

How can I resolve this? If you propose a fix, please make it concise.

@tyler6204 tyler6204 self-assigned this Mar 19, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fa2d38e239

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +157 to +159
// Normalize session key to match store format (lowercased keys).
const normalizedKey = sessionKey.toLowerCase();
const entry = (store[normalizedKey] ?? store[sessionKey]) as SessionEntry | undefined;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist session transcript updates with canonical key

This new normalized lookup allows mixed-case sessionKey inputs, but the function still continues with the original casing, and resolveAndPersistSessionFile persists using the exact key it receives. In mixed-case calls, that creates/updates a second store entry under a non-canonical key while the lowercase key remains stale, which can cause repeated reconciliation writes and split session metadata by casing. Use the normalized key for downstream persistence to keep session store state canonical.

Useful? React with 👍 / 👎.

tyler6204 added a commit that referenced this pull request Mar 19, 2026
@tyler6204 tyler6204 force-pushed the fix/sessions-send-persist branch from fa2d38e to 74adf41 Compare March 19, 2026 00:57
@openclaw-barnacle openclaw-barnacle bot added scripts Repository scripts size: XL and removed size: L labels Mar 19, 2026
… messages to session transcripts

Two fixes for BlueBubbles message tool behavior:

1. **Attachment sends to new phone numbers**: sendBlueBubblesAttachment now
   auto-creates a new DM chat (via /api/v1/chat/new) when no existing chat
   is found for a handle target, matching the behavior already present in
   sendMessageBlueBubbles for text sends. The existing createNewChatWithMessage
   is refactored into a reusable createChatForHandle that returns the chatGuid.

2. **Outbound message session persistence**: Ensures outbound messages sent
   via the message tool are reliably tracked in session transcripts:
   - ensureOutboundSessionEntry now falls back to directly creating a session
     store entry when recordSessionMetaFromInbound returns null, guaranteeing
     a sessionId exists for the subsequent mirror append.
   - appendAssistantMessageToSessionTranscript now normalizes the session key
     (lowercased) when looking up the store, preventing case mismatches
     between the store keys and the mirror sessionKey.

Tests added for all changes.
…r Slack

The shared infrastructure changes from the BlueBubbles fix (session key
normalization in transcript.ts and fallback session entry creation in
outbound-session.ts) already cover Slack. Slack's sendMessageSlack uses
conversations.open to auto-create DM channels for new user targets.

Add tests confirming:
- Slack user DM and channel session route resolution (outbound.test.ts)
- Slack session key normalization for transcript append (sessions.test.ts)
- Slack outbound sendText/sendMedia to new user and channel targets (channel.test.ts)
@tyler6204 tyler6204 force-pushed the fix/sessions-send-persist branch from 74adf41 to 99e978f Compare March 19, 2026 02:38
@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot bot commented Mar 19, 2026

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟡 Medium Unbounded global Slack channel type cache can cause memory exhaustion and Slack API rate-limit amplification
2 🔵 Low Unvalidated chat GUID from BlueBubbles /chat/new response can misroute attachments/messages

1. 🟡 Unbounded global Slack channel type cache can cause memory exhaustion and Slack API rate-limit amplification

Property Value
Severity Medium
CWE CWE-400
Location src/infra/outbound/outbound-session.ts:52-54

Description

resolveSlackChannelType() caches Slack channel type results in a process-global Map with no size bound or TTL, keyed by ${accountId}:${channelId}.

Impact:

  • Memory DoS: An attacker who can trigger outbound routing to many distinct Slack channelId values can grow SLACK_CHANNEL_TYPE_CACHE indefinitely, eventually exhausting memory.
  • API call amplification / rate-limit pressure: Each previously unseen (unique) channelId can trigger a conversations.info call (when a token is configured) and then be cached. An attacker can force many distinct cache misses to generate many Slack API calls.

Why this is attacker-reachable:

  • The outbound send paths derive routes from user-provided target (e.g., the gateway send method calls resolveOutboundSessionRoute(...) with target: deliveryTarget), which ultimately feeds parsed.id into resolveSlackChannelType() when it starts with G.
  • parseSlackTarget(..., { defaultKind: "channel" }) accepts arbitrary strings as IDs when defaultKind is set, so an attacker can supply many distinct G... identifiers.

Vulnerable code:

// process-global map, no bounds
const SLACK_CHANNEL_TYPE_CACHE = new Map<string, "channel" | "group" | "dm" | "unknown">();// on cache miss, may call Slack API and then inserts into the unbounded map
const cached = SLACK_CHANNEL_TYPE_CACHE.get(`${params.accountId ?? "default"}:${channelId}`);
...
const info = await client.conversations.info({ channel: channelId });
...
SLACK_CHANNEL_TYPE_CACHE.set(`${account.accountId}:${channelId}`, type);

Recommendation

Implement a bounded, expiring cache and validate Slack IDs before attempting lookups.

Suggested changes:

  1. Validate channelId format and length before caching / calling Slack:
  • Only allow known Slack conversation ID formats (e.g., C..., G..., D...) and enforce a reasonable max length.
  1. Replace the global Map with an LRU + TTL cache (or a small capped Map with eviction) and consider caching negative results with a shorter TTL.
  2. Optionally add in-flight de-duplication so concurrent misses for the same key share one API request.

Example (simple LRU-like eviction + TTL):

type CacheValue = { type: "channel"|"group"|"dm"|"unknown"; expiresAt: number };
const MAX = 1000;
const TTL_MS = 10 * 60 * 1000;
const cache = new Map<string, CacheValue>();

function get(key: string) {
  const v = cache.get(key);
  if (!v) return undefined;
  if (Date.now() > v.expiresAt) { cache.delete(key); return undefined; }// refresh LRU
  cache.delete(key); cache.set(key, v);
  return v.type;
}

function set(key: string, type: CacheValue["type"]) {
  cache.set(key, { type, expiresAt: Date.now() + TTL_MS });
  if (cache.size > MAX) {
    const oldestKey = cache.keys().next().value;
    cache.delete(oldestKey);
  }
}

function isValidSlackConversationId(id: string) {
  return /^[CGD][A-Z0-9]{8,}$/.test(id) && id.length <= 32;
}

Also ensure the cache key uses a normalized accountId consistently (use account.accountId for both get and set) to avoid accidental cache bypass and extra growth.


2. 🔵 Unvalidated chat GUID from BlueBubbles /chat/new response can misroute attachments/messages

Property Value
Severity Low
CWE CWE-345
Location extensions/bluebubbles/src/send.ts:371-389

Description

The new exported createChatForHandle() trusts the chatGuid returned by BlueBubbles’ /api/v1/chat/new response without validating it corresponds to the requested handle.

This chatGuid is then used by sendBlueBubblesAttachment() to route the attachment:

  • Input: address (derived from the tool/user-provided to handle)
  • Network boundary: response body from POST /api/v1/chat/new
  • Sink: attachment send uses chatGuid to choose the destination chat

If the BlueBubbles server response is malformed/buggy, or if an attacker can tamper with responses (e.g., non-TLS http:// deployments / on-path attacker), the client may accept a chatGuid pointing to a different conversation. That can lead to unintended delivery of attachments (and potentially messages) to the wrong chat/recipient.

Vulnerable code (chatGuid accepted from server response without verification):

chatGuid =
  (typeof data.chatGuid === "string" && data.chatGuid) ||
  (typeof data.guid === "string" && data.guid) ||
  null;

Additionally, JSON parse failures are ignored, allowing the function to return { chatGuid: null, messageId: "ok" }, which can mask server-side failures and complicate reliable delivery/error handling.

Recommendation

Fail closed and validate the returned chat GUID before using it:

  1. Validate the returned chatGuid is a DM GUID for the expected handle (and optionally service).
  2. If validation fails, ignore it and fall back to resolveChatGuidForTarget(); if still missing, throw.
  3. Consider throwing on non-empty bodies that fail JSON parsing when chatGuid is required.

Example hardening:

import { extractHandleFromChatGuid, normalizeBlueBubblesHandle } from "./targets.js";// after parsing chatGuid
const expected = normalizeBlueBubblesHandle(params.address);
const actual = chatGuid ? extractHandleFromChatGuid(chatGuid) : null;

if (!actual || actual !== expected) {// don’t trust mismatched/malformed GUIDs from the network
  chatGuid = null;
}

if (!chatGuid) {// optionally: resolveChatGuidForTarget({ ... target: {kind:"handle", address: expected }})// or throw if the operation requires a definitive destination.
}

Also ensure deployments use HTTPS to reduce the risk of on-path response tampering.


Analyzed PR: #50092 at commit 99e978f

Last updated on: 2026-03-19T04:10:15Z

@openclaw-barnacle openclaw-barnacle bot removed the scripts Repository scripts label Mar 19, 2026
@tyler6204 tyler6204 merged commit a290f5e into main Mar 19, 2026
34 of 42 checks passed
@tyler6204 tyler6204 deleted the fix/sessions-send-persist branch March 19, 2026 02:40
@tyler6204
Copy link
Copy Markdown
Member Author

tyler6204 commented Mar 19, 2026

Merged via squash.

Merge commit: a290f5e
Source head: 99e978f

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99e978fb11

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +958 to +962
const resolver = OUTBOUND_SESSION_RESOLVERS[params.channel];
if (!resolver) {
return resolveFallbackSession(nextParams);
}
return await resolver(nextParams);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve channel plugin outbound-session hooks

resolveOutboundSessionRoute no longer calls getChannelPlugin(...).messaging.resolveOutboundSessionRoute, so any channel plugin that defines custom outbound session routing is now silently bypassed and forced through the static resolver/fallback path. For third-party or future channels this changes session-key construction (for threads/group mapping/account-specific rules), which can split transcript history and break follow-up routing continuity on outbound sends.

Useful? React with 👍 / 👎.

Comment on lines +847 to +848
if (lower.startsWith("group:") || lower.startsWith("chat:")) {
trimmed = trimmed.replace(/^(group|chat):/i, "").trim();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle Feishu channel: targets as group chats

The Feishu resolver dropped support for the explicit channel: prefix that the plugin route previously treated as a group/chat target. With inputs like channel:oc_xxx, this branch now falls through as direct, leaving the prefix in the peer id and generating a different session key shape than existing group conversations, which causes outbound messages to fork from the established group session state.

Useful? React with 👍 / 👎.

Comment on lines +631 to +635
const isGroup =
lower.startsWith("chat_id:") ||
lower.startsWith("chat_guid:") ||
lower.startsWith("chat_identifier:") ||
lower.startsWith("group:");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Parse raw BlueBubbles chat IDs before inferring chat type

BlueBubbles chat-type detection now only treats prefixed forms (chat_id:, chat_guid:, chat_identifier:, group:) as group chats. Raw BlueBubbles identifiers that are accepted elsewhere (for example unprefixed chat GUIDs/identifiers returned by the API) will be misclassified as direct chats here, producing incompatible session keys and breaking outbound session continuity for those targets.

Useful? React with 👍 / 👎.

analysoor-assistant pushed a commit to analysoor-assistant/openclaw that referenced this pull request Mar 19, 2026
…50092)

* fix(bluebubbles): auto-create chats for new numbers, persist outbound messages to session transcripts

Two fixes for BlueBubbles message tool behavior:

1. **Attachment sends to new phone numbers**: sendBlueBubblesAttachment now
   auto-creates a new DM chat (via /api/v1/chat/new) when no existing chat
   is found for a handle target, matching the behavior already present in
   sendMessageBlueBubbles for text sends. The existing createNewChatWithMessage
   is refactored into a reusable createChatForHandle that returns the chatGuid.

2. **Outbound message session persistence**: Ensures outbound messages sent
   via the message tool are reliably tracked in session transcripts:
   - ensureOutboundSessionEntry now falls back to directly creating a session
     store entry when recordSessionMetaFromInbound returns null, guaranteeing
     a sessionId exists for the subsequent mirror append.
   - appendAssistantMessageToSessionTranscript now normalizes the session key
     (lowercased) when looking up the store, preventing case mismatches
     between the store keys and the mirror sessionKey.

Tests added for all changes.

* test(slack): verify outbound session tracking and new target sends for Slack

The shared infrastructure changes from the BlueBubbles fix (session key
normalization in transcript.ts and fallback session entry creation in
outbound-session.ts) already cover Slack. Slack's sendMessageSlack uses
conversations.open to auto-create DM channels for new user targets.

Add tests confirming:
- Slack user DM and channel session route resolution (outbound.test.ts)
- Slack session key normalization for transcript append (sessions.test.ts)
- Slack outbound sendText/sendMedia to new user and channel targets (channel.test.ts)

* fix(cron): skip stale delayed deliveries

* fix: prep PR openclaw#50092
cxa pushed a commit to cxa/openclaw that referenced this pull request Mar 19, 2026
…50092)

* fix(bluebubbles): auto-create chats for new numbers, persist outbound messages to session transcripts

Two fixes for BlueBubbles message tool behavior:

1. **Attachment sends to new phone numbers**: sendBlueBubblesAttachment now
   auto-creates a new DM chat (via /api/v1/chat/new) when no existing chat
   is found for a handle target, matching the behavior already present in
   sendMessageBlueBubbles for text sends. The existing createNewChatWithMessage
   is refactored into a reusable createChatForHandle that returns the chatGuid.

2. **Outbound message session persistence**: Ensures outbound messages sent
   via the message tool are reliably tracked in session transcripts:
   - ensureOutboundSessionEntry now falls back to directly creating a session
     store entry when recordSessionMetaFromInbound returns null, guaranteeing
     a sessionId exists for the subsequent mirror append.
   - appendAssistantMessageToSessionTranscript now normalizes the session key
     (lowercased) when looking up the store, preventing case mismatches
     between the store keys and the mirror sessionKey.

Tests added for all changes.

* test(slack): verify outbound session tracking and new target sends for Slack

The shared infrastructure changes from the BlueBubbles fix (session key
normalization in transcript.ts and fallback session entry creation in
outbound-session.ts) already cover Slack. Slack's sendMessageSlack uses
conversations.open to auto-create DM channels for new user targets.

Add tests confirming:
- Slack user DM and channel session route resolution (outbound.test.ts)
- Slack session key normalization for transcript append (sessions.test.ts)
- Slack outbound sendText/sendMedia to new user and channel targets (channel.test.ts)

* fix(cron): skip stale delayed deliveries

* fix: prep PR openclaw#50092
udftd pushed a commit to udftd/openclaw that referenced this pull request Mar 20, 2026
…50092)

* fix(bluebubbles): auto-create chats for new numbers, persist outbound messages to session transcripts

Two fixes for BlueBubbles message tool behavior:

1. **Attachment sends to new phone numbers**: sendBlueBubblesAttachment now
   auto-creates a new DM chat (via /api/v1/chat/new) when no existing chat
   is found for a handle target, matching the behavior already present in
   sendMessageBlueBubbles for text sends. The existing createNewChatWithMessage
   is refactored into a reusable createChatForHandle that returns the chatGuid.

2. **Outbound message session persistence**: Ensures outbound messages sent
   via the message tool are reliably tracked in session transcripts:
   - ensureOutboundSessionEntry now falls back to directly creating a session
     store entry when recordSessionMetaFromInbound returns null, guaranteeing
     a sessionId exists for the subsequent mirror append.
   - appendAssistantMessageToSessionTranscript now normalizes the session key
     (lowercased) when looking up the store, preventing case mismatches
     between the store keys and the mirror sessionKey.

Tests added for all changes.

* test(slack): verify outbound session tracking and new target sends for Slack

The shared infrastructure changes from the BlueBubbles fix (session key
normalization in transcript.ts and fallback session entry creation in
outbound-session.ts) already cover Slack. Slack's sendMessageSlack uses
conversations.open to auto-create DM channels for new user targets.

Add tests confirming:
- Slack user DM and channel session route resolution (outbound.test.ts)
- Slack session key normalization for transcript append (sessions.test.ts)
- Slack outbound sendText/sendMedia to new user and channel targets (channel.test.ts)

* fix(cron): skip stale delayed deliveries

* fix: prep PR openclaw#50092
fuller-stack-dev pushed a commit to fuller-stack-dev/openclaw that referenced this pull request Mar 20, 2026
…50092)

* fix(bluebubbles): auto-create chats for new numbers, persist outbound messages to session transcripts

Two fixes for BlueBubbles message tool behavior:

1. **Attachment sends to new phone numbers**: sendBlueBubblesAttachment now
   auto-creates a new DM chat (via /api/v1/chat/new) when no existing chat
   is found for a handle target, matching the behavior already present in
   sendMessageBlueBubbles for text sends. The existing createNewChatWithMessage
   is refactored into a reusable createChatForHandle that returns the chatGuid.

2. **Outbound message session persistence**: Ensures outbound messages sent
   via the message tool are reliably tracked in session transcripts:
   - ensureOutboundSessionEntry now falls back to directly creating a session
     store entry when recordSessionMetaFromInbound returns null, guaranteeing
     a sessionId exists for the subsequent mirror append.
   - appendAssistantMessageToSessionTranscript now normalizes the session key
     (lowercased) when looking up the store, preventing case mismatches
     between the store keys and the mirror sessionKey.

Tests added for all changes.

* test(slack): verify outbound session tracking and new target sends for Slack

The shared infrastructure changes from the BlueBubbles fix (session key
normalization in transcript.ts and fallback session entry creation in
outbound-session.ts) already cover Slack. Slack's sendMessageSlack uses
conversations.open to auto-create DM channels for new user targets.

Add tests confirming:
- Slack user DM and channel session route resolution (outbound.test.ts)
- Slack session key normalization for transcript append (sessions.test.ts)
- Slack outbound sendText/sendMedia to new user and channel targets (channel.test.ts)

* fix(cron): skip stale delayed deliveries

* fix: prep PR openclaw#50092
fuller-stack-dev pushed a commit to fuller-stack-dev/openclaw that referenced this pull request Mar 20, 2026
…50092)

* fix(bluebubbles): auto-create chats for new numbers, persist outbound messages to session transcripts

Two fixes for BlueBubbles message tool behavior:

1. **Attachment sends to new phone numbers**: sendBlueBubblesAttachment now
   auto-creates a new DM chat (via /api/v1/chat/new) when no existing chat
   is found for a handle target, matching the behavior already present in
   sendMessageBlueBubbles for text sends. The existing createNewChatWithMessage
   is refactored into a reusable createChatForHandle that returns the chatGuid.

2. **Outbound message session persistence**: Ensures outbound messages sent
   via the message tool are reliably tracked in session transcripts:
   - ensureOutboundSessionEntry now falls back to directly creating a session
     store entry when recordSessionMetaFromInbound returns null, guaranteeing
     a sessionId exists for the subsequent mirror append.
   - appendAssistantMessageToSessionTranscript now normalizes the session key
     (lowercased) when looking up the store, preventing case mismatches
     between the store keys and the mirror sessionKey.

Tests added for all changes.

* test(slack): verify outbound session tracking and new target sends for Slack

The shared infrastructure changes from the BlueBubbles fix (session key
normalization in transcript.ts and fallback session entry creation in
outbound-session.ts) already cover Slack. Slack's sendMessageSlack uses
conversations.open to auto-create DM channels for new user targets.

Add tests confirming:
- Slack user DM and channel session route resolution (outbound.test.ts)
- Slack session key normalization for transcript append (sessions.test.ts)
- Slack outbound sendText/sendMedia to new user and channel targets (channel.test.ts)

* fix(cron): skip stale delayed deliveries

* fix: prep PR openclaw#50092
pholpaphankorn pushed a commit to pholpaphankorn/openclaw that referenced this pull request Mar 22, 2026
…50092)

* fix(bluebubbles): auto-create chats for new numbers, persist outbound messages to session transcripts

Two fixes for BlueBubbles message tool behavior:

1. **Attachment sends to new phone numbers**: sendBlueBubblesAttachment now
   auto-creates a new DM chat (via /api/v1/chat/new) when no existing chat
   is found for a handle target, matching the behavior already present in
   sendMessageBlueBubbles for text sends. The existing createNewChatWithMessage
   is refactored into a reusable createChatForHandle that returns the chatGuid.

2. **Outbound message session persistence**: Ensures outbound messages sent
   via the message tool are reliably tracked in session transcripts:
   - ensureOutboundSessionEntry now falls back to directly creating a session
     store entry when recordSessionMetaFromInbound returns null, guaranteeing
     a sessionId exists for the subsequent mirror append.
   - appendAssistantMessageToSessionTranscript now normalizes the session key
     (lowercased) when looking up the store, preventing case mismatches
     between the store keys and the mirror sessionKey.

Tests added for all changes.

* test(slack): verify outbound session tracking and new target sends for Slack

The shared infrastructure changes from the BlueBubbles fix (session key
normalization in transcript.ts and fallback session entry creation in
outbound-session.ts) already cover Slack. Slack's sendMessageSlack uses
conversations.open to auto-create DM channels for new user targets.

Add tests confirming:
- Slack user DM and channel session route resolution (outbound.test.ts)
- Slack session key normalization for transcript append (sessions.test.ts)
- Slack outbound sendText/sendMedia to new user and channel targets (channel.test.ts)

* fix(cron): skip stale delayed deliveries

* fix: prep PR openclaw#50092
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 23, 2026
…50092)

* fix(bluebubbles): auto-create chats for new numbers, persist outbound messages to session transcripts

Two fixes for BlueBubbles message tool behavior:

1. **Attachment sends to new phone numbers**: sendBlueBubblesAttachment now
   auto-creates a new DM chat (via /api/v1/chat/new) when no existing chat
   is found for a handle target, matching the behavior already present in
   sendMessageBlueBubbles for text sends. The existing createNewChatWithMessage
   is refactored into a reusable createChatForHandle that returns the chatGuid.

2. **Outbound message session persistence**: Ensures outbound messages sent
   via the message tool are reliably tracked in session transcripts:
   - ensureOutboundSessionEntry now falls back to directly creating a session
     store entry when recordSessionMetaFromInbound returns null, guaranteeing
     a sessionId exists for the subsequent mirror append.
   - appendAssistantMessageToSessionTranscript now normalizes the session key
     (lowercased) when looking up the store, preventing case mismatches
     between the store keys and the mirror sessionKey.

Tests added for all changes.

* test(slack): verify outbound session tracking and new target sends for Slack

The shared infrastructure changes from the BlueBubbles fix (session key
normalization in transcript.ts and fallback session entry creation in
outbound-session.ts) already cover Slack. Slack's sendMessageSlack uses
conversations.open to auto-create DM channels for new user targets.

Add tests confirming:
- Slack user DM and channel session route resolution (outbound.test.ts)
- Slack session key normalization for transcript append (sessions.test.ts)
- Slack outbound sendText/sendMedia to new user and channel targets (channel.test.ts)

* fix(cron): skip stale delayed deliveries

* fix: prep PR openclaw#50092

(cherry picked from commit a290f5e)
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 23, 2026
…50092)

* fix(bluebubbles): auto-create chats for new numbers, persist outbound messages to session transcripts

Two fixes for BlueBubbles message tool behavior:

1. **Attachment sends to new phone numbers**: sendBlueBubblesAttachment now
   auto-creates a new DM chat (via /api/v1/chat/new) when no existing chat
   is found for a handle target, matching the behavior already present in
   sendMessageBlueBubbles for text sends. The existing createNewChatWithMessage
   is refactored into a reusable createChatForHandle that returns the chatGuid.

2. **Outbound message session persistence**: Ensures outbound messages sent
   via the message tool are reliably tracked in session transcripts:
   - ensureOutboundSessionEntry now falls back to directly creating a session
     store entry when recordSessionMetaFromInbound returns null, guaranteeing
     a sessionId exists for the subsequent mirror append.
   - appendAssistantMessageToSessionTranscript now normalizes the session key
     (lowercased) when looking up the store, preventing case mismatches
     between the store keys and the mirror sessionKey.

Tests added for all changes.

* test(slack): verify outbound session tracking and new target sends for Slack

The shared infrastructure changes from the BlueBubbles fix (session key
normalization in transcript.ts and fallback session entry creation in
outbound-session.ts) already cover Slack. Slack's sendMessageSlack uses
conversations.open to auto-create DM channels for new user targets.

Add tests confirming:
- Slack user DM and channel session route resolution (outbound.test.ts)
- Slack session key normalization for transcript append (sessions.test.ts)
- Slack outbound sendText/sendMedia to new user and channel targets (channel.test.ts)

* fix(cron): skip stale delayed deliveries

* fix: prep PR openclaw#50092

(cherry picked from commit a290f5e)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: bluebubbles Channel integration: bluebubbles channel: slack Channel integration: slack maintainer Maintainer-authored PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant