mirror of https://github.com/openclaw/openclaw.git
fix: include extension channels in subagent announce delivery path (#56348)
* fix: include extension channels in subagent announce delivery path * test: cover extension announce delivery routes
This commit is contained in:
parent
107969c725
commit
41cf93efff
|
|
@ -116,6 +116,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Discord/replies: preserve leading indentation when stripping inline reply tags so reply-tagged plain text and fenced code blocks keep their formatting. (#55960) Thanks @Nanako0129.
|
||||
- Daemon/status: surface immediate gateway close reasons from lightweight probes and prefer those concrete auth or pairing failures over generic timeouts in `openclaw daemon status`. (#56282) Thanks @mbelinky.
|
||||
- Agents/failover: classify HTTP 410 errors as retryable timeouts by default while still preserving explicit session-expired, billing, and auth signals from the payload. (#55201) thanks @nikus-pan.
|
||||
- Agents/subagents: restore completion announce delivery for extension channels like BlueBubbles. (#56348)
|
||||
|
||||
## 2026.3.24
|
||||
|
||||
|
|
|
|||
|
|
@ -21,11 +21,7 @@ import {
|
|||
normalizeDeliveryContext,
|
||||
resolveConversationDeliveryTarget,
|
||||
} from "../utils/delivery-context.js";
|
||||
import {
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
isDeliverableMessageChannel,
|
||||
isInternalMessageChannel,
|
||||
} from "../utils/message-channel.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL, isInternalMessageChannel } from "../utils/message-channel.js";
|
||||
import { buildAnnounceIdempotencyKey, resolveQueueAnnounceId } from "./announce-idempotency.js";
|
||||
import type { AgentInternalEvent } from "./internal-events.js";
|
||||
import { isEmbeddedPiRunActive, queueEmbeddedPiMessage } from "./pi-embedded.js";
|
||||
|
|
@ -263,7 +259,10 @@ export async function resolveSubagentCompletionOrigin(params: {
|
|||
},
|
||||
);
|
||||
const hookOrigin = normalizeDeliveryContext(result?.origin);
|
||||
if (!hookOrigin || (hookOrigin.channel && !isDeliverableMessageChannel(hookOrigin.channel))) {
|
||||
if (!hookOrigin) {
|
||||
return requesterOrigin;
|
||||
}
|
||||
if (hookOrigin.channel && isInternalMessageChannel(hookOrigin.channel)) {
|
||||
return requesterOrigin;
|
||||
}
|
||||
return mergeDeliveryContext(hookOrigin, requesterOrigin);
|
||||
|
|
@ -459,19 +458,17 @@ async function sendSubagentAnnounceDirectly(params: {
|
|||
params.expectsCompletionMessage && completionDirectOrigin
|
||||
? completionDirectOrigin
|
||||
: directOrigin;
|
||||
const directChannelRaw =
|
||||
const directChannel =
|
||||
typeof effectiveDirectOrigin?.channel === "string"
|
||||
? effectiveDirectOrigin.channel.trim()
|
||||
: "";
|
||||
const directChannel =
|
||||
directChannelRaw && isDeliverableMessageChannel(directChannelRaw) ? directChannelRaw : "";
|
||||
const directTo =
|
||||
typeof effectiveDirectOrigin?.to === "string" ? effectiveDirectOrigin.to.trim() : "";
|
||||
const hasDeliverableDirectTarget =
|
||||
!params.requesterIsSubagent && Boolean(directChannel) && Boolean(directTo);
|
||||
const shouldDeliverExternally =
|
||||
!params.requesterIsSubagent &&
|
||||
(!params.expectsCompletionMessage || hasDeliverableDirectTarget);
|
||||
Boolean(directChannel) &&
|
||||
Boolean(directTo) &&
|
||||
!isInternalMessageChannel(directChannel);
|
||||
|
||||
const threadId =
|
||||
effectiveDirectOrigin?.threadId != null && effectiveDirectOrigin.threadId !== ""
|
||||
|
|
@ -497,10 +494,10 @@ async function sendSubagentAnnounceDirectly(params: {
|
|||
deliver: shouldDeliverExternally,
|
||||
bestEffortDeliver: params.bestEffortDeliver,
|
||||
internalEvents: params.internalEvents,
|
||||
channel: shouldDeliverExternally ? directChannel : undefined,
|
||||
accountId: shouldDeliverExternally ? effectiveDirectOrigin?.accountId : undefined,
|
||||
to: shouldDeliverExternally ? directTo : undefined,
|
||||
threadId: shouldDeliverExternally ? threadId : undefined,
|
||||
channel: !params.requesterIsSubagent ? directChannel || undefined : undefined,
|
||||
accountId: !params.requesterIsSubagent ? effectiveDirectOrigin?.accountId : undefined,
|
||||
to: !params.requesterIsSubagent ? directTo || undefined : undefined,
|
||||
threadId: !params.requesterIsSubagent ? threadId : undefined,
|
||||
inputProvenance: {
|
||||
kind: "inter_session",
|
||||
sourceSessionKey: params.sourceSessionKey,
|
||||
|
|
|
|||
|
|
@ -610,6 +610,27 @@ describe("subagent announce formatting", () => {
|
|||
expect(msg).not.toContain("✅ Subagent");
|
||||
});
|
||||
|
||||
it("keeps completion delivery enabled for extension channels captured from requester origin", async () => {
|
||||
const didAnnounce = await runSubagentAnnounceFlow({
|
||||
childSessionKey: "agent:main:subagent:test",
|
||||
childRunId: "run-direct-completion-bluebubbles",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
requesterOrigin: { channel: "bluebubbles", to: "+1234567890", accountId: "acct-bb" },
|
||||
...defaultOutcomeAnnounce,
|
||||
expectsCompletionMessage: true,
|
||||
});
|
||||
|
||||
expect(didAnnounce).toBe(true);
|
||||
expect(sendSpy).not.toHaveBeenCalled();
|
||||
expect(agentSpy).toHaveBeenCalledTimes(1);
|
||||
const call = agentSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
|
||||
expect(call?.params?.deliver).toBe(true);
|
||||
expect(call?.params?.channel).toBe("bluebubbles");
|
||||
expect(call?.params?.to).toBe("+1234567890");
|
||||
expect(call?.params?.accountId).toBe("acct-bb");
|
||||
});
|
||||
|
||||
it("keeps direct completion announce delivery immediate even when sibling counters are non-zero", async () => {
|
||||
sessionStore = {
|
||||
"agent:main:subagent:test": {
|
||||
|
|
@ -1351,6 +1372,41 @@ describe("subagent announce formatting", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("uses hook-provided extension channel targets for completion delivery", async () => {
|
||||
hasSubagentDeliveryTargetHook = true;
|
||||
subagentDeliveryTargetHookMock.mockResolvedValueOnce({
|
||||
origin: {
|
||||
channel: "bluebubbles",
|
||||
accountId: "acct-bb",
|
||||
to: "+1234567890",
|
||||
},
|
||||
});
|
||||
|
||||
const didAnnounce = await runSubagentAnnounceFlow({
|
||||
childSessionKey: "agent:main:subagent:test",
|
||||
childRunId: "run-direct-hook-bluebubbles",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
requesterOrigin: {
|
||||
channel: "discord",
|
||||
to: "channel:12345",
|
||||
accountId: "acct-1",
|
||||
},
|
||||
...defaultOutcomeAnnounce,
|
||||
expectsCompletionMessage: true,
|
||||
spawnMode: "session",
|
||||
});
|
||||
|
||||
expect(didAnnounce).toBe(true);
|
||||
expect(sendSpy).not.toHaveBeenCalled();
|
||||
expect(agentSpy).toHaveBeenCalledTimes(1);
|
||||
const call = agentSpy.mock.calls[0]?.[0] as { params?: Record<string, unknown> };
|
||||
expect(call?.params?.deliver).toBe(true);
|
||||
expect(call?.params?.channel).toBe("bluebubbles");
|
||||
expect(call?.params?.to).toBe("+1234567890");
|
||||
expect(call?.params?.accountId).toBe("acct-bb");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "delivery-target hook returns no override",
|
||||
|
|
@ -1358,7 +1414,7 @@ describe("subagent announce formatting", () => {
|
|||
hookResult: undefined,
|
||||
},
|
||||
{
|
||||
name: "delivery-target hook returns non-deliverable channel",
|
||||
name: "delivery-target hook returns internal channel",
|
||||
childRunId: "run-direct-thread-multi-no-origin",
|
||||
hookResult: {
|
||||
origin: {
|
||||
|
|
@ -1963,6 +2019,33 @@ describe("subagent announce formatting", () => {
|
|||
expect(call?.expectFinal).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps direct announce delivery enabled for extension channels", async () => {
|
||||
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false);
|
||||
embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false);
|
||||
|
||||
const didAnnounce = await runSubagentAnnounceFlow({
|
||||
childSessionKey: "agent:main:subagent:test",
|
||||
childRunId: "run-direct-bluebubbles",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterOrigin: { channel: "bluebubbles", accountId: "acct-bb", to: "+1234567890" },
|
||||
requesterDisplayKey: "main",
|
||||
...defaultOutcomeAnnounce,
|
||||
});
|
||||
|
||||
expect(didAnnounce).toBe(true);
|
||||
expect(sendSpy).not.toHaveBeenCalled();
|
||||
expect(agentSpy).toHaveBeenCalledTimes(1);
|
||||
const call = agentSpy.mock.calls[0]?.[0] as {
|
||||
params?: Record<string, unknown>;
|
||||
expectFinal?: boolean;
|
||||
};
|
||||
expect(call?.params?.deliver).toBe(true);
|
||||
expect(call?.params?.channel).toBe("bluebubbles");
|
||||
expect(call?.params?.to).toBe("+1234567890");
|
||||
expect(call?.params?.accountId).toBe("acct-bb");
|
||||
expect(call?.expectFinal).toBe(true);
|
||||
});
|
||||
|
||||
it("injects direct announce into requester subagent session as a user-turn agent call", async () => {
|
||||
embeddedRunMock.isEmbeddedPiRunActive.mockReturnValue(false);
|
||||
embeddedRunMock.isEmbeddedPiRunStreaming.mockReturnValue(false);
|
||||
|
|
@ -2817,7 +2900,7 @@ describe("subagent announce formatting", () => {
|
|||
},
|
||||
},
|
||||
expectedSessionKey: "agent:main:main",
|
||||
expectedDeliver: true,
|
||||
expectedDeliver: false,
|
||||
expectedChannel: "discord",
|
||||
},
|
||||
{
|
||||
|
|
@ -2839,7 +2922,7 @@ describe("subagent announce formatting", () => {
|
|||
},
|
||||
},
|
||||
expectedSessionKey: "agent:main:main",
|
||||
expectedDeliver: true,
|
||||
expectedDeliver: false,
|
||||
expectedChannel: "discord",
|
||||
},
|
||||
] as const;
|
||||
|
|
|
|||
Loading…
Reference in New Issue