


























@@ -2,10 +2,15 @@ import crypto from "node:crypto";
22import { Type } from "typebox";
33import { isRequesterParentOfBackgroundAcpSession } from "../../acp/session-interaction-mode.js";
44import { parseSessionThreadInfoFast } from "../../config/sessions/thread-info.js";
5+import type { SessionEntry } from "../../config/sessions/types.js";
56import type { OpenClawConfig } from "../../config/types.openclaw.js";
67import { callGateway } from "../../gateway/call.js";
78import { formatErrorMessage } from "../../infra/errors.js";
8-import { normalizeAgentId, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
9+import {
10+isSubagentSessionKey,
11+normalizeAgentId,
12+resolveAgentIdFromSessionKey,
13+} from "../../routing/session-key.js";
914import { annotateInterSessionPromptText } from "../../sessions/input-provenance.js";
1015import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js";
1116import { normalizeOptionalString } from "../../shared/string-coerce.js";
@@ -47,6 +52,25 @@ const SessionsSendToolSchema = Type.Object({
4752type GatewayCaller = typeof callGateway;
4853const SESSIONS_SEND_REPLY_HISTORY_LIMIT = 50;
495455+type SessionsSendRouteEntry = Pick<SessionEntry, "acp" | "parentSessionKey" | "spawnedBy">;
56+57+function isRequesterParentOfNativeSubagentSession(params: {
58+entry: SessionsSendRouteEntry | null | undefined;
59+requesterSessionKey: string | null | undefined;
60+targetSessionKey: string;
61+}): boolean {
62+if (!params.entry || params.entry.acp || !isSubagentSessionKey(params.targetSessionKey)) {
63+return false;
64+}
65+const requester = normalizeOptionalString(params.requesterSessionKey);
66+if (!requester) {
67+return false;
68+}
69+const spawnedBy = normalizeOptionalString(params.entry.spawnedBy);
70+const parentSessionKey = normalizeOptionalString(params.entry.parentSessionKey);
71+return requester === spawnedBy || requester === parentSessionKey;
72+}
73+5074async function startAgentRun(params: {
5175callGateway: GatewayCaller;
5276runId: string;
@@ -304,24 +328,34 @@ export function createSessionsSendTool(opts?: {
304328const maxPingPongTurns = resolvePingPongTurns(cfg);
305329306330// Skip the A2A ping-pong + announce flow when the current caller is the
307-// parent of a parent-owned background ACP subagent it spawned itself.
308-// Such sessions already report their results back to the parent through
309-// the `[Internal task completion event]` announcement path, and treating
310-// them as a peer agent causes the parent to be woken with the child's
311-// reply, generate a user-facing response, and have that response
312-// forwarded back to the child as a new message — producing a
313-// ping-pong loop between parent and ACP child (bounded by
314-// maxPingPongTurns, but user-visible as a runaway conversation).
331+// parent of a parent-owned child session it spawned itself and another
332+// parent-visible result path already exists.
333+//
334+// ACP background sessions report through the internal task completion
335+// path. Waited native subagent sends return the child reply inline. In
336+// both cases treating the child as a peer agent wakes the parent with
337+// the child's reply, can generate another user-facing response, and can
338+// forward that response back to the child as a new message — producing a
339+// ping-pong loop (bounded by maxPingPongTurns, but visible as duplicate
340+// conversation output).
315341//
316342// The skip is gated on requester ownership, not just target type: an
317343// unrelated sender that can see the same target (e.g. under
318344// `tools.sessions.visibility=all`) must still go through the normal A2A
319345// path so it actually receives a follow-up delivery.
320346const targetSessionEntry = loadSessionEntryByKey(resolvedKey);
321-const skipA2AFlow = isRequesterParentOfBackgroundAcpSession(
347+const skipAcpA2AFlow = isRequesterParentOfBackgroundAcpSession(
322348targetSessionEntry,
323349effectiveRequesterKey,
324350);
351+const skipNativeParentA2AFlow =
352+timeoutSeconds !== 0 &&
353+isRequesterParentOfNativeSubagentSession({
354+entry: targetSessionEntry,
355+requesterSessionKey: effectiveRequesterKey,
356+targetSessionKey: resolvedKey,
357+});
358+const skipA2AFlow = skipAcpA2AFlow || skipNativeParentA2AFlow;
325359// When the A2A flow is skipped, no follow-up announcement will fire and
326360// the reply (when present) is returned inline via the `reply` field.
327361// Reflect that in the metadata so the parent LLM does not wait for a
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。