fix(acp): restore inline delivery for run-mode spawns from main sessions (#52426)

* fix(acp): restore inline delivery for run-mode spawns from main sessions

* test: restore matrix ACP spawn coverage (#52426) (thanks @distractedCoding)

---------

Co-authored-by: Felix <distractedCoding@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Felix
2026-03-22 22:54:09 +01:00
committed by GitHub
parent ef3f64952a
commit 57267b23d5
3 changed files with 43 additions and 5 deletions

View File

@@ -565,6 +565,12 @@ Docs: https://docs.openclaw.ai
- Gateway/node pending drain followup: keep `hasMore` true when the deferred baseline status item still needs delivery, and avoid allocating empty pending-work state for drain-only nodes with no queued work. (#41429) Thanks @mbelinky.
- Protocol/Swift model sync: regenerate pending node work Swift bindings after the landed `node.pending.*` schema additions so generated protocol artifacts are consistent again. (#41477) Thanks @mbelinky.
- Cron/subagent followup: do not misclassify empty or `NO_REPLY` cron responses as interim acknowledgements that need a rerun, so deliberately silent cron jobs are no longer retried. (#41383) thanks @jackal092927.
- CLI/memory teardown: close cached memory search/index managers in the one-shot CLI shutdown path so watcher-backed memory caches no longer keep completed CLI runs alive after output finishes. (#40389) thanks @Julbarth.
- Tools/web search: treat Brave `llm-context` grounding snippets as plain strings so `web_search` no longer returns empty snippet arrays in LLM Context mode. (#41387) thanks @zheliu2.
- ACP/run-mode delivery: restore inline delivery for one-shot ACP run spawns from non-subagent (main) requester sessions so completions reach the originating Discord/Telegram/etc. channel again. Subagent orchestrators continue to use stream-to-parent when an active heartbeat relay route is available. (#52426) Thanks @distractedCoding.
- Telegram/exec approvals: reject `/approve` commands aimed at other bots, keep deterministic approval prompts visible when tool-result delivery fails, and stop resolved exact IDs from matching other pending approvals by prefix. (#37233) Thanks @huntharo.
- Control UI/Sessions: restore single-column session table collapse on narrow viewport or container widths by moving the responsive table override next to the base grid rule and enabling inline-size container queries. (#12175) Thanks @benjipeng.
- Telegram/final preview delivery: split active preview lifecycle from cleanup retention so missing archived preview edits avoid duplicate fallback sends without clearing the live preview or blocking later in-place finalization. (#41662) thanks @hougangdev.
- Cron/state errors: record `lastErrorReason` in cron job state and keep the gateway schema aligned with the full failover-reason set, including regression coverage for protocol conformance. (#14382) thanks @futuremind2026.
- Browser/Browserbase 429 handling: surface stable no-retry rate-limit guidance without buffering discarded HTTP 429 response bodies from remote browser services. (#40491) thanks @mvanhorn.
- CI/CodeQL Swift toolchain: select Xcode 26.1 before installing Swift build tools so the CodeQL Swift job uses Swift tools 6.2 on `macos-latest`. (#41787) thanks @BunsDev.

View File

@@ -390,6 +390,7 @@ describe("spawnAcpDirect", () => {
matrix: {
threadBindings: {
enabled: true,
spawnAcpSessions: true,
},
},
},
@@ -460,7 +461,7 @@ describe("spawnAcpDirect", () => {
expect(agentCall?.params?.threadId).toBe("child-thread");
});
it("does not inline delivery for fresh oneshot ACP runs", async () => {
it("inlines delivery for run-mode spawns from non-subagent requester sessions", async () => {
const result = await spawnAcpDirect(
{
task: "Investigate flaky tests",
@@ -487,13 +488,39 @@ describe("spawnAcpDirect", () => {
agentId: "codex",
}),
);
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");
expect(agentCall?.params?.to).toBe("telegram:6098642967");
});
it("does not inline delivery for run-mode spawns from subagent requester sessions", async () => {
const result = await spawnAcpDirect(
{
task: "Investigate flaky tests",
agentId: "codex",
mode: "run",
},
{
agentSessionKey: "agent:main:subagent:orchestrator",
agentChannel: "telegram",
agentAccountId: "default",
agentTo: "telegram:6098642967",
},
);
expect(result.status).toBe("accepted");
expect(result.mode).toBe("run");
expect(result.streamLogPath).toBeUndefined();
expect(hoisted.startAcpSpawnParentStreamRelayMock).not.toHaveBeenCalled();
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("keeps ACP spawn running when session-file persistence fails", async () => {

View File

@@ -685,10 +685,15 @@ export async function spawnAcpDirect(
});
const resolvedDeliveryThreadId = boundDeliveryTarget.threadId ?? deliveryThreadId;
const hasDeliveryTarget = Boolean(requesterOrigin?.channel && inferredDeliveryTo);
// 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.
// Thread-bound session spawns always deliver inline to their bound thread.
// Run-mode spawns use stream-to-parent when the requester is a subagent
// orchestrator with an active heartbeat relay route. For all other run-mode
// spawns from non-subagent requester sessions, fall back to inline delivery
// so the result reaches the originating channel.
const useInlineDelivery =
hasDeliveryTarget && spawnMode === "session" && !effectiveStreamToParent;
hasDeliveryTarget &&
!effectiveStreamToParent &&
(spawnMode === "session" || (!requesterIsSubagentSession && !requestThreadBinding));
const childIdem = crypto.randomUUID();
let childRunId: string = childIdem;
const streamLogPath =