mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(acp): avoid inline delivery for oneshot run spawns (#39014)
* fix(acp): scope inline delivery to session spawns * test(acp): cover run and session delivery behavior * Changelog: add ACP run delivery bootstrap fix --------- Co-authored-by: 徐善 <samxu633@gmail.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
@@ -327,6 +327,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Telegram/preview-final edit idempotence: treat `message is not modified` errors during preview finalization as delivered so partial-stream final replies do not fall back to duplicate sends. Landed from contributor PR #34983 by @HOYALIM. Thanks @HOYALIM.
|
||||
- Telegram/DM streaming transport parity: use message preview transport for all DM streaming lanes so final delivery can edit the active preview instead of sending duplicate finals. Landed from contributor PR #38906 by @gambletan. Thanks @gambletan.
|
||||
- Telegram/send retry safety: retry non-idempotent send paths only for pre-connect failures and make custom retry predicates strict, preventing ambiguous reconnect retries from sending duplicate messages. Landed from contributor PR #34238 by @hal-crackbot. Thanks @hal-crackbot.
|
||||
- ACP/run spawn delivery bootstrap: stop reusing requester inline delivery targets for one-shot `mode: "run"` ACP spawns, so fresh run-mode workers bootstrap in isolation instead of inheriting thread-bound session delivery behavior. (#39014) Thanks @lidamao633.
|
||||
- Discord/DM session-key normalization: rewrite legacy `discord:dm:*` and phantom direct-message `discord:channel:<user>` session keys to `discord:direct:*` when the sender matches, so multi-agent Discord DMs stop falling into empty channel-shaped sessions and resume replying correctly.
|
||||
- Discord/native slash session fallback: treat empty configured bound-session keys as missing so `/status` and other native commands fall back to the routed slash session and routed channel session instead of blanking Discord session keys in normal channel bindings.
|
||||
- Agents/tool-call dispatch normalization: normalize provider-prefixed tool names before dispatch across `toolCall`, `toolUse`, and `functionCall` blocks, while preserving multi-segment tool suffixes when stripping provider wrappers so malformed-but-recoverable tool names no longer fail with `Tool not found`. (#39328) Thanks @vincentkoc.
|
||||
|
||||
@@ -310,6 +310,33 @@ describe("spawnAcpDirect", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not inline delivery for fresh oneshot ACP runs", async () => {
|
||||
const result = await spawnAcpDirect(
|
||||
{
|
||||
task: "Investigate flaky tests",
|
||||
agentId: "codex",
|
||||
mode: "run",
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:main:telegram:direct:6098642967",
|
||||
agentChannel: "telegram",
|
||||
agentAccountId: "default",
|
||||
agentTo: "telegram:6098642967",
|
||||
agentThreadId: "1",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe("accepted");
|
||||
expect(result.mode).toBe("run");
|
||||
const agentCall = hoisted.callGatewayMock.mock.calls
|
||||
.map((call: unknown[]) => call[0] as { method?: string; params?: Record<string, unknown> })
|
||||
.find((request) => request.method === "agent");
|
||||
expect(agentCall?.params?.deliver).toBe(false);
|
||||
expect(agentCall?.params?.channel).toBeUndefined();
|
||||
expect(agentCall?.params?.to).toBeUndefined();
|
||||
expect(agentCall?.params?.threadId).toBeUndefined();
|
||||
});
|
||||
|
||||
it("includes cwd in ACP thread intro banner when provided at spawn time", async () => {
|
||||
const result = await spawnAcpDirect(
|
||||
{
|
||||
@@ -540,6 +567,32 @@ describe("spawnAcpDirect", () => {
|
||||
expect(notifyOrder[0] > agentCallOrder).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps inline delivery for thread-bound ACP session mode", async () => {
|
||||
const result = await spawnAcpDirect(
|
||||
{
|
||||
task: "Investigate flaky tests",
|
||||
agentId: "codex",
|
||||
mode: "session",
|
||||
thread: true,
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:main:telegram:group:-1003342490704:topic:2",
|
||||
agentChannel: "telegram",
|
||||
agentAccountId: "default",
|
||||
agentTo: "telegram:-1003342490704",
|
||||
agentThreadId: "2",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe("accepted");
|
||||
expect(result.mode).toBe("session");
|
||||
const agentCall = hoisted.callGatewayMock.mock.calls
|
||||
.map((call: unknown[]) => call[0] as { method?: string; params?: Record<string, unknown> })
|
||||
.find((request) => request.method === "agent");
|
||||
expect(agentCall?.params?.deliver).toBe(true);
|
||||
expect(agentCall?.params?.channel).toBe("telegram");
|
||||
});
|
||||
|
||||
it("disposes pre-registered parent relay when initial ACP dispatch fails", async () => {
|
||||
const relayHandle = createRelayHandle();
|
||||
hoisted.startAcpSpawnParentStreamRelayMock.mockReturnValueOnce(relayHandle);
|
||||
|
||||
@@ -440,7 +440,10 @@ export async function spawnAcpDirect(
|
||||
? `channel:${boundThreadId}`
|
||||
: requesterOrigin?.to?.trim() || (deliveryThreadId ? `channel:${deliveryThreadId}` : undefined);
|
||||
const hasDeliveryTarget = Boolean(requesterOrigin?.channel && inferredDeliveryTo);
|
||||
const deliverToBoundTarget = hasDeliveryTarget && !streamToParentRequested;
|
||||
// Fresh one-shot ACP runs should bootstrap the worker first, then let higher layers
|
||||
// decide how to relay status. Inline delivery is reserved for thread-bound sessions.
|
||||
const useInlineDelivery =
|
||||
hasDeliveryTarget && spawnMode === "session" && !streamToParentRequested;
|
||||
const childIdem = crypto.randomUUID();
|
||||
let childRunId: string = childIdem;
|
||||
const streamLogPath =
|
||||
@@ -467,12 +470,12 @@ export async function spawnAcpDirect(
|
||||
params: {
|
||||
message: params.task,
|
||||
sessionKey,
|
||||
channel: hasDeliveryTarget ? requesterOrigin?.channel : undefined,
|
||||
to: hasDeliveryTarget ? inferredDeliveryTo : undefined,
|
||||
accountId: hasDeliveryTarget ? (requesterOrigin?.accountId ?? undefined) : undefined,
|
||||
threadId: hasDeliveryTarget ? deliveryThreadId : undefined,
|
||||
channel: useInlineDelivery ? requesterOrigin?.channel : undefined,
|
||||
to: useInlineDelivery ? inferredDeliveryTo : undefined,
|
||||
accountId: useInlineDelivery ? (requesterOrigin?.accountId ?? undefined) : undefined,
|
||||
threadId: useInlineDelivery ? deliveryThreadId : undefined,
|
||||
idempotencyKey: childIdem,
|
||||
deliver: deliverToBoundTarget,
|
||||
deliver: useInlineDelivery,
|
||||
label: params.label || undefined,
|
||||
},
|
||||
timeoutMs: 10_000,
|
||||
|
||||
Reference in New Issue
Block a user