fix: persist outbound sends and skip stale cron deliveries#50092
fix: persist outbound sends and skip stale cron deliveries#50092
Conversation
Greptile SummaryThis PR bundles three related delivery fixes: (1) outbound sends are now persisted into session transcripts via a Key changes:
Confidence Score: 4/5
Prompt To Fix All With AIThis 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..." |
| if (store[key]?.sessionId) { | ||
| return null; // Already exists | ||
| } |
There was a problem hiding this 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:
| 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.| 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`, | ||
| ); |
There was a problem hiding this 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:
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.There was a problem hiding this comment.
💡 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".
src/config/sessions/transcript.ts
Outdated
| // Normalize session key to match store format (lowercased keys). | ||
| const normalizedKey = sessionKey.toLowerCase(); | ||
| const entry = (store[normalizedKey] ?? store[sessionKey]) as SessionEntry | undefined; |
There was a problem hiding this comment.
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 👍 / 👎.
fa2d38e to
74adf41
Compare
… 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)
74adf41 to
99e978f
Compare
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟡 Unbounded global Slack channel type cache can cause memory exhaustion and Slack API rate-limit amplification
Description
Impact:
Why this is attacker-reachable:
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);RecommendationImplement a bounded, expiring cache and validate Slack IDs before attempting lookups. Suggested changes:
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 2. 🔵 Unvalidated chat GUID from BlueBubbles /chat/new response can misroute attachments/messages
DescriptionThe new exported This
If the BlueBubbles server response is malformed/buggy, or if an attacker can tamper with responses (e.g., non-TLS 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 RecommendationFail closed and validate the returned chat GUID before using it:
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 Last updated on: 2026-03-19T04:10:15Z |
There was a problem hiding this comment.
💡 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".
| const resolver = OUTBOUND_SESSION_RESOLVERS[params.channel]; | ||
| if (!resolver) { | ||
| return resolveFallbackSession(nextParams); | ||
| } | ||
| return await resolver(nextParams); |
There was a problem hiding this comment.
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 👍 / 👎.
| if (lower.startsWith("group:") || lower.startsWith("chat:")) { | ||
| trimmed = trimmed.replace(/^(group|chat):/i, "").trim(); |
There was a problem hiding this comment.
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 👍 / 👎.
| const isGroup = | ||
| lower.startsWith("chat_id:") || | ||
| lower.startsWith("chat_guid:") || | ||
| lower.startsWith("chat_identifier:") || | ||
| lower.startsWith("group:"); |
There was a problem hiding this comment.
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 👍 / 👎.
…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
…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
…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
…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
…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
…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
…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)
…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)
Summary
This branch bundles a few related delivery and session tracking fixes:
What changed
Outbound session persistence
BlueBubbles
Cron stale delivery protection
Testing
pnpm vitest run src/cron/isolated-agent/delivery-dispatch.double-announce.test.tsNotes
This should prevent cases where a failed or delayed morning briefing finally completes the next day and gets delivered alongside the current briefing.