mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
fix(agents): fail closed missing requester completion routes
This commit is contained in:
@@ -39,6 +39,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- Agents/subagents: fail closed instead of selecting a single child thread binding when completion delivery lacks requester conversation signal. Thanks @suyua9.
|
||||
- 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.
|
||||
|
||||
@@ -985,6 +985,62 @@ describe("subagent announce formatting", () => {
|
||||
expect(call?.params?.to).toBe("channel:thread-bound-1");
|
||||
});
|
||||
|
||||
it("does not use a child bound destination when completion requester conversation is missing", async () => {
|
||||
sessionStore = {
|
||||
"agent:main:subagent:test": {
|
||||
sessionId: "child-session-bound-missing-requester",
|
||||
},
|
||||
"agent:main:main": {
|
||||
sessionId: "requester-session-bound-missing-requester",
|
||||
},
|
||||
};
|
||||
chatHistoryMock.mockResolvedValueOnce({
|
||||
messages: [{ role: "assistant", content: [{ type: "text", text: "bound answer: 2" }] }],
|
||||
});
|
||||
registerSessionBindingAdapter({
|
||||
channel: "discord",
|
||||
accountId: "acct-1",
|
||||
listBySession: (targetSessionKey: string) =>
|
||||
targetSessionKey === "agent:main:subagent:test"
|
||||
? [
|
||||
{
|
||||
bindingId: "discord:acct-1:thread-bound-1",
|
||||
targetSessionKey,
|
||||
targetKind: "subagent",
|
||||
conversation: {
|
||||
channel: "discord",
|
||||
accountId: "acct-1",
|
||||
conversationId: "thread-bound-1",
|
||||
parentConversationId: "parent-main",
|
||||
},
|
||||
status: "active",
|
||||
boundAt: Date.now(),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
resolveByConversation: () => null,
|
||||
});
|
||||
|
||||
const didAnnounce = await runSubagentAnnounceFlow({
|
||||
childSessionKey: "agent:main:subagent:test",
|
||||
childRunId: "run-session-bound-missing-requester",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterDisplayKey: "main",
|
||||
requesterOrigin: { channel: "discord", 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(false);
|
||||
expect(call?.params?.to).toBeUndefined();
|
||||
expect(call?.params?.threadId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not duplicate to main channel when two active bound sessions complete from the same requester channel", async () => {
|
||||
sessionStore = {
|
||||
"agent:main:subagent:child-a": {
|
||||
|
||||
@@ -105,7 +105,17 @@ describe("bound delivery router", () => {
|
||||
expected: {
|
||||
binding: null,
|
||||
mode: "fallback",
|
||||
reason: "ambiguous-without-requester",
|
||||
reason: "missing-requester",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fails closed when requester signal is missing even with a single binding",
|
||||
bindings: [createRuntimeBinding(TARGET_SESSION_KEY, "thread-1", 1)],
|
||||
failClosed: true,
|
||||
expected: {
|
||||
binding: null,
|
||||
mode: "fallback",
|
||||
reason: "missing-requester",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -79,6 +79,13 @@ export function createBoundDeliveryRouter(
|
||||
}
|
||||
|
||||
if (!input.requester) {
|
||||
if (input.failClosed) {
|
||||
return {
|
||||
binding: null,
|
||||
mode: "fallback",
|
||||
reason: "missing-requester",
|
||||
};
|
||||
}
|
||||
if (activeBindings.length === 1) {
|
||||
return {
|
||||
binding: activeBindings[0] ?? null,
|
||||
|
||||
Reference in New Issue
Block a user