mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:38:38 +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/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/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.
|
- 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/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.
|
- 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.
|
- 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 () => {
|
it("includes cwd in ACP thread intro banner when provided at spawn time", async () => {
|
||||||
const result = await spawnAcpDirect(
|
const result = await spawnAcpDirect(
|
||||||
{
|
{
|
||||||
@@ -540,6 +567,32 @@ describe("spawnAcpDirect", () => {
|
|||||||
expect(notifyOrder[0] > agentCallOrder).toBe(true);
|
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 () => {
|
it("disposes pre-registered parent relay when initial ACP dispatch fails", async () => {
|
||||||
const relayHandle = createRelayHandle();
|
const relayHandle = createRelayHandle();
|
||||||
hoisted.startAcpSpawnParentStreamRelayMock.mockReturnValueOnce(relayHandle);
|
hoisted.startAcpSpawnParentStreamRelayMock.mockReturnValueOnce(relayHandle);
|
||||||
|
|||||||
@@ -440,7 +440,10 @@ export async function spawnAcpDirect(
|
|||||||
? `channel:${boundThreadId}`
|
? `channel:${boundThreadId}`
|
||||||
: requesterOrigin?.to?.trim() || (deliveryThreadId ? `channel:${deliveryThreadId}` : undefined);
|
: requesterOrigin?.to?.trim() || (deliveryThreadId ? `channel:${deliveryThreadId}` : undefined);
|
||||||
const hasDeliveryTarget = Boolean(requesterOrigin?.channel && inferredDeliveryTo);
|
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();
|
const childIdem = crypto.randomUUID();
|
||||||
let childRunId: string = childIdem;
|
let childRunId: string = childIdem;
|
||||||
const streamLogPath =
|
const streamLogPath =
|
||||||
@@ -467,12 +470,12 @@ export async function spawnAcpDirect(
|
|||||||
params: {
|
params: {
|
||||||
message: params.task,
|
message: params.task,
|
||||||
sessionKey,
|
sessionKey,
|
||||||
channel: hasDeliveryTarget ? requesterOrigin?.channel : undefined,
|
channel: useInlineDelivery ? requesterOrigin?.channel : undefined,
|
||||||
to: hasDeliveryTarget ? inferredDeliveryTo : undefined,
|
to: useInlineDelivery ? inferredDeliveryTo : undefined,
|
||||||
accountId: hasDeliveryTarget ? (requesterOrigin?.accountId ?? undefined) : undefined,
|
accountId: useInlineDelivery ? (requesterOrigin?.accountId ?? undefined) : undefined,
|
||||||
threadId: hasDeliveryTarget ? deliveryThreadId : undefined,
|
threadId: useInlineDelivery ? deliveryThreadId : undefined,
|
||||||
idempotencyKey: childIdem,
|
idempotencyKey: childIdem,
|
||||||
deliver: deliverToBoundTarget,
|
deliver: useInlineDelivery,
|
||||||
label: params.label || undefined,
|
label: params.label || undefined,
|
||||||
},
|
},
|
||||||
timeoutMs: 10_000,
|
timeoutMs: 10_000,
|
||||||
|
|||||||
Reference in New Issue
Block a user