mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
fix: route subagent announce to originating parent session instead of channel-bound peer session (#80242)
* fix: route subagent announce to originating parent session instead of channel-bound peer session When a subagent is spawned from agent:main:main while a Telegram DM is active, the completion announce was delivered to the parallel Telegram channel session instead of the originating parent. Two interacting bugs: 1. The spawn tool received the sandbox/policy session key (Telegram peer key) as the requester, instead of the real run session key. Fixed by passing runSessionKey to createSessionsSpawnTool so the registered requester points to the actual parent session. 2. resolveSubagentCompletionOrigin checked child session bindings before requester bindings. When both share the same channel+accountId (common for Telegram DMs), the child binding hijacked the delivery target. Fixed by checking requester binding first, with child as fallback. Fixes #80201 * fix: drop subagent_announce from mediated completion set The subagent_announce addition to AGENT_MEDIATED_COMPLETION_TOOLS was unrelated to the routing fix and could cause group/channel completions to fail silently when the subagent does not use the message tool. This should be addressed separately with proper message-tool-only guidance (tracked in #80223). * fix: separate sandbox policy from completion owner in sessions_spawn PR #80242 passed runSessionKey as agentSessionKey to createSessionsSpawnTool, which caused spawnSubagentDirect to use the run session key for sandbox policy checks (resolveSandboxRuntimeStatus). This could make a sandboxed channel run appear unsandboxed. Introduce completionOwnerKey as a separate field that is only used for registerSubagentRun routing (requesterSessionKey), keeping agentSessionKey for sandbox enforcement, callerDepth, activeChildren, and all other policy checks. * fix(agents): preserve subagent ownership routing --------- Co-authored-by: 忻役 <xinyi@mininglamp.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
co-authored by
忻役
Peter Steinberger
parent
d1cd74b243
commit
3e9e1d6321
@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Feishu: detect SecretRef top-level credentials as a configured default account instead of treating object-backed app secrets as missing.
|
||||
- Providers/Google: preserve and recover Gemini 3 tool-call thought signatures during native replay so function-calling turns no longer fail with missing `thought_signature` 400s. Fixes #72879. (#80358) Thanks @abnershang.
|
||||
- Memory-core: distinguish sqlite-vec load failures from missing semantic vector embeddings in degraded `memory index` warnings, so vector recall diagnostics point at unresolved dimensions instead of blaming sqlite-vec when the store is ready. Fixes #75624. (#83056) Thanks @xuruiray and @Noah3521.
|
||||
- Agents/subagents: preserve sandbox-peer controller ownership while routing completion announcements back to the originating run session, keeping subagent control and completion delivery scoped correctly. Fixes #80201. (#80242) Thanks @Jerry-Xin.
|
||||
- Gateway/secrets: split the lightweight secrets runtime state and auth-store cache from the full secrets runtime and take a startup fast path when the gateway startup config has no SecretRef values, speeding up secrets startup while preserving cleanup and refresh semantics.
|
||||
- Codex app-server: rotate oversized native Codex threads before resume and cap dynamic tool-result text entering native Codex sessions, preventing stale oversized context from surviving OpenClaw compaction. (#82981) Thanks @hansolo949.
|
||||
- Gateway/restart: drain pending replies and active chat runs during restart shutdown before sockets and channels close, aborting timed-out chat runs through the normal cleanup path. (#69121) Thanks @alexlomt.
|
||||
|
||||
@@ -428,6 +428,7 @@ export function createOpenClawTools(
|
||||
? [
|
||||
createSessionsSpawnTool({
|
||||
agentSessionKey: options?.agentSessionKey,
|
||||
completionOwnerKey: options?.runSessionKey,
|
||||
agentChannel: options?.agentChannel,
|
||||
agentAccountId: options?.agentAccountId,
|
||||
agentTo: options?.agentTo,
|
||||
|
||||
@@ -436,6 +436,112 @@ describe("resolveSubagentCompletionOrigin", () => {
|
||||
to: "channel:parent-main",
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers requester binding when child and requester share the same channel and accountId", async () => {
|
||||
registerSessionBindingAdapter({
|
||||
channel: "telegram",
|
||||
accountId: "bot-1",
|
||||
listBySession: (targetSessionKey: string) => {
|
||||
if (targetSessionKey === "agent:main:telegram:default:direct:123") {
|
||||
return [
|
||||
{
|
||||
bindingId: "telegram:bot-1:child-dm",
|
||||
targetSessionKey,
|
||||
targetKind: "subagent",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: "bot-1",
|
||||
conversationId: "direct:123",
|
||||
},
|
||||
status: "active",
|
||||
boundAt: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (targetSessionKey === "agent:main:main") {
|
||||
return [
|
||||
{
|
||||
bindingId: "telegram:bot-1:parent-main",
|
||||
targetSessionKey,
|
||||
targetKind: "session",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: "bot-1",
|
||||
conversationId: "direct:789",
|
||||
},
|
||||
status: "active",
|
||||
boundAt: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
resolveByConversation: () => null,
|
||||
});
|
||||
|
||||
const origin = await resolveSubagentCompletionOrigin({
|
||||
childSessionKey: "agent:main:telegram:default:direct:123",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterOrigin: {
|
||||
channel: "telegram",
|
||||
accountId: "bot-1",
|
||||
to: "telegram:direct:789",
|
||||
},
|
||||
spawnMode: "run",
|
||||
expectsCompletionMessage: true,
|
||||
});
|
||||
|
||||
expect(origin).toEqual({
|
||||
channel: "telegram",
|
||||
accountId: "bot-1",
|
||||
to: "telegram:direct:789",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to child binding when requester has no binding", async () => {
|
||||
registerSessionBindingAdapter({
|
||||
channel: "telegram",
|
||||
accountId: "bot-1",
|
||||
listBySession: (targetSessionKey: string) => {
|
||||
if (targetSessionKey === "agent:main:telegram:default:direct:123") {
|
||||
return [
|
||||
{
|
||||
bindingId: "telegram:bot-1:child-dm",
|
||||
targetSessionKey,
|
||||
targetKind: "subagent",
|
||||
conversation: {
|
||||
channel: "telegram",
|
||||
accountId: "bot-1",
|
||||
conversationId: "direct:123",
|
||||
},
|
||||
status: "active",
|
||||
boundAt: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
},
|
||||
resolveByConversation: () => null,
|
||||
});
|
||||
|
||||
const origin = await resolveSubagentCompletionOrigin({
|
||||
childSessionKey: "agent:main:telegram:default:direct:123",
|
||||
requesterSessionKey: "agent:main:main",
|
||||
requesterOrigin: {
|
||||
channel: "telegram",
|
||||
accountId: "bot-1",
|
||||
to: "telegram:direct:123",
|
||||
},
|
||||
spawnMode: "run",
|
||||
expectsCompletionMessage: true,
|
||||
});
|
||||
|
||||
expect(origin).toEqual({
|
||||
channel: "telegram",
|
||||
accountId: "bot-1",
|
||||
to: "telegram:direct:123",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("deliverSubagentAnnouncement active requester steering", () => {
|
||||
|
||||
@@ -363,6 +363,23 @@ export async function resolveSubagentCompletionOrigin(params: {
|
||||
channel && conversationId ? { channel, accountId, conversationId } : undefined;
|
||||
|
||||
const router = createBoundDeliveryRouter();
|
||||
const requesterRoute = router.resolveDestination({
|
||||
eventKind: "task_completion",
|
||||
targetSessionKey: params.requesterSessionKey,
|
||||
requester: requesterConversation,
|
||||
failClosed: true,
|
||||
});
|
||||
if (requesterRoute.mode === "bound" && requesterRoute.binding) {
|
||||
return mergeDeliveryContext(
|
||||
resolveBoundConversationOrigin({
|
||||
bindingConversation: requesterRoute.binding.conversation,
|
||||
requesterConversation,
|
||||
requesterOrigin,
|
||||
}),
|
||||
requesterOrigin,
|
||||
);
|
||||
}
|
||||
|
||||
const childRoute = router.resolveDestination({
|
||||
eventKind: "task_completion",
|
||||
targetSessionKey: params.childSessionKey,
|
||||
@@ -380,23 +397,6 @@ export async function resolveSubagentCompletionOrigin(params: {
|
||||
);
|
||||
}
|
||||
|
||||
const route = router.resolveDestination({
|
||||
eventKind: "task_completion",
|
||||
targetSessionKey: params.requesterSessionKey,
|
||||
requester: requesterConversation,
|
||||
failClosed: true,
|
||||
});
|
||||
if (route.mode === "bound" && route.binding) {
|
||||
return mergeDeliveryContext(
|
||||
resolveBoundConversationOrigin({
|
||||
bindingConversation: route.binding.conversation,
|
||||
requesterConversation,
|
||||
requesterOrigin,
|
||||
}),
|
||||
requesterOrigin,
|
||||
);
|
||||
}
|
||||
|
||||
const hookRunner = getGlobalHookRunner();
|
||||
if (!hookRunner?.hasHooks("subagent_delivery_target")) {
|
||||
return requesterOrigin;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import {
|
||||
resolveDisplaySessionKey,
|
||||
resolveInternalSessionKey,
|
||||
resolveMainSessionAlias,
|
||||
} from "./tools/sessions-helpers.js";
|
||||
|
||||
export type SubagentSpawnOwnership = {
|
||||
controllerSessionKey: string;
|
||||
threadBindingRequesterSessionKey: string;
|
||||
completionRequesterSessionKey: string;
|
||||
completionRequesterDisplayKey: string;
|
||||
};
|
||||
|
||||
export function resolveSubagentSpawnOwnership(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentSessionKey?: string;
|
||||
completionOwnerKey?: string;
|
||||
}): SubagentSpawnOwnership {
|
||||
const { mainKey, alias } = resolveMainSessionAlias(params.cfg);
|
||||
const controllerSessionKey = params.agentSessionKey
|
||||
? resolveInternalSessionKey({
|
||||
key: params.agentSessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
})
|
||||
: alias;
|
||||
const completionOwnerKey = params.completionOwnerKey?.trim();
|
||||
const completionRequesterSessionKey = completionOwnerKey
|
||||
? resolveInternalSessionKey({
|
||||
key: completionOwnerKey,
|
||||
alias,
|
||||
mainKey,
|
||||
})
|
||||
: controllerSessionKey;
|
||||
const completionRequesterDisplayKey = resolveDisplaySessionKey({
|
||||
key: completionRequesterSessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
});
|
||||
|
||||
return {
|
||||
controllerSessionKey,
|
||||
threadBindingRequesterSessionKey: controllerSessionKey,
|
||||
completionRequesterSessionKey,
|
||||
completionRequesterDisplayKey,
|
||||
};
|
||||
}
|
||||
@@ -259,6 +259,26 @@ describe("spawnSubagentDirect seam flow", () => {
|
||||
expect(agentParams.cleanupBundleMcpOnRunEnd).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps controller ownership separate from completion ownership", async () => {
|
||||
await spawnSubagentDirect(
|
||||
{
|
||||
task: "background work",
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:main:telegram:default:direct:456",
|
||||
completionOwnerKey: "agent:main:main",
|
||||
agentChannel: "telegram",
|
||||
agentAccountId: "default",
|
||||
agentTo: "telegram:direct:456",
|
||||
},
|
||||
);
|
||||
|
||||
const registerInput = firstRegisteredSubagentRun();
|
||||
expect(registerInput.controllerSessionKey).toBe("agent:main:telegram:default:direct:456");
|
||||
expect(registerInput.requesterSessionKey).toBe("agent:main:main");
|
||||
expect(registerInput.requesterDisplayKey).toBe("agent:main:main");
|
||||
});
|
||||
|
||||
it("omits requesterOrigin threadId when no requester thread is provided", async () => {
|
||||
hoisted.callGatewayMock.mockImplementation(async (request: { method?: string }) => {
|
||||
if (request.method === "agent") {
|
||||
|
||||
@@ -19,12 +19,18 @@ const hoisted = vi.hoisted(() => ({
|
||||
}));
|
||||
|
||||
function firstRegisteredSubagentRun(): {
|
||||
controllerSessionKey?: string;
|
||||
requesterSessionKey?: string;
|
||||
requesterDisplayKey?: string;
|
||||
requesterOrigin?: { channel?: string; accountId?: string; to?: string };
|
||||
expectsCompletionMessage?: boolean;
|
||||
spawnMode?: string;
|
||||
} {
|
||||
const call = hoisted.registerSubagentRunMock.mock.calls[0]?.[0] as
|
||||
| {
|
||||
controllerSessionKey?: string;
|
||||
requesterSessionKey?: string;
|
||||
requesterDisplayKey?: string;
|
||||
requesterOrigin?: { channel?: string; accountId?: string; to?: string };
|
||||
expectsCompletionMessage?: boolean;
|
||||
spawnMode?: string;
|
||||
@@ -187,6 +193,45 @@ describe("spawnSubagentDirect thread binding delivery", () => {
|
||||
expect(registeredRun?.spawnMode).toBe("session");
|
||||
});
|
||||
|
||||
it("uses controller ownership for thread binding while completion routes to owner", async () => {
|
||||
let hookRequesterSessionKey: string | undefined;
|
||||
hoisted.hookRunner.hasHooks.mockImplementation(
|
||||
(hookName?: string) => hookName === "subagent_spawning",
|
||||
);
|
||||
hoisted.hookRunner.runSubagentSpawning.mockImplementation(
|
||||
async (_event: unknown, ctx?: { requesterSessionKey?: string }) => {
|
||||
hookRequesterSessionKey = ctx?.requesterSessionKey;
|
||||
return {
|
||||
status: "ok",
|
||||
threadBindingReady: true,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const result = await spawnSubagentDirect(
|
||||
{
|
||||
task: "reply with a marker",
|
||||
thread: true,
|
||||
mode: "session",
|
||||
context: "isolated",
|
||||
},
|
||||
{
|
||||
agentSessionKey: "agent:main:telegram:default:direct:456",
|
||||
completionOwnerKey: "agent:main:main",
|
||||
agentChannel: "telegram",
|
||||
agentAccountId: "default",
|
||||
agentTo: "telegram:direct:456",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status).toBe("accepted");
|
||||
expect(hookRequesterSessionKey).toBe("agent:main:telegram:default:direct:456");
|
||||
const registeredRun = firstRegisteredSubagentRun();
|
||||
expect(registeredRun.controllerSessionKey).toBe("agent:main:telegram:default:direct:456");
|
||||
expect(registeredRun.requesterSessionKey).toBe("agent:main:main");
|
||||
expect(registeredRun.requesterDisplayKey).toBe("agent:main:main");
|
||||
});
|
||||
|
||||
it("keeps completion announcements when only a generic binding is available", async () => {
|
||||
hoisted.hookRunner.hasHooks.mockImplementation(
|
||||
(hookName?: string) => hookName === "subagent_spawning",
|
||||
|
||||
@@ -35,6 +35,7 @@ import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
|
||||
import { buildSubagentInitialUserMessage } from "./subagent-initial-user-message.js";
|
||||
import { countActiveRunsForSession, registerSubagentRun } from "./subagent-registry.js";
|
||||
import { resolveSubagentSpawnAcceptedNote } from "./subagent-spawn-accepted-note.js";
|
||||
import { resolveSubagentSpawnOwnership } from "./subagent-spawn-ownership.js";
|
||||
import { resolveSubagentTargetPolicy } from "./subagent-target-policy.js";
|
||||
import { normalizeSubagentTaskName } from "./subagent-task-name.js";
|
||||
export {
|
||||
@@ -66,7 +67,6 @@ import {
|
||||
resolveParentForkDecision,
|
||||
resolveAgentConfig,
|
||||
resolveContextEngine,
|
||||
resolveDisplaySessionKey,
|
||||
resolveGatewaySessionStoreTarget,
|
||||
resolveInternalSessionKey,
|
||||
resolveMainSessionAlias,
|
||||
@@ -147,6 +147,8 @@ export type SpawnSubagentParams = {
|
||||
|
||||
export type SpawnSubagentContext = {
|
||||
agentSessionKey?: string;
|
||||
/** Separate key used only for completion routing, not sandbox policy. */
|
||||
completionOwnerKey?: string;
|
||||
agentChannel?: string;
|
||||
agentAccountId?: string;
|
||||
agentTo?: string;
|
||||
@@ -769,10 +771,10 @@ export async function spawnSubagentDirect(
|
||||
mainKey,
|
||||
})
|
||||
: alias;
|
||||
const requesterDisplayKey = resolveDisplaySessionKey({
|
||||
key: requesterInternalKey,
|
||||
alias,
|
||||
mainKey,
|
||||
const ownership = resolveSubagentSpawnOwnership({
|
||||
cfg,
|
||||
agentSessionKey: ctx.agentSessionKey,
|
||||
completionOwnerKey: ctx.completionOwnerKey,
|
||||
});
|
||||
|
||||
const callerDepth = getSubagentDepthFromSessionStore(requesterInternalKey, { cfg });
|
||||
@@ -980,7 +982,7 @@ export async function spawnSubagentDirect(
|
||||
agentId: targetAgentId,
|
||||
label: label || undefined,
|
||||
mode: spawnMode,
|
||||
requesterSessionKey: requesterInternalKey,
|
||||
requesterSessionKey: ownership.threadBindingRequesterSessionKey,
|
||||
requester: {
|
||||
channel: childSessionOrigin?.channel,
|
||||
accountId: childSessionOrigin?.accountId,
|
||||
@@ -1245,10 +1247,10 @@ export async function spawnSubagentDirect(
|
||||
registerSubagentRun({
|
||||
runId: childRunId,
|
||||
childSessionKey,
|
||||
controllerSessionKey: requesterInternalKey,
|
||||
requesterSessionKey: requesterInternalKey,
|
||||
controllerSessionKey: ownership.controllerSessionKey,
|
||||
requesterSessionKey: ownership.completionRequesterSessionKey,
|
||||
requesterOrigin,
|
||||
requesterDisplayKey,
|
||||
requesterDisplayKey: ownership.completionRequesterDisplayKey,
|
||||
task,
|
||||
taskName,
|
||||
cleanup,
|
||||
|
||||
@@ -902,4 +902,87 @@ describe("sessions_spawn tool", () => {
|
||||
expect(contentSchema?.type).toBe("string");
|
||||
expect(contentSchema?.maxLength).toBeUndefined();
|
||||
});
|
||||
|
||||
it("registers requesterSessionKey from the provided agentSessionKey, not the sandbox peer key", async () => {
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:main",
|
||||
agentChannel: "telegram",
|
||||
agentAccountId: "bot-1",
|
||||
agentTo: "telegram:direct:123",
|
||||
});
|
||||
|
||||
await tool.execute("call-requester-key", {
|
||||
task: "background research",
|
||||
});
|
||||
|
||||
expect(hoisted.spawnSubagentDirectMock).toHaveBeenCalledTimes(1);
|
||||
const spawnContext = mockCallArg(hoisted.spawnSubagentDirectMock, 0, 1, "spawnSubagentDirect");
|
||||
expect(spawnContext.agentSessionKey).toBe("agent:main:main");
|
||||
});
|
||||
|
||||
it("does not use the Telegram peer key as requesterSessionKey when agentSessionKey is the run session", async () => {
|
||||
const telegramPeerKey = "agent:main:telegram:default:direct:456";
|
||||
const runSessionKey = "agent:main:main";
|
||||
|
||||
const toolWithPeerKey = createSessionsSpawnTool({
|
||||
agentSessionKey: telegramPeerKey,
|
||||
agentChannel: "telegram",
|
||||
agentAccountId: "default",
|
||||
agentTo: "telegram:direct:456",
|
||||
});
|
||||
|
||||
await toolWithPeerKey.execute("call-peer-key", { task: "task A" });
|
||||
|
||||
const toolWithRunKey = createSessionsSpawnTool({
|
||||
agentSessionKey: runSessionKey,
|
||||
agentChannel: "telegram",
|
||||
agentAccountId: "default",
|
||||
agentTo: "telegram:direct:456",
|
||||
});
|
||||
|
||||
await toolWithRunKey.execute("call-run-key", { task: "task B" });
|
||||
|
||||
const peerContext = mockCallArg(hoisted.spawnSubagentDirectMock, 0, 1, "spawnSubagentDirect");
|
||||
const runContext = mockCallArg(hoisted.spawnSubagentDirectMock, 1, 1, "spawnSubagentDirect");
|
||||
expect(peerContext.agentSessionKey).toBe(telegramPeerKey);
|
||||
expect(runContext.agentSessionKey).toBe(runSessionKey);
|
||||
});
|
||||
|
||||
it("passes completionOwnerKey through to spawnSubagentDirect separately from agentSessionKey", async () => {
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:telegram:default:direct:456",
|
||||
completionOwnerKey: "agent:main:main",
|
||||
agentChannel: "telegram",
|
||||
agentAccountId: "default",
|
||||
agentTo: "telegram:direct:456",
|
||||
});
|
||||
|
||||
await tool.execute("call-completion-owner", { task: "background work" });
|
||||
|
||||
const spawnContext = mockCallArg(hoisted.spawnSubagentDirectMock, 0, 1, "spawnSubagentDirect");
|
||||
expect(spawnContext.agentSessionKey).toBe("agent:main:telegram:default:direct:456");
|
||||
expect(spawnContext.completionOwnerKey).toBe("agent:main:main");
|
||||
});
|
||||
|
||||
it("uses completionOwnerKey for ACP registerSubagentRun requesterSessionKey", async () => {
|
||||
registerAcpBackendForTest();
|
||||
const tool = createSessionsSpawnTool({
|
||||
agentSessionKey: "agent:main:telegram:default:direct:456",
|
||||
completionOwnerKey: "agent:main:main",
|
||||
agentChannel: "telegram",
|
||||
agentAccountId: "default",
|
||||
agentTo: "telegram:direct:456",
|
||||
});
|
||||
|
||||
await tool.execute("call-acp-completion-owner", {
|
||||
runtime: "acp",
|
||||
task: "investigate",
|
||||
agentId: "codex",
|
||||
});
|
||||
|
||||
const registration = mockCallArg(hoisted.registerSubagentRunMock, 0, 0, "registerSubagentRun");
|
||||
expect(registration.controllerSessionKey).toBe("agent:main:telegram:default:direct:456");
|
||||
expect(registration.requesterSessionKey).toBe("agent:main:main");
|
||||
expect(registration.requesterDisplayKey).toBe("agent:main:main");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import { optionalStringEnum } from "../schema/typebox.js";
|
||||
import type { SpawnedToolContext } from "../spawned-context.js";
|
||||
import { registerSubagentRun } from "../subagent-registry.js";
|
||||
import { resolveSubagentSpawnOwnership } from "../subagent-spawn-ownership.js";
|
||||
import {
|
||||
SUBAGENT_SPAWN_CONTEXT_MODES,
|
||||
SUBAGENT_SPAWN_MODES,
|
||||
@@ -37,11 +38,6 @@ import {
|
||||
readStringParam,
|
||||
ToolInputError,
|
||||
} from "./common.js";
|
||||
import {
|
||||
resolveDisplaySessionKey,
|
||||
resolveInternalSessionKey,
|
||||
resolveMainSessionAlias,
|
||||
} from "./sessions-helpers.js";
|
||||
|
||||
const SESSIONS_SPAWN_RUNTIMES = ["subagent", "acp"] as const;
|
||||
const SESSIONS_SPAWN_SANDBOX_MODES = ["inherit", "require"] as const;
|
||||
@@ -250,6 +246,8 @@ function resolveAcpUnavailableMessage(opts?: { sandboxed?: boolean; config?: Ope
|
||||
export function createSessionsSpawnTool(
|
||||
opts?: {
|
||||
agentSessionKey?: string;
|
||||
/** Separate key used only for completion routing (registerSubagentRun requesterSessionKey). */
|
||||
completionOwnerKey?: string;
|
||||
agentChannel?: GatewayMessageChannel;
|
||||
agentAccountId?: string;
|
||||
agentTo?: string;
|
||||
@@ -419,18 +417,10 @@ export function createSessionsSpawnTool(
|
||||
threadRequested: thread,
|
||||
});
|
||||
const trackedCleanup = trackedSpawnMode === "session" ? "keep" : cleanup;
|
||||
const { mainKey, alias } = resolveMainSessionAlias(cfg);
|
||||
const requesterInternalKey = opts?.agentSessionKey
|
||||
? resolveInternalSessionKey({
|
||||
key: opts.agentSessionKey,
|
||||
alias,
|
||||
mainKey,
|
||||
})
|
||||
: alias;
|
||||
const requesterDisplayKey = resolveDisplaySessionKey({
|
||||
key: requesterInternalKey,
|
||||
alias,
|
||||
mainKey,
|
||||
const ownership = resolveSubagentSpawnOwnership({
|
||||
cfg,
|
||||
agentSessionKey: opts?.agentSessionKey,
|
||||
completionOwnerKey: opts?.completionOwnerKey,
|
||||
});
|
||||
const requesterOrigin = normalizeDeliveryContext({
|
||||
channel: opts?.agentChannel,
|
||||
@@ -445,9 +435,10 @@ export function createSessionsSpawnTool(
|
||||
registerSubagentRun({
|
||||
runId: childRunId,
|
||||
childSessionKey,
|
||||
requesterSessionKey: requesterInternalKey,
|
||||
controllerSessionKey: ownership.controllerSessionKey,
|
||||
requesterSessionKey: ownership.completionRequesterSessionKey,
|
||||
requesterOrigin,
|
||||
requesterDisplayKey,
|
||||
requesterDisplayKey: ownership.completionRequesterDisplayKey,
|
||||
task,
|
||||
taskName,
|
||||
cleanup: trackedCleanup,
|
||||
@@ -497,6 +488,7 @@ export function createSessionsSpawnTool(
|
||||
},
|
||||
{
|
||||
agentSessionKey: opts?.agentSessionKey,
|
||||
completionOwnerKey: opts?.completionOwnerKey,
|
||||
agentChannel: opts?.agentChannel,
|
||||
agentAccountId: opts?.agentAccountId,
|
||||
agentTo: opts?.agentTo,
|
||||
|
||||
Reference in New Issue
Block a user