mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:10:44 +00:00
fix: preserve requester route for subagent completion delivery (#72806)
* fix: preserve requester route for subagent completion delivery * fix(agents): preserve requester subagent completion routes --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -35,6 +35,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Local models: default custom providers with only `baseUrl` to the Chat Completions adapter and trust loopback model requests automatically, so local OpenAI-compatible proxies receive `/v1/chat/completions` without timing out. Fixes #40024. Thanks @parachuteshe.
|
||||
- Channels/message tool: surface Discord, Slack, and Mattermost `user:`/`channel:` target syntax in the shared message target schema and Discord ambiguity errors, so DM sends by numeric id stop burning retries before finding `user:<id>`. Fixes #72401. Thanks @garyd9, @hclsys, and @praveen9354.
|
||||
- Agents/tools: scope tool-loop detection history to the active run when available, so scheduled heartbeat cycles no longer inherit stale repeated-call counts from previous runs. Fixes #40144. Thanks @mattbrown319.
|
||||
- Agents/subagents: preserve requester delivery for completion announces when a child agent is bound to a different channel account while keeping same-channel thread completions routed to the child thread. Thanks @sfuminya.
|
||||
- Control UI: show loading, reload, and retry states when a lazy dashboard panel cannot load after an upgrade, so the Logs tab no longer appears blank on stale browser bundles. Fixes #72450. Thanks @sobergou.
|
||||
- Gateway/plugins: start the Gateway in degraded mode when a single plugin entry has invalid schema config, and let `openclaw doctor --fix` quarantine that plugin config instead of crash-looping every channel. Fixes #62976 and #70371. Thanks @Doraemon-Claw and @pksidekyk.
|
||||
- Agents/plugins: skip malformed plugin tools with missing schema objects and report plugin diagnostics, so one broken tool no longer crashes Anthropic agent runs. Fixes #69423. Thanks @jmnickels.
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
__testing as sessionBindingServiceTesting,
|
||||
registerSessionBindingAdapter,
|
||||
} from "../infra/outbound/session-binding-service.js";
|
||||
import type { AgentInternalEvent } from "./internal-events.js";
|
||||
import {
|
||||
__testing,
|
||||
deliverSubagentAnnouncement,
|
||||
extractThreadCompletionFallbackText,
|
||||
resolveSubagentCompletionOrigin,
|
||||
} from "./subagent-announce-delivery.js";
|
||||
import {
|
||||
callGateway as runtimeCallGateway,
|
||||
@@ -14,6 +19,7 @@ import { resetAnnounceQueuesForTests } from "./subagent-announce-queue.js";
|
||||
|
||||
afterEach(() => {
|
||||
resetAnnounceQueuesForTests();
|
||||
sessionBindingServiceTesting.resetSessionBindingAdaptersForTests();
|
||||
__testing.setDepsForTest();
|
||||
});
|
||||
|
||||
@@ -270,6 +276,77 @@ describe("resolveAnnounceOrigin threaded route targets", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSubagentCompletionOrigin", () => {
|
||||
it("resolves bound completion delivery from the requester session, not the child session", async () => {
|
||||
registerSessionBindingAdapter({
|
||||
channel: "discord",
|
||||
accountId: "bot-alpha",
|
||||
listBySession: (targetSessionKey: string) => {
|
||||
if (targetSessionKey === "agent:worker:subagent:child") {
|
||||
return [
|
||||
{
|
||||
bindingId: "discord:bot-alpha:child-window",
|
||||
targetSessionKey,
|
||||
targetKind: "subagent",
|
||||
conversation: {
|
||||
channel: "discord",
|
||||
accountId: "bot-alpha",
|
||||
conversationId: "child-window",
|
||||
},
|
||||
status: "active",
|
||||
boundAt: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
resolveByConversation: () => null,
|
||||
});
|
||||
registerSessionBindingAdapter({
|
||||
channel: "discord",
|
||||
accountId: "acct-1",
|
||||
listBySession: (targetSessionKey: string) => {
|
||||
if (targetSessionKey === "agent:main:main") {
|
||||
return [
|
||||
{
|
||||
bindingId: "discord:acct-1:parent-main",
|
||||
targetSessionKey,
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "discord",
|
||||
accountId: "acct-1",
|
||||
conversationId: "parent-main",
|
||||
},
|
||||
status: "active",
|
||||
boundAt: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
resolveByConversation: () => null,
|
||||
});
|
||||
|
||||
const origin = await resolveSubagentCompletionOrigin({
|
||||
childSessionKey: "agent:worker:subagent:child",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterOrigin: {
|
||||
channel: "discord",
|
||||
accountId: "acct-1",
|
||||
to: "channel:parent-main",
|
||||
},
|
||||
spawnMode: "session",
|
||||
expectsCompletionMessage: true,
|
||||
});
|
||||
|
||||
expect(origin).toEqual({
|
||||
channel: "discord",
|
||||
accountId: "acct-1",
|
||||
to: "channel:parent-main",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("deliverSubagentAnnouncement queued delivery", () => {
|
||||
async function deliverQueuedAnnouncement(params: {
|
||||
requesterOrigin?: {
|
||||
|
||||
@@ -306,11 +306,29 @@ export async function resolveSubagentCompletionOrigin(params: {
|
||||
const requesterConversation: ConversationRef | undefined =
|
||||
channel && conversationId ? { channel, accountId, conversationId } : undefined;
|
||||
|
||||
const route = createBoundDeliveryRouter().resolveDestination({
|
||||
const router = createBoundDeliveryRouter();
|
||||
const childRoute = router.resolveDestination({
|
||||
eventKind: "task_completion",
|
||||
targetSessionKey: params.childSessionKey,
|
||||
requester: requesterConversation,
|
||||
failClosed: false,
|
||||
failClosed: true,
|
||||
});
|
||||
if (childRoute.mode === "bound" && childRoute.binding) {
|
||||
return mergeDeliveryContext(
|
||||
resolveBoundConversationOrigin({
|
||||
bindingConversation: childRoute.binding.conversation,
|
||||
requesterConversation,
|
||||
requesterOrigin,
|
||||
}),
|
||||
requesterOrigin,
|
||||
);
|
||||
}
|
||||
|
||||
const route = router.resolveDestination({
|
||||
eventKind: "task_completion",
|
||||
targetSessionKey: params.requesterSessionKey,
|
||||
requester: requesterConversation,
|
||||
failClosed: true,
|
||||
});
|
||||
if (route.mode === "bound" && route.binding) {
|
||||
return mergeDeliveryContext(
|
||||
|
||||
@@ -163,9 +163,8 @@ describe("spawnSubagentDirect thread binding delivery", () => {
|
||||
expect.objectContaining({
|
||||
requesterOrigin: {
|
||||
channel: "matrix",
|
||||
accountId: "bot-alpha",
|
||||
accountId: "bot-beta",
|
||||
to: `room:${boundRoom}`,
|
||||
threadId: "$thread-root",
|
||||
},
|
||||
expectsCompletionMessage: false,
|
||||
spawnMode: "session",
|
||||
|
||||
@@ -725,7 +725,15 @@ export async function spawnSubagentDirect(
|
||||
};
|
||||
}
|
||||
const targetAgentId = requestedAgentId ? normalizeAgentId(requestedAgentId) : requesterAgentId;
|
||||
const requesterOrigin = resolveRequesterOriginForChild({
|
||||
const requesterOrigin = normalizeDeliveryContext({
|
||||
channel: ctx.agentChannel,
|
||||
accountId: ctx.agentAccountId,
|
||||
to: ctx.agentTo,
|
||||
...(ctx.agentThreadId != null && ctx.agentThreadId !== ""
|
||||
? { threadId: ctx.agentThreadId }
|
||||
: {}),
|
||||
});
|
||||
let childSessionOrigin = resolveRequesterOriginForChild({
|
||||
cfg,
|
||||
targetAgentId,
|
||||
requesterAgentId,
|
||||
@@ -736,7 +744,6 @@ export async function spawnSubagentDirect(
|
||||
requesterGroupSpace: ctx.agentGroupSpace,
|
||||
requesterMemberRoleIds: ctx.agentMemberRoleIds,
|
||||
});
|
||||
let childSessionOrigin = requesterOrigin;
|
||||
if (targetAgentId !== requesterAgentId) {
|
||||
const allowAgents =
|
||||
resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ??
|
||||
@@ -892,10 +899,10 @@ export async function spawnSubagentDirect(
|
||||
mode: spawnMode,
|
||||
requesterSessionKey: requesterInternalKey,
|
||||
requester: {
|
||||
channel: requesterOrigin?.channel,
|
||||
accountId: requesterOrigin?.accountId,
|
||||
to: requesterOrigin?.to,
|
||||
threadId: requesterOrigin?.threadId,
|
||||
channel: childSessionOrigin?.channel,
|
||||
accountId: childSessionOrigin?.accountId,
|
||||
to: childSessionOrigin?.to,
|
||||
threadId: childSessionOrigin?.threadId,
|
||||
},
|
||||
});
|
||||
if (bindResult.status === "error") {
|
||||
@@ -917,7 +924,7 @@ export async function spawnSubagentDirect(
|
||||
threadBindingReady = true;
|
||||
hasBoundThreadDeliveryOrigin = hasRoutableDeliveryOrigin(bindResult.deliveryOrigin);
|
||||
childSessionOrigin =
|
||||
mergeDeliveryContext(bindResult.deliveryOrigin, requesterOrigin) ?? childSessionOrigin;
|
||||
mergeDeliveryContext(bindResult.deliveryOrigin, childSessionOrigin) ?? childSessionOrigin;
|
||||
}
|
||||
const mountPathHint = sanitizeMountPathHint(params.attachMountPath);
|
||||
|
||||
@@ -1152,7 +1159,7 @@ export async function spawnSubagentDirect(
|
||||
childSessionKey,
|
||||
controllerSessionKey: requesterInternalKey,
|
||||
requesterSessionKey: requesterInternalKey,
|
||||
requesterOrigin: childSessionOrigin,
|
||||
requesterOrigin,
|
||||
requesterDisplayKey,
|
||||
task,
|
||||
cleanup,
|
||||
|
||||
Reference in New Issue
Block a user