refactor(agents): move spawn family into subagents/spawn (#121350)

* refactor(agents): move spawn family into subagents/spawn

* refactor(agents): keep spawn imports within lint budget
This commit is contained in:
Peter Steinberger
2026-08-09 19:51:29 -07:00
committed by GitHub
parent 5d191c3984
commit 8430fc0e3b
76 changed files with 410 additions and 370 deletions
+3 -3
View File
@@ -319,8 +319,8 @@ packages/tool-call-repair/src/stream-normalizer.test.ts
packages/tool-call-repair/src/stream-normalizer.ts
src/acp/control-plane/manager.test.ts
src/acp/control-plane/manager.turn-results.test.ts
src/agents/acp-spawn-parent-stream.test.ts
src/agents/acp-spawn.test.ts
src/agents/subagents/spawn/acp-spawn-parent-stream.test.ts
src/agents/subagents/spawn/acp-spawn.test.ts
src/agents/agent-bundle-mcp-runtime.test.ts
src/agents/agent-bundle-mcp-runtime.ts
src/agents/agent-command.live-model-switch.test.ts
@@ -454,7 +454,7 @@ src/agents/subagent-registry-lifecycle.test.ts
src/agents/subagent-registry-run-manager.ts
src/agents/subagent-registry.steer-restart.test.ts
src/agents/subagent-registry.test.ts
src/agents/subagent-spawn.test.ts
src/agents/subagents/spawn/subagent-spawn.test.ts
src/agents/system-prompt.test.ts
src/agents/system-prompt.ts
src/agents/tool-display-common.ts
+1 -1
View File
@@ -100,7 +100,7 @@ New `tools.swarm` (global + per-agent override, same merge pattern as
carries `structured: undefined` plus the raw text and a `schemaError`.
- `fastMode: true | "auto" | false` — threaded into the child session patch
alongside model/thinking via `resolveSubagentModelAndThinkingPlan`
(`src/agents/subagent-spawn-plan.ts`), using the existing `FastMode` axis
(`src/agents/subagents/spawn/subagent-spawn-plan.ts`), using the existing `FastMode` axis
(`src/shared/fast-mode.ts`). Omitted = inherit.
- `groupId: string` — swarm group stamp. Defaults to
`swarm:<requesterSessionKey>:<runId-of-requesting-run>`. Persisted on the
@@ -18,7 +18,7 @@ scenario:
- docs/tools/subagents.md
- docs/help/testing.md
codeRefs:
- src/agents/subagent-spawn.ts
- src/agents/subagents/spawn/subagent-spawn.ts
- src/agents/system-prompt.ts
- extensions/qa-lab/src/suite.ts
execution:
@@ -17,7 +17,7 @@ scenario:
- docs/concepts/session-tool.md
codeRefs:
- src/agents/tools/sessions-spawn-tool.ts
- src/agents/subagent-spawn.ts
- src/agents/subagents/spawn/subagent-spawn.ts
execution:
kind: flow
summary: Ask the agent to delegate work that depends on the current transcript and assert sessions_spawn carries context=fork.
+17 -2
View File
@@ -689,12 +689,16 @@ function describeSubagentSeamKinds(relativePath: string, source: string) {
relativePath === "src/agents/subagent-announce-dispatch.ts";
const importsSpawnRuntime = hasAnyImportSource(source, [
"./subagent-spawn.js",
"../subagent-spawn.js",
"./acp-spawn.js",
"../acp-spawn.js",
"./subagents/spawn/subagent-spawn.js",
"../subagents/spawn/subagent-spawn.js",
"./subagents/spawn/acp-spawn.js",
"../subagents/spawn/acp-spawn.js",
"./subagent-registry.js",
"../subagent-registry.js",
"../../subagent-registry.js",
"../acp/control-plane/manager.js",
"../../../acp/control-plane/manager.js",
]);
const importsLifecycleRegistry = hasAnyImportSource(source, [
"./subagent-registry-completion.js",
@@ -705,6 +709,9 @@ function describeSubagentSeamKinds(relativePath: string, source: string) {
"../context-engine/init.js",
"../context-engine/registry.js",
"../sessions/session-lifecycle-events.js",
"../../../context-engine/init.js",
"../../../context-engine/registry.js",
"../../../sessions/session-lifecycle-events.js",
]);
const importsAnnounceDelivery = hasAnyImportSource(source, [
"./subagent-announce.js",
@@ -713,17 +720,25 @@ function describeSubagentSeamKinds(relativePath: string, source: string) {
"../infra/outbound/bound-delivery-router.js",
"../utils/delivery-context.shared.js",
"../gateway/call.js",
"../../../infra/outbound/bound-delivery-router.js",
"../../../utils/delivery-context.shared.js",
"../../../gateway/call.js",
]);
const importsCleanup = hasAnyImportSource(source, [
"../gateway/call.js",
"./subagent-registry-cleanup.js",
"../acp/control-plane/spawn.js",
"../../../gateway/call.js",
"../../../acp/control-plane/spawn.js",
]);
const importsParentStream = hasAnyImportSource(source, [
"./acp-spawn-parent-stream.js",
"../infra/heartbeat-wake.js",
"../infra/system-events.js",
"../infra/agent-events.js",
"../../../infra/heartbeat-wake.js",
"../../../infra/system-events.js",
"../../../infra/agent-events.js",
]);
if (
@@ -17,7 +17,7 @@ const repoRoot = resolveRepoRoot(import.meta.url);
const acpCoreProtectedSources = [
path.join(repoRoot, "src", "acp"),
path.join(repoRoot, "src", "agents", "acp-spawn.ts"),
path.join(repoRoot, "src", "agents", "subagents", "spawn", "acp-spawn.ts"),
path.join(repoRoot, "src", "auto-reply", "reply", "commands-acp"),
path.join(repoRoot, "src", "infra", "outbound", "conversation-id.ts"),
];
+1 -1
View File
@@ -101,7 +101,7 @@ export const migratedSessionAccessorFiles = new Set([
"src/acp/control-plane/manager.background-task.ts",
"src/acp/control-plane/manager.core.ts",
"src/acp/runtime/session-meta.ts",
"src/agents/acp-spawn.ts",
"src/agents/subagents/spawn/acp-spawn.ts",
"src/agents/auth-profiles/session-override.ts",
"src/agents/embedded-agent-runner/compaction-successor-transcript.ts",
"src/agents/embedded-agent-runner/run/attempt.ts",
@@ -9,7 +9,7 @@
"src/config/sessions/session-accessor.entry.ts": 2
},
"sessionAccessorWrite": {
"src/agents/subagent-spawn.test-helpers.ts": 1,
"src/agents/subagents/spawn/subagent-spawn.test-helpers.ts": 1,
"src/plugins/registry-runtime.ts": 1
},
"sessionCompactManualTrim": {},
@@ -13,7 +13,7 @@ type GuardedSource = {
const GUARDED_SOURCES: GuardedSource[] = [
{
path: "agents/acp-spawn.ts",
path: "agents/subagents/spawn/acp-spawn.ts",
forbiddenPatterns: [/\bgetThreadBindingManager\b/, /\bparseDiscordTarget\b/],
},
{
@@ -410,7 +410,7 @@ vi.mock("../../sessions/sdk.js", () => ({
hoisted.createAgentSessionMock(...args),
}));
vi.mock("../../subagent-spawn.js", () => ({
vi.mock("../../subagents/spawn/subagent-spawn.js", () => ({
SUBAGENT_SPAWN_MODES: ["run", "session"],
spawnSubagentDirect: (...args: unknown[]) => hoisted.spawnSubagentDirectMock(...args),
}));
@@ -4,7 +4,7 @@ import {
createSubagentSpawnTestConfig,
loadSubagentSpawnModuleForTest,
setupAcceptedSubagentGatewayMock,
} from "./subagent-spawn.test-helpers.js";
} from "./subagents/spawn/subagent-spawn.test-helpers.js";
const hoisted = vi.hoisted(() => ({
callGatewayMock: vi.fn(),
@@ -12,7 +12,7 @@ const hoisted = vi.hoisted(() => ({
}));
let resetSubagentRegistryForTests: typeof import("./subagent-registry.test-helpers.js").resetSubagentRegistryForTests;
let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect;
let spawnSubagentDirect: typeof import("./subagents/spawn/subagent-spawn.js").spawnSubagentDirect;
function resolveAgentConfigFromList(cfg: Record<string, unknown>, agentId: string) {
const agents = (cfg.agents as { list?: Array<Record<string, unknown>> } | undefined)?.list;
@@ -3,13 +3,13 @@ import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
import { resolveConfiguredSubagentSpawnModelSelection } from "./model-selection.js";
import { resolveSubagentSpawnAcceptedNote } from "./subagent-spawn-accepted-note.js";
import { resolveSubagentSpawnAcceptedNote } from "./subagents/spawn/subagent-spawn-accepted-note.js";
import {
resolveConfiguredSubagentRunTimeoutSeconds,
resolveSubagentModelAndThinkingPlan,
splitModelRef,
} from "./subagent-spawn-plan.js";
import { resolveSubagentThinkingOverride } from "./subagent-spawn-thinking.js";
} from "./subagents/spawn/subagent-spawn-plan.js";
import { resolveSubagentThinkingOverride } from "./subagents/spawn/subagent-spawn-thinking.js";
type SubagentModelPlan = ReturnType<typeof resolveSubagentModelAndThinkingPlan>;
type OkSubagentModelPlan = Extract<SubagentModelPlan, { status: "ok" }>;
@@ -14,7 +14,8 @@ type RunSubagentAnnounceFlow = (typeof import("./subagent-announce.js"))["runSub
type CreateSessionsSpawnTool =
(typeof import("./tools/sessions-spawn-tool.js"))["createSessionsSpawnTool"];
type SubagentRegistryTesting = (typeof import("./subagent-registry.test-helpers.js"))["testing"];
type SubagentSpawnTesting = (typeof import("./subagent-spawn.test-support.js"))["testing"];
type SubagentSpawnTesting =
(typeof import("./subagents/spawn/subagent-spawn.test-support.js"))["testing"];
type CreateOpenClawToolsOpts = Parameters<CreateSessionsSpawnTool>[0];
type GatewayRequest = { method?: string; params?: unknown; timeoutMs?: number };
type AgentWaitCall = { runId?: string; timeoutMs?: number };
@@ -194,7 +195,7 @@ export async function getSessionsSpawnTool(opts: CreateOpenClawToolsOpts) {
if (!cachedSubagentSpawnTesting || !cachedSubagentRegistryTesting) {
const [{ testing: subagentSpawnTesting }, { testing: subagentRegistryTesting }] =
await Promise.all([
import("./subagent-spawn.test-support.js"),
import("./subagents/spawn/subagent-spawn.test-support.js"),
import("./subagent-registry.test-helpers.js"),
]);
cachedSubagentSpawnTesting = subagentSpawnTesting;
+2 -2
View File
@@ -4,7 +4,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
import {
createSubagentSpawnTestConfig,
loadSubagentSpawnModuleForTest,
} from "./subagent-spawn.test-helpers.js";
} from "./subagents/spawn/subagent-spawn.test-helpers.js";
type GatewayRequest = { method?: string; params?: Record<string, unknown> };
type TestBindingRequest = {
@@ -57,7 +57,7 @@ const bindingMocks = vi.hoisted(() => ({
}));
let resetSubagentRegistryForTests: typeof import("./subagent-registry.test-helpers.js").resetSubagentRegistryForTests;
let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect;
let spawnSubagentDirect: typeof import("./subagents/spawn/subagent-spawn.js").spawnSubagentDirect;
function getGatewayRequests(): GatewayRequest[] {
// Gateway call list is the observable side effect for spawn orchestration.
@@ -14,7 +14,7 @@ import type {
dispatchGatewayMethodInProcess,
getRuntimeConfig,
} from "./subagent-announce.runtime.js";
import { terminateAcceptedCollectorRun } from "./subagent-spawn-cleanup.js";
import { terminateAcceptedCollectorRun } from "./subagents/spawn/subagent-spawn-cleanup.js";
type DescendantWakeDeps = {
callGateway: typeof callGateway;
+1 -1
View File
@@ -39,7 +39,7 @@ import {
getGlobalHookRunner,
resolveConversationIdFromTargets,
} from "./subagent-announce-delivery.runtime.js";
import type { SpawnSubagentMode } from "./subagent-spawn.types.js";
import type { SpawnSubagentMode } from "./subagents/spawn/subagent-spawn.types.js";
export type { DeliveryContext } from "../utils/delivery-context.types.js";
function normalizeAnnounceRouteTarget(context?: DeliveryContext): string | undefined {
+1 -1
View File
@@ -59,7 +59,7 @@ import {
} from "./subagent-announce.runtime.js";
import { getSubagentDepthFromSessionStore } from "./subagent-depth.js";
import { deleteSubagentSessionForCleanup } from "./subagent-session-cleanup.js";
import type { SpawnSubagentMode } from "./subagent-spawn.types.js";
import type { SpawnSubagentMode } from "./subagents/spawn/subagent-spawn.types.js";
import { isAnnounceSkip } from "./tools/sessions-send-tokens.js";
type SubagentAnnounceDeps = {
+1 -1
View File
@@ -54,7 +54,7 @@ import {
replaceSubagentRunAfterSteer,
} from "./subagent-registry.js";
import type { SubagentRunRecord } from "./subagent-registry.types.js";
import { terminateAcceptedCollectorRun } from "./subagent-spawn-cleanup.js";
import { terminateAcceptedCollectorRun } from "./subagents/spawn/subagent-spawn-cleanup.js";
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./tools/sessions-helpers.js";
/** Recent-run default window used by subagent control UI/tools. */
+2 -2
View File
@@ -21,8 +21,8 @@ import {
loadSubagentSessionEntry,
type SubagentSessionStoreCache,
} from "./subagent-session-reconciliation.js";
import { retrySubagentCleanup } from "./subagent-spawn-cleanup.js";
import { readGatewayRunId } from "./subagent-spawn-gateway.js";
import { retrySubagentCleanup } from "./subagents/spawn/subagent-spawn-cleanup.js";
import { readGatewayRunId } from "./subagents/spawn/subagent-spawn-gateway.js";
import { resolveSwarmConfig } from "./subagents/swarm/swarm-config.js";
import { enqueueSwarmRun } from "./subagents/swarm/swarm-scheduler.js";
+1 -1
View File
@@ -47,7 +47,7 @@ import {
resolveSubagentSessionCompletion,
resolveSubagentSessionStartedAt,
} from "./subagent-session-reconciliation.js";
import { terminateAcceptedCollectorRun } from "./subagent-spawn-cleanup.js";
import { terminateAcceptedCollectorRun } from "./subagents/spawn/subagent-spawn-cleanup.js";
export type { SubagentRunRecord } from "./subagent-registry.types.js";
const log = createSubsystemLogger("agents/subagent-registry");
+1 -1
View File
@@ -6,7 +6,7 @@ import type { AgentRunSessionTarget } from "./run-session-target.js";
import type { SubagentRunOutcome } from "./subagent-announce-output.js";
import type { SubagentLaunchAuthorization } from "./subagent-launch-authorization.js";
import type { SubagentLifecycleEndedReason } from "./subagent-lifecycle-events.js";
import type { SpawnSubagentMode } from "./subagent-spawn.types.js";
import type { SpawnSubagentMode } from "./subagents/spawn/subagent-spawn.types.js";
export type SubagentCompletionRequest = {
runId: string;
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import { SESSION_LIFECYCLE_CHANGED_ERROR_REASON } from "../config/sessions/lifecycle.js";
import type { callGateway } from "../gateway/call.js";
import type { SpawnSubagentMode } from "./subagent-spawn.types.js";
import type { SpawnSubagentMode } from "./subagents/spawn/subagent-spawn.types.js";
type CallGateway = (options: Parameters<typeof callGateway>[0]) => Promise<unknown>;
type SubagentSessionCleanupOutcome = "deleted" | "changed" | "failed";
-36
View File
@@ -1,36 +0,0 @@
/**
* Runtime dependency barrel for subagent spawning. Keeping these imports in a
* single module lets spawn tests replace runtime seams without loading the
* entire gateway/channel stack.
*/
export { getRuntimeConfig } from "../config/config.js";
export {
loadSessionEntryReadOnly as loadSessionEntry,
upsertSessionEntry,
} from "../config/sessions/session-accessor.js";
export { forkSessionEntryFromParent } from "../auto-reply/reply/session-fork.js";
export { ensureContextEnginesInitialized } from "../context-engine/init.js";
export { resolveContextEngine } from "../context-engine/registry.js";
export { callGateway } from "../gateway/call.js";
export {
dispatchGatewayMethodInProcess,
hasInProcessGatewayContext,
} from "../gateway/server-plugins.js";
export {
ADMIN_SCOPE,
resolveLeastPrivilegeOperatorScopesForMethod,
} from "../gateway/method-scopes.js";
export { getSessionBindingService } from "../infra/outbound/session-binding-service.js";
export { resolveGatewaySessionStoreTarget } from "../gateway/session-utils.js";
export { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
export { emitSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js";
export {
mergeDeliveryContext,
normalizeDeliveryContext,
} from "../utils/delivery-context.shared.js";
export { resolveAgentConfig } from "./agent-scope.js";
export { AGENT_LANE_SUBAGENT } from "./lanes.js";
export { loadPreparedModelCatalog } from "./prepared-model-catalog.js";
export { resolveSandboxRuntimeStatus } from "./sandbox/runtime-status.js";
export { buildSubagentSystemPrompt } from "./subagent-system-prompt.js";
export { resolveInternalSessionKey, resolveMainSessionAlias } from "./tools/sessions-helpers.js";
@@ -1,6 +1,6 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { listTasksForOwnerKey } from "../tasks/runtime-internal.js";
import { getSubagentRunByChildSessionKey } from "./subagent-registry.js";
import { listTasksForOwnerKey } from "../../../tasks/runtime-internal.js";
import { getSubagentRunByChildSessionKey } from "../../subagent-registry.js";
function isActiveTaskStatus(status: string | undefined): boolean {
return status === "queued" || status === "running";
@@ -1,17 +1,17 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { AcpTurnAttachment } from "../acp/control-plane/manager.types.js";
import type { AcpTurnAttachment } from "../../../acp/control-plane/manager.types.js";
import {
formatConversationTarget,
routeFromBindingRecord,
routeToDeliveryFields,
} from "../channels/route-projection.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { SessionBindingRecord } from "../infra/outbound/session-binding-service.js";
import type { AcpSpawnRequesterState } from "./acp-spawn-requester.js";
} from "../../../channels/route-projection.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import type { SessionBindingRecord } from "../../../infra/outbound/session-binding-service.js";
import {
resolveConversationRefForThreadBinding,
resolveSpawnChannelAccountId,
} from "./spawn-plan.js";
} from "../../spawn-plan.js";
import type { AcpSpawnRequesterState } from "./acp-spawn-requester.js";
type GatewayImageAttachmentInput = {
type: "image";
@@ -1,14 +1,14 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { DEFAULT_HEARTBEAT_EVERY } from "../auto-reply/heartbeat.js";
import { parseDurationMs } from "../cli/parse-duration.js";
import { resolveStorePath } from "../config/sessions/paths.js";
import { loadSessionEntryReadOnly } from "../config/sessions/session-accessor.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { areHeartbeatsEnabled } from "../infra/heartbeat-wake.js";
import { normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
import { deliveryContextFromSession } from "../utils/delivery-context.shared.js";
import { listAgentEntries } from "./agent-scope-config.js";
import { resolveAgentConfig, resolveDefaultAgentId } from "./agent-scope.js";
import { DEFAULT_HEARTBEAT_EVERY } from "../../../auto-reply/heartbeat.js";
import { parseDurationMs } from "../../../cli/parse-duration.js";
import { resolveStorePath } from "../../../config/sessions/paths.js";
import { loadSessionEntryReadOnly } from "../../../config/sessions/session-accessor.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { areHeartbeatsEnabled } from "../../../infra/heartbeat-wake.js";
import { normalizeAgentId, parseAgentSessionKey } from "../../../routing/session-key.js";
import { deliveryContextFromSession } from "../../../utils/delivery-context.shared.js";
import { listAgentEntries } from "../../agent-scope-config.js";
import { resolveAgentConfig, resolveDefaultAgentId } from "../../agent-scope.js";
export function isHeartbeatEnabledForSessionAgent(params: {
cfg: OpenClawConfig;
@@ -1,20 +1,20 @@
/** Tests ACP child-to-parent stream relay notices and routing. */
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { mergeMockedModule } from "../test-utils/vitest-module-mocks.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { mergeMockedModule } from "../../../test-utils/vitest-module-mocks.js";
const enqueueSystemEventMock = vi.fn();
const requestHeartbeatMock = vi.fn();
const recordAcpParentStreamEventsMock = vi.fn();
vi.mock("../infra/system-events.js", () => ({
vi.mock("../../../infra/system-events.js", () => ({
enqueueSystemEvent: (...args: unknown[]) => enqueueSystemEventMock(...args),
}));
vi.mock("../infra/heartbeat-wake.js", async () => {
vi.mock("../../../infra/heartbeat-wake.js", async () => {
return await mergeMockedModule(
await vi.importActual<typeof import("../infra/heartbeat-wake.js")>(
"../infra/heartbeat-wake.js",
await vi.importActual<typeof import("../../../infra/heartbeat-wake.js")>(
"../../../infra/heartbeat-wake.js",
),
() => ({
requestHeartbeat: (...args: unknown[]) => requestHeartbeatMock(...args),
@@ -22,10 +22,10 @@ vi.mock("../infra/heartbeat-wake.js", async () => {
);
});
vi.mock("./acp-parent-stream-store.sqlite.js", async () => {
vi.mock("../../acp-parent-stream-store.sqlite.js", async () => {
return await mergeMockedModule(
await vi.importActual<typeof import("./acp-parent-stream-store.sqlite.js")>(
"./acp-parent-stream-store.sqlite.js",
await vi.importActual<typeof import("../../acp-parent-stream-store.sqlite.js")>(
"../../acp-parent-stream-store.sqlite.js",
),
() => ({
recordAcpParentStreamEvents: (...args: unknown[]) => recordAcpParentStreamEventsMock(...args),
@@ -33,7 +33,7 @@ vi.mock("./acp-parent-stream-store.sqlite.js", async () => {
);
});
let emitAgentEvent: typeof import("../infra/agent-events.js").emitAgentEvent;
let emitAgentEvent: typeof import("../../../infra/agent-events.js").emitAgentEvent;
let startAcpSpawnParentStreamRelay: typeof import("./acp-spawn-parent-stream.js").startAcpSpawnParentStreamRelay;
const progressCommentaryDeliveryContext = {
@@ -86,7 +86,7 @@ function firstMockCall(
describe("startAcpSpawnParentStreamRelay", () => {
beforeAll(async () => {
({ emitAgentEvent } = await import("../infra/agent-events.js"));
({ emitAgentEvent } = await import("../../../infra/agent-events.js"));
({ startAcpSpawnParentStreamRelay } = await import("./acp-spawn-parent-stream.js"));
});
@@ -11,30 +11,30 @@ import {
isAcpTagVisible,
resolveAcpProjectionSettings,
type AcpProjectionSettings,
} from "../auto-reply/reply/acp-stream-settings.js";
} from "../../../auto-reply/reply/acp-stream-settings.js";
import {
resolveChannelStreamingProgressCommentary,
type StreamingCompatEntry,
} from "../channels/streaming.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { onAgentEvent } from "../infra/agent-events.js";
} from "../../../channels/streaming.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { onAgentEvent } from "../../../infra/agent-events.js";
import {
type EventSessionRoutingPolicy,
resolveEventSessionKeyForPolicy,
scopedHeartbeatWakeOptionsForPolicy,
} from "../infra/event-session-routing.js";
import { requestHeartbeat } from "../infra/heartbeat-wake.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { resolveNormalizedAccountEntry } from "../routing/account-lookup.js";
import { normalizeAccountId } from "../routing/session-key.js";
import { normalizeAssistantPhase } from "../shared/chat-message-content.js";
import { recordTaskRunProgressByRunId } from "../tasks/detached-task-runtime.js";
import type { DeliveryContext } from "../utils/delivery-context.types.js";
} from "../../../infra/event-session-routing.js";
import { requestHeartbeat } from "../../../infra/heartbeat-wake.js";
import { enqueueSystemEvent } from "../../../infra/system-events.js";
import { createSubsystemLogger } from "../../../logging/subsystem.js";
import { resolveNormalizedAccountEntry } from "../../../routing/account-lookup.js";
import { normalizeAccountId } from "../../../routing/session-key.js";
import { normalizeAssistantPhase } from "../../../shared/chat-message-content.js";
import { recordTaskRunProgressByRunId } from "../../../tasks/detached-task-runtime.js";
import type { DeliveryContext } from "../../../utils/delivery-context.types.js";
import {
recordAcpParentStreamEvents,
type AcpParentStreamEvent,
} from "./acp-parent-stream-store.sqlite.js";
} from "../../acp-parent-stream-store.sqlite.js";
const DEFAULT_STREAM_FLUSH_MS = 2_500;
const DEFAULT_NO_OUTPUT_NOTICE_MS = 60_000;
@@ -1,25 +1,28 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { readAcpSessionMeta } from "../acp/runtime/session-meta.js";
import { resolveStorePath } from "../config/sessions/paths.js";
import { readAcpSessionMeta } from "../../../acp/runtime/session-meta.js";
import { resolveStorePath } from "../../../config/sessions/paths.js";
import {
listSessionEntriesReadOnly,
loadSessionEntryReadOnly,
resolveSessionTranscriptRuntimeTarget,
} from "../config/sessions/session-accessor.js";
import type { SessionAcpMeta, SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { formatErrorMessage } from "../infra/errors.js";
import { getSessionBindingService } from "../infra/outbound/session-binding-service.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { isSubagentSessionKey, parseAgentSessionKey } from "../routing/session-key.js";
import { normalizeDeliveryContext } from "../utils/delivery-context.shared.js";
} from "../../../config/sessions/session-accessor.js";
import type { SessionAcpMeta, SessionEntry } from "../../../config/sessions/types.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { formatErrorMessage } from "../../../infra/errors.js";
import { getSessionBindingService } from "../../../infra/outbound/session-binding-service.js";
import { createSubsystemLogger } from "../../../logging/subsystem.js";
import { isSubagentSessionKey, parseAgentSessionKey } from "../../../routing/session-key.js";
import { normalizeDeliveryContext } from "../../../utils/delivery-context.shared.js";
import { resolveRequesterOriginForChild } from "../../spawn-requester-origin.js";
import type { SessionCapabilityStore } from "../../subagent-capabilities.js";
import {
resolveInternalSessionKey,
resolveMainSessionAlias,
} from "../../tools/sessions-helpers.js";
import {
hasSessionLocalHeartbeatRelayRoute,
isHeartbeatEnabledForSessionAgent,
} from "./acp-spawn-heartbeat.js";
import { resolveRequesterOriginForChild } from "./spawn-requester-origin.js";
import type { SessionCapabilityStore } from "./subagent-capabilities.js";
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./tools/sessions-helpers.js";
const log = createSubsystemLogger("agents/acp-spawn");
@@ -4,33 +4,33 @@ import {
} from "@openclaw/acp-core/runtime/session-identifiers";
import type { AcpRuntimeSessionMode } from "@openclaw/acp-core/runtime/types";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { getAcpSessionManager } from "../acp/control-plane/manager.js";
import type { AcpSpawnRuntimeCloseHandle } from "../acp/control-plane/spawn.js";
import { formatThinkingLevels } from "../auto-reply/thinking.js";
import { getAcpSessionManager } from "../../../acp/control-plane/manager.js";
import type { AcpSpawnRuntimeCloseHandle } from "../../../acp/control-plane/spawn.js";
import { formatThinkingLevels } from "../../../auto-reply/thinking.js";
import {
resolveThreadBindingIntroText,
resolveThreadBindingThreadName,
} from "../channels/thread-bindings-messages.js";
} from "../../../channels/thread-bindings-messages.js";
import {
resolveThreadBindingIdleTimeoutMsForChannel,
resolveThreadBindingMaxAgeMsForChannel,
} from "../channels/thread-bindings-policy.js";
import { resolveStorePath } from "../config/sessions/paths.js";
import { loadSessionEntry } from "../config/sessions/session-accessor.js";
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { isMissingPathError } from "../infra/errors.js";
} from "../../../channels/thread-bindings-policy.js";
import { resolveStorePath } from "../../../config/sessions/paths.js";
import { loadSessionEntry } from "../../../config/sessions/session-accessor.js";
import type { SessionEntry } from "../../../config/sessions/types.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { isMissingPathError } from "../../../infra/errors.js";
import {
getSessionBindingService,
type SessionBindingRecord,
} from "../infra/outbound/session-binding-service.js";
import { persistAcpSpawnSessionFileBestEffort } from "./acp-spawn-requester.js";
import { resolveAgentConfig } from "./agent-scope.js";
} from "../../../infra/outbound/session-binding-service.js";
import { resolveAgentConfig } from "../../agent-scope.js";
import {
resolveConfiguredSubagentSpawnModelSelection,
resolveThinkingDefault,
} from "./model-selection.js";
import type { PreparedSpawnThreadBinding } from "./spawn-plan.js";
} from "../../model-selection.js";
import type { PreparedSpawnThreadBinding } from "../../spawn-plan.js";
import { persistAcpSpawnSessionFileBestEffort } from "./acp-spawn-requester.js";
import { splitModelRef } from "./subagent-spawn-plan.js";
import { resolveSubagentThinkingOverride } from "./subagent-spawn-thinking.js";
@@ -1,7 +1,7 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeOptionalAgentId } from "../routing/session-key.js";
import { listAgentEntries } from "./agent-scope-config.js";
import { listAgentIds } from "./agent-scope.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { normalizeOptionalAgentId } from "../../../routing/session-key.js";
import { listAgentEntries } from "../../agent-scope-config.js";
import { listAgentIds } from "../../agent-scope.js";
export function resolveTargetAcpAgentId(params: {
requestedAgentId?: string;
@@ -4,33 +4,33 @@ import os from "node:os";
import path from "node:path";
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { AcpInitializeSessionInput } from "../acp/control-plane/manager.types.js";
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { setGatewayDedupeEntry, waitForAgentJob } from "../gateway/agent-turn/agent-job.js";
import type { CallGatewayOptions } from "../gateway/call.js";
import type { DedupeEntry } from "../gateway/server-shared.js";
import type { AcpInitializeSessionInput } from "../../../acp/control-plane/manager.types.js";
import type { SessionEntry } from "../../../config/sessions/types.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { setGatewayDedupeEntry, waitForAgentJob } from "../../../gateway/agent-turn/agent-job.js";
import type { CallGatewayOptions } from "../../../gateway/call.js";
import type { DedupeEntry } from "../../../gateway/server-shared.js";
import {
testing as sessionBindingServiceTesting,
registerSessionBindingAdapter,
type SessionBindingAdapter,
type SessionBindingPlacement,
type SessionBindingRecord,
} from "../infra/outbound/session-binding-service.js";
import { normalizeSessionDeliveryState } from "../utils/delivery-context.shared.js";
import type { AgentRunTerminalReplySnapshot } from "./agent-run-terminal-reply.js";
import { reserveChildAdmissionSlot } from "./child-admission.js";
import { createAcpVisibleTextAccumulator } from "./command/attempt-execution.helpers.js";
} from "../../../infra/outbound/session-binding-service.js";
import { normalizeSessionDeliveryState } from "../../../utils/delivery-context.shared.js";
import type { AgentRunTerminalReplySnapshot } from "../../agent-run-terminal-reply.js";
import { reserveChildAdmissionSlot } from "../../child-admission.js";
import { createAcpVisibleTextAccumulator } from "../../command/attempt-execution.helpers.js";
import {
buildAcpResult,
createAcpToolLifecycleTracker,
emitAcpLifecycleEnd,
} from "./command/attempt-execution.js";
import { resolveThinkingDefault } from "./model-selection.js";
import { SUBAGENT_ENDED_REASON_COMPLETE } from "./subagent-lifecycle-events.js";
import { createSubagentRegistryLifecycleController } from "./subagent-registry-lifecycle.js";
import type { RegisterSubagentRunParams } from "./subagent-registry-run-manager.js";
import type { SubagentRunRecord } from "./subagent-registry.types.js";
} from "../../command/attempt-execution.js";
import { resolveThinkingDefault } from "../../model-selection.js";
import { SUBAGENT_ENDED_REASON_COMPLETE } from "../../subagent-lifecycle-events.js";
import { createSubagentRegistryLifecycleController } from "../../subagent-registry-lifecycle.js";
import type { RegisterSubagentRunParams } from "../../subagent-registry-run-manager.js";
import type { SubagentRunRecord } from "../../subagent-registry.types.js";
type SessionBindingAdapterCapabilities = NonNullable<SessionBindingAdapter["capabilities"]>;
type BoundaryLifecycleControllerParams = Parameters<
@@ -189,59 +189,59 @@ const hoisted = vi.hoisted(() => {
};
});
vi.mock("../acp/control-plane/manager.js", () => ({
vi.mock("../../../acp/control-plane/manager.js", () => ({
getAcpSessionManager: hoisted.getAcpSessionManagerMock,
}));
vi.mock("../acp/control-plane/spawn.js", () => ({
vi.mock("../../../acp/control-plane/spawn.js", () => ({
cleanupFailedAcpSpawn: hoisted.cleanupFailedAcpSpawnMock,
}));
vi.mock("../acp/runtime/session-meta.js", () => ({
vi.mock("../../../acp/runtime/session-meta.js", () => ({
readAcpSessionMeta: (params: unknown) => hoisted.readAcpSessionMetaMock(params),
}));
vi.mock("../channels/plugins/index.js", () => ({
vi.mock("../../../channels/plugins/index.js", () => ({
getChannelPlugin: hoisted.getChannelPluginMock,
getLoadedChannelPlugin: hoisted.getLoadedChannelPluginMock,
normalizeChannelId: hoisted.normalizeChannelIdMock,
}));
vi.mock("../channels/plugins/registry.js", () => ({
vi.mock("../../../channels/plugins/registry.js", () => ({
getChannelPlugin: hoisted.getChannelPluginMock,
getLoadedChannelPlugin: hoisted.getLoadedChannelPluginMock,
normalizeChannelId: hoisted.normalizeChannelIdMock,
}));
vi.mock("../config/sessions/paths.js", () => ({
vi.mock("../../../config/sessions/paths.js", () => ({
resolveStorePath: hoisted.resolveStorePathMock,
}));
vi.mock("../config/sessions/session-accessor.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../config/sessions/session-accessor.js")>()),
vi.mock("../../../config/sessions/session-accessor.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../../../config/sessions/session-accessor.js")>()),
...hoisted.createSessionAccessorMock(),
}));
vi.mock("../config/sessions.js", () => ({
vi.mock("../../../config/sessions.js", () => ({
loadSessionStore: hoisted.loadSessionStoreMock,
resolveAgentIdFromSessionKey: (sessionKey: string) =>
sessionKey.match(/^agent:([^:]+)/)?.[1] ?? "main",
resolveStorePath: hoisted.resolveStorePathMock,
}));
vi.mock("../config/config.js", () => ({
vi.mock("../../../config/config.js", () => ({
getRuntimeConfig: () => hoisted.state.cfg,
}));
vi.mock("../config/sessions/transcript.js", () => ({
vi.mock("../../../config/sessions/transcript.js", () => ({
resolveSessionTranscriptFile: hoisted.resolveSessionTranscriptFileMock,
}));
vi.mock("../gateway/call.js", () => ({
vi.mock("../../../gateway/call.js", () => ({
callGateway: hoisted.callGatewayMock,
}));
vi.mock("../infra/heartbeat-wake.js", () => ({
vi.mock("../../../infra/heartbeat-wake.js", () => ({
areHeartbeatsEnabled: hoisted.areHeartbeatsEnabledMock,
}));
@@ -249,16 +249,16 @@ vi.mock("./acp-spawn-parent-stream.js", () => ({
startAcpSpawnParentStreamRelay: hoisted.startAcpSpawnParentStreamRelayMock,
}));
vi.mock("./subagent-registry.js", async (importOriginal) => ({
...(await importOriginal<typeof import("./subagent-registry.js")>()),
vi.mock("../../subagent-registry.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../../subagent-registry.js")>()),
countActiveRunsForSession: hoisted.countActiveRunsForSessionMock,
getSubagentRunByChildSessionKey: hoisted.getSubagentRunByChildSessionKeyMock,
// ACP registration deliberately moved behind the shared spawn pipeline.
registerSubagentRun: hoisted.registerSubagentRunMock,
}));
vi.mock("../tasks/runtime-internal.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../tasks/runtime-internal.js")>()),
vi.mock("../../../tasks/runtime-internal.js", async (importOriginal) => ({
...(await importOriginal<typeof import("../../../tasks/runtime-internal.js")>()),
listTasksForOwnerKey: hoisted.listTasksForOwnerKeyMock,
}));
@@ -1,38 +1,69 @@
/** Implements ACP subagent/session spawning, binding, limits, and parent-stream setup. */
import crypto from "node:crypto";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { AcpTurnAttachment } from "../acp/control-plane/manager.types.js";
import {
cleanupFailedAcpSpawn,
type AcpSpawnRuntimeCloseHandle,
} from "../acp/control-plane/spawn.js";
import { isAcpEnabledByPolicy, resolveAcpAgentPolicyError } from "../acp/policy.js";
import { getRuntimeConfig } from "../config/config.js";
import { resolveStorePath } from "../config/sessions/paths.js";
import type { AcpTurnAttachment } from "../../../acp/control-plane/manager.types.js";
import type { AcpSpawnRuntimeCloseHandle } from "../../../acp/control-plane/spawn.js";
import { cleanupFailedAcpSpawn } from "../../../acp/control-plane/spawn.js";
import { isAcpEnabledByPolicy, resolveAcpAgentPolicyError } from "../../../acp/policy.js";
import { getRuntimeConfig } from "../../../config/config.js";
import { resolveStorePath } from "../../../config/sessions/paths.js";
import {
loadSessionEntryReadOnly,
upsertSessionEntry,
} from "../config/sessions/session-accessor.js";
import { buildSessionCreationStamp } from "../config/sessions/session-entry-provenance.js";
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { callGateway } from "../gateway/call.js";
import { formatErrorMessage } from "../infra/errors.js";
import { resolveEventSessionRoutingPolicy } from "../infra/event-session-routing.js";
} from "../../../config/sessions/session-accessor.js";
import { buildSessionCreationStamp } from "../../../config/sessions/session-entry-provenance.js";
import type { SessionEntry } from "../../../config/sessions/types.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { callGateway } from "../../../gateway/call.js";
import { formatErrorMessage } from "../../../infra/errors.js";
import { resolveEventSessionRoutingPolicy } from "../../../infra/event-session-routing.js";
import {
getSessionBindingService,
isSessionBindingError,
type SessionBindingRecord,
} from "../infra/outbound/session-binding-service.js";
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
} from "../../../infra/outbound/session-binding-service.js";
import { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
import {
normalizeAgentId,
normalizeOptionalAgentId,
parseAgentSessionKey,
resolveAgentIdFromSessionKey,
} from "../routing/session-key.js";
import { recordSessionCreated, recordSubagentSpawned } from "../sessions/session-state-events.js";
import { deliveryContextFromSession } from "../utils/delivery-context.shared.js";
} from "../../../routing/session-key.js";
import {
recordSessionCreated,
recordSubagentSpawned,
} from "../../../sessions/session-state-events.js";
import { deliveryContextFromSession } from "../../../utils/delivery-context.shared.js";
import { resolveDefaultAgentId } from "../../agent-scope.js";
import { reserveChildAdmissionSlot } from "../../child-admission.js";
import {
findAcpUnsupportedInheritedToolAllow,
findAcpUnsupportedInheritedToolDeny,
formatAcpInheritedToolAllowError,
formatAcpInheritedToolDenyError,
inheritedToolAllowPatch,
inheritedToolDenyPatch,
} from "../../inherited-tool-deny.js";
import { AGENT_LANE_SUBAGENT } from "../../lanes.js";
import { resolveSandboxRuntimeStatus } from "../../sandbox/runtime-status.js";
import {
runSpawnPipeline,
type SpawnBackendAdapter,
summarizeSpawnError,
} from "../../spawn-pipeline.js";
import {
mintSpawnSessionKey,
prepareSpawnThreadBinding,
resolveSpawnAdmission,
resolveSpawnMode,
resolveSpawnSandboxError,
type PreparedSpawnThreadBinding,
} from "../../spawn-plan.js";
import { resolveSpawnedWorkspaceInheritance } from "../../spawned-context.js";
import {
isSubagentEnvelopeSession,
resolveSubagentCapabilityStore,
} from "../../subagent-capabilities.js";
import { countUntrackedActiveAcpRunsForOwner } from "./acp-spawn-admission.js";
import {
resolveAcpSpawnBootstrapDeliveryPlan,
@@ -61,36 +92,6 @@ import {
resolveConfiguredAcpSubagentTargetIds,
resolveTargetAcpAgentId,
} from "./acp-spawn-target.js";
import { resolveDefaultAgentId } from "./agent-scope.js";
import { reserveChildAdmissionSlot } from "./child-admission.js";
import {
findAcpUnsupportedInheritedToolAllow,
findAcpUnsupportedInheritedToolDeny,
formatAcpInheritedToolAllowError,
formatAcpInheritedToolDenyError,
inheritedToolAllowPatch,
inheritedToolDenyPatch,
} from "./inherited-tool-deny.js";
import { AGENT_LANE_SUBAGENT } from "./lanes.js";
import { resolveSandboxRuntimeStatus } from "./sandbox/runtime-status.js";
import {
runSpawnPipeline,
type SpawnBackendAdapter,
summarizeSpawnError,
} from "./spawn-pipeline.js";
import {
mintSpawnSessionKey,
prepareSpawnThreadBinding,
resolveSpawnAdmission,
resolveSpawnMode,
resolveSpawnSandboxError,
type PreparedSpawnThreadBinding,
} from "./spawn-plan.js";
import { resolveSpawnedWorkspaceInheritance } from "./spawned-context.js";
import {
isSubagentEnvelopeSession,
resolveSubagentCapabilityStore,
} from "./subagent-capabilities.js";
import { resolveSubagentSpawnOwnership } from "./subagent-spawn-ownership.js";
import { resolveConfiguredSubagentRunTimeoutSeconds } from "./subagent-spawn-plan.js";
@@ -3,7 +3,7 @@
*
* Returns push-based completion guidance for run spawns and thread-binding guidance for session spawns.
*/
import { isCronSessionKey } from "../routing/session-key.js";
import { isCronSessionKey } from "../../../routing/session-key.js";
const SUBAGENT_SPAWN_ACCEPTED_NOTE =
"Auto-announce is push-based. After spawning children, do NOT call sessions_list, sessions_history, exec sleep, or any polling tool. Track expected child session keys. Continue any independent work. If your final answer depends on child output, wait for runtime completion events to arrive as user messages and only answer after completion events for ALL required children arrive. If a child completion event arrives AFTER your final answer, reply ONLY with NO_REPLY.";
@@ -1,19 +1,19 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { isIncognitoSessionKey } from "../routing/session-key.js";
import { resolveUserPath } from "../utils.js";
import { resolveAgentDir } from "./agent-scope-config.js";
import { findModelCatalogEntry } from "./model-catalog-lookup.js";
import { resolveDefaultModelForAgent } from "./model-selection.js";
import { supportsModelTools } from "./model-tool-support.js";
import { summarizeSpawnError } from "./spawn-pipeline.js";
import { resolveSpawnSandboxError, mintSpawnSessionKey } from "./spawn-plan.js";
import { resolveRequesterOriginForChild } from "./spawn-requester-origin.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { isIncognitoSessionKey } from "../../../routing/session-key.js";
import { resolveUserPath } from "../../../utils.js";
import { resolveAgentDir } from "../../agent-scope-config.js";
import { findModelCatalogEntry } from "../../model-catalog-lookup.js";
import { resolveDefaultModelForAgent } from "../../model-selection.js";
import { supportsModelTools } from "../../model-tool-support.js";
import { summarizeSpawnError } from "../../spawn-pipeline.js";
import { resolveSpawnSandboxError, mintSpawnSessionKey } from "../../spawn-plan.js";
import { resolveRequesterOriginForChild } from "../../spawn-requester-origin.js";
import {
mapToolContextToSpawnedRunMetadata,
resolveSpawnedWorkspaceInheritance,
} from "./spawned-context.js";
import type { SubagentLaunchAuthorization } from "./subagent-launch-authorization.js";
} from "../../spawned-context.js";
import type { SubagentLaunchAuthorization } from "../../subagent-launch-authorization.js";
import type {
SpawnSubagentContext,
SpawnSubagentParams,
@@ -1,7 +1,7 @@
import { promises as fs } from "node:fs";
import type { callGateway } from "../gateway/call.js";
import { isFastTestRuntimeEnv } from "../infra/env.js";
import { deleteSubagentSessionForCleanup } from "./subagent-session-cleanup.js";
import type { callGateway } from "../../../gateway/call.js";
import { isFastTestRuntimeEnv } from "../../../infra/env.js";
import { deleteSubagentSessionForCleanup } from "../../subagent-session-cleanup.js";
import { callSubagentGateway } from "./subagent-spawn-gateway.js";
const SUBAGENT_CONTROL_GATEWAY_TIMEOUT_MS = 60_000;
@@ -1,9 +1,9 @@
import { finiteSecondsToTimerSafeMilliseconds } from "@openclaw/normalization-core/number-coercion";
import { resolveThreadBindingSpawnPolicy } from "../channels/thread-bindings-policy.js";
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { SubagentSpawnPreparation } from "../context-engine/types.js";
import { summarizeSpawnError } from "./spawn-pipeline.js";
import { resolveThreadBindingSpawnPolicy } from "../../../channels/thread-bindings-policy.js";
import type { SessionEntry } from "../../../config/sessions/types.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import type { SubagentSpawnPreparation } from "../../../context-engine/types.js";
import { summarizeSpawnError } from "../../spawn-pipeline.js";
import { getSubagentSpawnDeps } from "./subagent-spawn-deps.js";
import { resolveGatewaySessionStoreTarget } from "./subagent-spawn.runtime.js";
import type { SpawnSubagentContextMode } from "./subagent-spawn.types.js";
@@ -1,4 +1,4 @@
import type { FastMode } from "../shared/fast-mode.js";
import type { FastMode } from "../../../shared/fast-mode.js";
import type {
SpawnSubagentContextMode,
SpawnSubagentMode,
@@ -1,4 +1,4 @@
import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js";
import type { SubagentLifecycleHookRunner } from "../../../plugins/hooks.js";
import {
callGateway,
dispatchGatewayMethodInProcess,
@@ -1,6 +1,6 @@
import type { SubagentLaunchAuthorization } from "./subagent-launch-authorization.js";
import { applySubagentLaunchAuthorization } from "./subagent-launch-authorization.js";
import { resolveSubagentRunTimerDelayMs } from "./subagent-run-timeout.js";
import type { SubagentLaunchAuthorization } from "../../subagent-launch-authorization.js";
import { applySubagentLaunchAuthorization } from "../../subagent-launch-authorization.js";
import { resolveSubagentRunTimerDelayMs } from "../../subagent-run-timeout.js";
import { getSubagentSpawnDeps } from "./subagent-spawn-deps.js";
import {
ADMIN_SCOPE,
@@ -1,8 +1,8 @@
import { stringifyRouteThreadId } from "../plugin-sdk/channel-route.js";
import type { BootstrapContextMode } from "./bootstrap-files.js";
import { normalizeSpawnedRunMetadata } from "./spawned-context.js";
import { buildSubagentInitialUserMessage } from "./subagent-initial-user-message.js";
import type { SubagentLaunchAuthorization } from "./subagent-launch-authorization.js";
import { stringifyRouteThreadId } from "../../../plugin-sdk/channel-route.js";
import type { BootstrapContextMode } from "../../bootstrap-files.js";
import { normalizeSpawnedRunMetadata } from "../../spawned-context.js";
import { buildSubagentInitialUserMessage } from "../../subagent-initial-user-message.js";
import type { SubagentLaunchAuthorization } from "../../subagent-launch-authorization.js";
import { resolveSubagentAgentGatewayTimeoutMs } from "./subagent-spawn-gateway.js";
import { AGENT_LANE_SUBAGENT } from "./subagent-spawn.runtime.js";
import type { SpawnSubagentMode } from "./subagent-spawn.types.js";
@@ -1,5 +1,5 @@
import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js";
import type { DeliveryContext } from "../utils/delivery-context.types.js";
import type { SubagentLifecycleHookRunner } from "../../../plugins/hooks.js";
import type { DeliveryContext } from "../../../utils/delivery-context.types.js";
import type { SpawnSubagentMode } from "./subagent-spawn.types.js";
export function createSubagentSpawnLifecycleEmitter(params: {
@@ -3,12 +3,12 @@
*
* Resolves which session controls spawn state, thread binding, and completion delivery.
*/
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import {
resolveDisplaySessionKey,
resolveInternalSessionKey,
resolveMainSessionAlias,
} from "./tools/sessions-helpers.js";
} from "../../tools/sessions-helpers.js";
type SubagentSpawnOwnership = {
controllerSessionKey: string;
@@ -3,14 +3,14 @@
*
* Resolves model, thinking, and timeout choices before the sessions_spawn executor launches work.
*/
import { formatThinkingLevels } from "../auto-reply/thinking.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { FastMode } from "../shared/fast-mode.js";
import { formatThinkingLevels } from "../../../auto-reply/thinking.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import type { FastMode } from "../../../shared/fast-mode.js";
import {
resolveDefaultModelForAgent,
resolveSubagentConfiguredModelSelection,
resolveSubagentSpawnModelSelection,
} from "./model-selection.js";
} from "../../model-selection.js";
import { resolveSubagentThinkingOverride } from "./subagent-spawn-thinking.js";
/** Splits a provider/model ref while preserving model-only refs. */
@@ -1,12 +1,20 @@
import crypto from "node:crypto";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js";
import { isValidAgentId, normalizeAgentId, parseAgentSessionKey } from "../routing/session-key.js";
import { listAgentIds } from "./agent-scope-config.js";
import { reserveChildAdmissionSlot } from "./child-admission.js";
import { resolveSpawnAdmission, resolveSpawnMode } from "./spawn-plan.js";
import { listSwarmRunsForGroup } from "./subagent-registry.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import type { SubagentLifecycleHookRunner } from "../../../plugins/hooks.js";
import {
isValidAgentId,
normalizeAgentId,
parseAgentSessionKey,
} from "../../../routing/session-key.js";
import { listAgentIds } from "../../agent-scope-config.js";
import { reserveChildAdmissionSlot } from "../../child-admission.js";
import { resolveSpawnAdmission, resolveSpawnMode } from "../../spawn-plan.js";
import { listSwarmRunsForGroup } from "../../subagent-registry.js";
import { normalizeSubagentTaskName } from "../../subagent-task-name.js";
import { resolveSwarmConfig } from "../swarm/swarm-config.js";
import { validateStructuredOutputSchema } from "../swarm/swarm-output-schema.js";
import { reserveSwarmRun } from "../swarm/swarm-scheduler.js";
import { resolveSubagentContextMode } from "./subagent-spawn-context.js";
import type {
SpawnSubagentContext,
@@ -18,10 +26,6 @@ import { resolveSubagentSpawnOwnership } from "./subagent-spawn-ownership.js";
import { resolveConfiguredSubagentRunTimeoutSeconds } from "./subagent-spawn-plan.js";
import { loadSubagentConfig } from "./subagent-spawn-session-patch.js";
import { resolveInternalSessionKey, resolveMainSessionAlias } from "./subagent-spawn.runtime.js";
import { normalizeSubagentTaskName } from "./subagent-task-name.js";
import { resolveSwarmConfig } from "./subagents/swarm/swarm-config.js";
import { validateStructuredOutputSchema } from "./subagents/swarm/swarm-output-schema.js";
import { reserveSwarmRun } from "./subagents/swarm/swarm-scheduler.js";
type ResolvedSubagentSpawnRequest = {
request: {
@@ -1,13 +1,13 @@
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { FastMode } from "../shared/fast-mode.js";
import { resolveFastModeState } from "./fast-mode.js";
import type { SessionEntry } from "../../../config/sessions/types.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import type { FastMode } from "../../../shared/fast-mode.js";
import { resolveFastModeState } from "../../fast-mode.js";
import {
normalizeStoredOverrideModel,
resolveDefaultModelForAgent,
resolvePersistedSelectedModelRef,
} from "./model-selection.js";
import { resolveThinkingDefault } from "./model-thinking-default.js";
} from "../../model-selection.js";
import { resolveThinkingDefault } from "../../model-thinking-default.js";
import {
loadSessionEntry,
resolveAgentConfig,
@@ -1,15 +1,15 @@
import { randomUUID } from "node:crypto";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { buildSessionCreationStamp } from "../config/sessions/session-entry-provenance.js";
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveIncognitoOpenClawAgentSqlitePath } from "../state/openclaw-agent-db.js";
import { buildSessionCreationStamp } from "../../../config/sessions/session-entry-provenance.js";
import type { SessionEntry } from "../../../config/sessions/types.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { resolveIncognitoOpenClawAgentSqlitePath } from "../../../state/openclaw-agent-db.js";
import {
inheritedToolAllowPatch,
inheritedToolDenyPatch,
normalizeInheritedToolAllowlist,
normalizeInheritedToolDenylist,
} from "./inherited-tool-deny.js";
} from "../../inherited-tool-deny.js";
import { getSubagentSpawnDeps } from "./subagent-spawn-deps.js";
import { splitModelRef } from "./subagent-spawn-plan.js";
import { resolveGatewaySessionStoreTarget, upsertSessionEntry } from "./subagent-spawn.runtime.js";
@@ -3,8 +3,8 @@
* this helper to patch the child session without leaking invalid caller input.
*/
import { asOptionalObjectRecord } from "@openclaw/normalization-core/record-coerce";
import { normalizeThinkLevel } from "../auto-reply/thinking.shared.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeThinkLevel } from "../../../auto-reply/thinking.shared.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
function readString(value: Record<string, unknown>, key: string): string | undefined {
const raw = value[key];
@@ -1,16 +1,19 @@
import { routeFromBindingRecord, routeToDeliveryFields } from "../channels/route-projection.js";
import {
routeFromBindingRecord,
routeToDeliveryFields,
} from "../../../channels/route-projection.js";
import {
resolveThreadBindingIntroText,
resolveThreadBindingThreadName,
} from "../channels/thread-bindings-messages.js";
} from "../../../channels/thread-bindings-messages.js";
import {
resolveThreadBindingIdleTimeoutMsForChannel,
resolveThreadBindingMaxAgeMsForChannel,
} from "../channels/thread-bindings-policy.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { DeliveryContext } from "../utils/delivery-context.types.js";
import { summarizeSpawnError } from "./spawn-pipeline.js";
import { prepareSpawnThreadBinding } from "./spawn-plan.js";
} from "../../../channels/thread-bindings-policy.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import type { DeliveryContext } from "../../../utils/delivery-context.types.js";
import { summarizeSpawnError } from "../../spawn-pipeline.js";
import { prepareSpawnThreadBinding } from "../../spawn-plan.js";
import { getSessionBindingService } from "./subagent-spawn.runtime.js";
import type { SpawnSubagentMode } from "./subagent-spawn.types.js";
@@ -4,7 +4,7 @@ import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { withEnvAsync } from "../test-utils/env.js";
import { withEnvAsync } from "../../../test-utils/env.js";
import {
createSubagentSpawnTestConfig,
loadSubagentSpawnModuleForTest,
@@ -6,34 +6,34 @@ import {
clearConfigCache,
clearRuntimeConfigSnapshot,
getRuntimeConfig,
} from "../config/config.js";
import { prepareAgentRequestPreflight } from "../gateway/agent-turn/agent-request-preflight.js";
import { createAgentTurnIo } from "../gateway/agent-turn/io.js";
} from "../../../config/config.js";
import { prepareAgentRequestPreflight } from "../../../gateway/agent-turn/agent-request-preflight.js";
import { createAgentTurnIo } from "../../../gateway/agent-turn/io.js";
import type {
GatewayRequestContext,
GatewayRequestOptions,
} from "../gateway/server-methods/types.js";
import { createSyntheticPluginRuntimeClient } from "../gateway/server-plugin-runtime-client.js";
} from "../../../gateway/server-methods/types.js";
import { createSyntheticPluginRuntimeClient } from "../../../gateway/server-plugin-runtime-client.js";
import {
clearFallbackGatewayContext,
type dispatchGatewayMethodInProcess,
} from "../gateway/server-plugins.js";
import { withPluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js";
} from "../../../gateway/server-plugins.js";
import { withPluginRuntimeGatewayRequestScope } from "../../../plugins/runtime/gateway-request-scope.js";
import {
isGatewaySubordinateWorkAdmissionClosed,
resetGatewayWorkAdmission,
tryBeginGatewayRootWorkAdmission,
} from "../process/gateway-work-admission.js";
import { captureEnv, setTestEnvValue } from "../test-utils/env.js";
import { subagentRuns } from "./subagent-registry-memory.js";
import { markSubagentRunTerminated } from "./subagent-registry.js";
} from "../../../process/gateway-work-admission.js";
import { captureEnv, setTestEnvValue } from "../../../test-utils/env.js";
import { subagentRuns } from "../../subagent-registry-memory.js";
import { markSubagentRunTerminated } from "../../subagent-registry.js";
import {
resetSubagentRegistryForTests,
testing as subagentRegistryTesting,
} from "./subagent-registry.test-helpers.js";
} from "../../subagent-registry.test-helpers.js";
import { testing as swarmSchedulerTesting } from "../swarm/swarm-scheduler.test-support.js";
import { spawnSubagentDirect } from "./subagent-spawn.js";
import { testing as subagentSpawnTesting } from "./subagent-spawn.test-support.js";
import { testing as swarmSchedulerTesting } from "./subagents/swarm/swarm-scheduler.test-support.js";
const envSnapshot = captureEnv(["OPENCLAW_CONFIG_PATH", "OPENCLAW_STATE_DIR"]);
let stateDir = "";
@@ -10,7 +10,7 @@ import {
describe('spawnSubagentDirect mode="session" diagnostics (#67400)', () => {
const callGatewayMock = vi.fn();
let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect;
let resetSubagentRegistryForTests: typeof import("./subagent-registry.test-helpers.js").resetSubagentRegistryForTests;
let resetSubagentRegistryForTests: typeof import("../../subagent-registry.test-helpers.js").resetSubagentRegistryForTests;
beforeEach(async () => {
callGatewayMock.mockReset();
@@ -68,7 +68,7 @@ describe('spawnSubagentDirect mode="session" diagnostics (#67400)', () => {
describe('spawnSubagentDirect mode="session" with thread binding-capable channels (#67400)', () => {
const callGatewayMock = vi.fn();
let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect;
let resetSubagentRegistryForTests: typeof import("./subagent-registry.test-helpers.js").resetSubagentRegistryForTests;
let resetSubagentRegistryForTests: typeof import("../../subagent-registry.test-helpers.js").resetSubagentRegistryForTests;
beforeEach(async () => {
callGatewayMock.mockReset();
@@ -13,7 +13,7 @@ import {
const callGatewayMock = vi.fn();
const updateSessionStoreMock = vi.fn();
let resetSubagentRegistryForTests: typeof import("./subagent-registry.test-helpers.js").resetSubagentRegistryForTests;
let resetSubagentRegistryForTests: typeof import("../../subagent-registry.test-helpers.js").resetSubagentRegistryForTests;
let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect;
describe("spawnSubagentDirect runtime model persistence", () => {
@@ -0,0 +1,39 @@
/**
* Runtime dependency barrel for subagent spawning. Keeping these imports in a
* single module lets spawn tests replace runtime seams without loading the
* entire gateway/channel stack.
*/
export { getRuntimeConfig } from "../../../config/config.js";
export {
loadSessionEntryReadOnly as loadSessionEntry,
upsertSessionEntry,
} from "../../../config/sessions/session-accessor.js";
export { forkSessionEntryFromParent } from "../../../auto-reply/reply/session-fork.js";
export { ensureContextEnginesInitialized } from "../../../context-engine/init.js";
export { resolveContextEngine } from "../../../context-engine/registry.js";
export { callGateway } from "../../../gateway/call.js";
export {
dispatchGatewayMethodInProcess,
hasInProcessGatewayContext,
} from "../../../gateway/server-plugins.js";
export {
ADMIN_SCOPE,
resolveLeastPrivilegeOperatorScopesForMethod,
} from "../../../gateway/method-scopes.js";
export { getSessionBindingService } from "../../../infra/outbound/session-binding-service.js";
export { resolveGatewaySessionStoreTarget } from "../../../gateway/session-utils.js";
export { getGlobalHookRunner } from "../../../plugins/hook-runner-global.js";
export { emitSessionLifecycleEvent } from "../../../sessions/session-lifecycle-events.js";
export {
mergeDeliveryContext,
normalizeDeliveryContext,
} from "../../../utils/delivery-context.shared.js";
export { resolveAgentConfig } from "../../agent-scope.js";
export { AGENT_LANE_SUBAGENT } from "../../lanes.js";
export { loadPreparedModelCatalog } from "../../prepared-model-catalog.js";
export { resolveSandboxRuntimeStatus } from "../../sandbox/runtime-status.js";
export { buildSubagentSystemPrompt } from "../../subagent-system-prompt.js";
export {
resolveInternalSessionKey,
resolveMainSessionAlias,
} from "../../tools/sessions-helpers.js";
@@ -3,8 +3,8 @@
import os from "node:os";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { expect, vi } from "vitest";
import { resolveLeastPrivilegeOperatorScopesForMethod } from "../gateway/method-scopes.js";
import type { SubagentLifecycleHookRunner } from "../plugins/hooks.js";
import { resolveLeastPrivilegeOperatorScopesForMethod } from "../../../gateway/method-scopes.js";
import type { SubagentLifecycleHookRunner } from "../../../plugins/hooks.js";
type MockFn = (...args: unknown[]) => unknown;
type MockImplementationTarget = {
@@ -211,7 +211,7 @@ export async function loadSubagentSpawnModuleForTest(params: {
const resetSubagentRegistryForTests = vi.fn();
vi.doMock("./provider-model-normalization.runtime.js", () => ({
vi.doMock("../../provider-model-normalization.runtime.js", () => ({
normalizeProviderModelIdWithRuntime: () => undefined,
}));
@@ -383,11 +383,11 @@ export async function loadSubagentSpawnModuleForTest(params: {
...createDefaultSessionHelperMocks(),
}));
vi.doMock("./subagent-depth.js", () => ({
vi.doMock("../../subagent-depth.js", () => ({
getSubagentDepthFromSessionStore: params.getSubagentDepthFromSessionStore ?? (() => 0),
}));
vi.doMock("./subagent-registry.js", () => ({
vi.doMock("../../subagent-registry.js", () => ({
completeCollectorLaunchCleanup: params.completeCollectorLaunchCleanupMock ?? vi.fn(),
countActiveRunsForSession: params.countActiveRunsForSession ?? (() => 0),
listSwarmRunsForGroup: params.listSwarmRunsForGroup ?? vi.fn(() => []),
@@ -16,7 +16,7 @@ type SpawnDeps = Omit<
>,
"getGlobalHookRunner"
> & {
getGlobalHookRunner: () => import("../plugins/hooks.js").SubagentLifecycleHookRunner | null;
getGlobalHookRunner: () => import("../../../plugins/hooks.js").SubagentLifecycleHookRunner | null;
};
type Testing = {
@@ -3,16 +3,16 @@ import os from "node:os";
// persistence, registry registration, and lifecycle event emission.
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveIncognitoOpenClawAgentSqlitePath } from "../state/openclaw-agent-db.paths.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
import { resolveIncognitoOpenClawAgentSqlitePath } from "../../../state/openclaw-agent-db.paths.js";
import { installAcceptedSubagentGatewayMock } from "../../test-helpers/subagent-gateway.js";
import { testing as swarmSchedulerTesting } from "../swarm/swarm-scheduler.test-support.js";
import {
createSubagentSpawnTestConfig,
expectPersistedRuntimeModel,
installSessionStoreCaptureMock,
loadSubagentSpawnModuleForTest,
} from "./subagent-spawn.test-helpers.js";
import { testing as swarmSchedulerTesting } from "./subagents/swarm/swarm-scheduler.test-support.js";
import { installAcceptedSubagentGatewayMock } from "./test-helpers/subagent-gateway.js";
const hoisted = vi.hoisted(() => ({
callGatewayMock: vi.fn(),
@@ -33,7 +33,7 @@ const hoisted = vi.hoisted(() => ({
configOverride: {} as Record<string, unknown>,
}));
let resetSubagentRegistryForTests: typeof import("./subagent-registry.test-helpers.js").resetSubagentRegistryForTests;
let resetSubagentRegistryForTests: typeof import("../../subagent-registry.test-helpers.js").resetSubagentRegistryForTests;
let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect;
function createConfigOverride(overrides?: Record<string, unknown>) {
@@ -881,7 +881,7 @@ describe("spawnSubagentDirect seam flow", () => {
});
it("shares pending child capacity between native and visible spawn paths", async () => {
const { maybeSpawnVisibleSession } = await import("./tools/sessions-spawn-visible.js");
const { maybeSpawnVisibleSession } = await import("../../tools/sessions-spawn-visible.js");
hoisted.configOverride = createConfigOverride({
agents: {
defaults: {
@@ -2,12 +2,12 @@
// account selection, and completion routing for channel thread spawns.
import os from "node:os";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { installAcceptedSubagentGatewayMock } from "../../test-helpers/subagent-gateway.js";
import {
createSubagentSpawnTestConfig,
installSessionStoreCaptureMock,
loadSubagentSpawnModuleForTest,
} from "./subagent-spawn.test-helpers.js";
import { installAcceptedSubagentGatewayMock } from "./test-helpers/subagent-gateway.js";
const hoisted = vi.hoisted(() => ({
callGatewayMock: vi.fn(),
@@ -45,10 +45,12 @@ function firstRegisteredSubagentRun(): {
describe("spawnSubagentDirect thread binding delivery", () => {
type SpawnModule = Awaited<ReturnType<typeof loadSubagentSpawnModuleForTest>>;
type SetActivePluginRegistry = typeof import("../plugins/runtime.js").setActivePluginRegistry;
type SetActivePluginRegistry =
typeof import("../../../plugins/runtime.js").setActivePluginRegistry;
type CreateChannelTestPluginBase =
typeof import("../test-utils/channel-plugins.js").createChannelTestPluginBase;
type CreateTestRegistry = typeof import("../test-utils/channel-plugins.js").createTestRegistry;
typeof import("../../../test-utils/channel-plugins.js").createChannelTestPluginBase;
type CreateTestRegistry =
typeof import("../../../test-utils/channel-plugins.js").createTestRegistry;
type SessionBindingService = NonNullable<
Parameters<typeof loadSubagentSpawnModuleForTest>[0]["getSessionBindingService"]
>;
@@ -78,11 +80,11 @@ describe("spawnSubagentDirect thread binding delivery", () => {
resolveConversationDeliveryTarget: (params) => currentDeliveryTargetResolver(params),
}));
({ setActivePluginRegistry: setActivePluginRegistryForTest } =
await import("../plugins/runtime.js"));
await import("../../../plugins/runtime.js"));
({
createChannelTestPluginBase: createChannelTestPluginBaseForTest,
createTestRegistry: createTestRegistryForTest,
} = await import("../test-utils/channel-plugins.js"));
} = await import("../../../test-utils/channel-plugins.js"));
});
function installChannelRouteProjectionPluginsForTest() {
@@ -5,28 +5,32 @@
*/
import { promises as fs } from "node:fs";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { isAcpRuntimeSpawnAvailable } from "../acp/runtime/availability.js";
import type { SubagentSpawnPreparation } from "../context-engine/types.js";
import { listRegisteredPluginAgentPromptGuidance } from "../plugins/command-registry-state.js";
import { isAcpRuntimeSpawnAvailable } from "../../../acp/runtime/availability.js";
import type { SubagentSpawnPreparation } from "../../../context-engine/types.js";
import { listRegisteredPluginAgentPromptGuidance } from "../../../plugins/command-registry-state.js";
import {
GatewayDrainingError,
runWithGatewayIndependentRootWorkContinuation,
} from "../process/gateway-work-admission.js";
import { recordSessionCreated, recordSubagentSpawned } from "../sessions/session-state-events.js";
} from "../../../process/gateway-work-admission.js";
import {
recordSessionCreated,
recordSubagentSpawned,
} from "../../../sessions/session-state-events.js";
import {
runSpawnPipeline,
type SpawnBackendAdapter,
summarizeSpawnError,
} from "./spawn-pipeline.js";
} from "../../spawn-pipeline.js";
import {
materializeSubagentAttachments,
type SubagentAttachmentReceiptFile,
} from "./subagent-attachments.js";
} from "../../subagent-attachments.js";
import {
completeCollectorLaunchCleanup,
settleFailedQueuedSubagentLaunch,
startQueuedSubagentRun,
} from "./subagent-registry.js";
} from "../../subagent-registry.js";
import { activateSwarmRun, removeQueuedSwarmRun } from "../swarm/swarm-scheduler.js";
import { resolveSubagentSpawnAcceptedNote } from "./subagent-spawn-accepted-note.js";
import { resolveSubagentChildPlan } from "./subagent-spawn-child-plan.js";
import {
@@ -63,7 +67,6 @@ import {
emitSessionLifecycleEvent,
mergeDeliveryContext,
} from "./subagent-spawn.runtime.js";
import { activateSwarmRun, removeQueuedSwarmRun } from "./subagents/swarm/swarm-scheduler.js";
export { SUBAGENT_SPAWN_CONTEXT_MODES, SUBAGENT_SPAWN_MODES } from "./subagent-spawn.types.js";
@@ -63,7 +63,7 @@ const hoisted = vi.hoisted(() => ({
}));
let spawnSubagentDirect: typeof import("./subagent-spawn.js").spawnSubagentDirect;
let resetSubagentRegistryForTests: typeof import("./subagent-registry.test-helpers.js").resetSubagentRegistryForTests;
let resetSubagentRegistryForTests: typeof import("../../subagent-registry.test-helpers.js").resetSubagentRegistryForTests;
function createConfigOverride(overrides?: Record<string, unknown>) {
return createSubagentSpawnTestConfig("/tmp/workspace-main", {
+2 -2
View File
@@ -29,13 +29,13 @@ const hoisted = vi.hoisted(() => {
};
});
vi.mock("../subagent-spawn.js", () => ({
vi.mock("../subagents/spawn/subagent-spawn.js", () => ({
SUBAGENT_SPAWN_CONTEXT_MODES: ["isolated", "fork"],
SUBAGENT_SPAWN_MODES: ["run", "session"],
spawnSubagentDirect: (...args: unknown[]) => hoisted.spawnSubagentDirectMock(...args),
}));
vi.mock("../acp-spawn.js", () => ({
vi.mock("../subagents/spawn/acp-spawn.js", () => ({
ACP_SPAWN_MODES: ["run", "session"],
ACP_SPAWN_STREAM_TARGETS: ["parent"],
isSpawnAcpAcceptedResult: (result: { status?: string }) => result?.status === "accepted",
+4 -4
View File
@@ -24,12 +24,12 @@ import { optionalStringEnum } from "../schema/typebox.js";
import type { SpawnedToolContext } from "../spawned-context.js";
import { resolveAcpSessionsSpawnImageAttachments } from "../subagent-attachments.js";
import { getSubagentDeliveryBacklogPressure } from "../subagent-registry.js";
import { normalizeSubagentTaskName } from "../subagent-task-name.js";
import {
SUBAGENT_SPAWN_CONTEXT_MODES,
SUBAGENT_SPAWN_MODES,
spawnSubagentDirect,
} from "../subagent-spawn.js";
import { normalizeSubagentTaskName } from "../subagent-task-name.js";
} from "../subagents/spawn/subagent-spawn.js";
import {
SWARM_CODE_MODE_IDEMPOTENCY_KEY,
SWARM_CODE_MODE_REQUEST_FINGERPRINT,
@@ -72,10 +72,10 @@ const UNSUPPORTED_SESSIONS_SPAWN_PARAM_KEYS = [
"replyTo",
"reply_to",
] as const;
type AcpSpawnModule = typeof import("../acp-spawn.js");
type AcpSpawnModule = typeof import("../subagents/spawn/acp-spawn.js");
const acpSpawnModuleLoader = createLazyImportLoader<AcpSpawnModule>(
() => import("../acp-spawn.js"),
() => import("../subagents/spawn/acp-spawn.js"),
);
async function loadAcpSpawnModule(): Promise<AcpSpawnModule> {
+2 -2
View File
@@ -20,9 +20,9 @@ import { resolveSandboxRuntimeStatus } from "../sandbox/runtime-status.js";
import { resolveSpawnedWorkspaceInheritance } from "../spawned-context.js";
import { getSubagentDepthFromSessionStore } from "../subagent-depth.js";
import { countActiveRunsForSession, registerSubagentRun } from "../subagent-registry.js";
import { resolveSubagentSpawnOwnership } from "../subagent-spawn-ownership.js";
import { resolveConfiguredSubagentRunTimeoutSeconds } from "../subagent-spawn-plan.js";
import { resolveSubagentTargetPolicy } from "../subagent-target-policy.js";
import { resolveSubagentSpawnOwnership } from "../subagents/spawn/subagent-spawn-ownership.js";
import { resolveConfiguredSubagentRunTimeoutSeconds } from "../subagents/spawn/subagent-spawn-plan.js";
import { normalizeToolModelOverride, readStringParam, ToolInputError } from "./common.js";
import {
callInProcessGatewayTool,
@@ -8,7 +8,7 @@ import {
testing as registryTesting,
} from "../subagent-registry.test-helpers.js";
import "../subagent-registry.mocks.shared.js";
import { testing as spawnTesting } from "../subagent-spawn.test-support.js";
import { testing as spawnTesting } from "../subagents/spawn/subagent-spawn.test-support.js";
import { testing as swarmSchedulerTesting } from "../subagents/swarm/swarm-scheduler.test-support.js";
import { createAgentsWaitTool } from "./agents-wait-tool.js";
import { createSessionsSpawnTool } from "./sessions-spawn-tool.js";
+1 -1
View File
@@ -98,7 +98,7 @@ vi.mock("../../acp/runtime/session-meta.js", () => ({
resolveSessionStorePathForAcp: (args: unknown) => hoisted.resolveSessionStorePathForAcpMock(args),
}));
vi.mock("../../agents/acp-spawn.js", () => ({
vi.mock("../../agents/subagents/spawn/acp-spawn.js", () => ({
resolveAcpSpawnRuntimePolicyError: (params: { cfg?: OpenClawConfig }) =>
params.cfg?.agents?.defaults?.sandbox?.mode === "all"
? 'Sandboxed sessions cannot spawn ACP sessions because runtime="acp" runs on the host. Use runtime="subagent" from sandboxed sessions.'
@@ -15,11 +15,11 @@ import {
resolveAcpDispatchPolicyMessage,
} from "../../../acp/policy.js";
import { resolveSessionStorePathForAcp } from "../../../acp/runtime/session-meta.js";
import { resolveSpawnedWorkspaceInheritance } from "../../../agents/spawned-context.js";
import {
resolveAcpSpawnRuntimePolicyError,
resolveRuntimeCwdForAcpSpawn,
} from "../../../agents/acp-spawn.js";
import { resolveSpawnedWorkspaceInheritance } from "../../../agents/spawned-context.js";
} from "../../../agents/subagents/spawn/acp-spawn.js";
import { updateSessionEntry } from "../../../config/sessions/session-accessor.js";
import type { SessionAcpMeta } from "../../../config/sessions/types.js";
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
@@ -10,8 +10,8 @@ import { asNullableRecord } from "@openclaw/normalization-core/record-coerce";
import { describe, expect, it } from "vitest";
import { getAcpSessionManager } from "../acp/control-plane/manager.js";
import { getAcpRuntimeBackend } from "../acp/runtime/registry.js";
import { isSpawnAcpAcceptedResult, spawnAcpDirect } from "../agents/acp-spawn.js";
import { isLiveTestEnabled, readLiveTestConfig } from "../agents/live-test-helpers.js";
import { isSpawnAcpAcceptedResult, spawnAcpDirect } from "../agents/subagents/spawn/acp-spawn.js";
import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js";
import { resolveStorePath } from "../config/sessions/paths.js";
import { loadSessionEntry } from "../config/sessions/session-accessor.js";
@@ -1857,7 +1857,7 @@ async function verifyCodexSubagentProbe(params: {
});
try {
const { testing: subagentSpawnTesting, spawnSubagentDirect } =
await import("../agents/subagent-spawn.test-support.js");
await import("../agents/subagents/spawn/subagent-spawn.test-support.js");
const noOpContextEngine: ContextEngine = {
info: { id: "codex-harness-subagent-smoke", name: "Codex harness subagent smoke" },
ingest: async () => ({ ingested: false }),
@@ -1980,7 +1980,7 @@ async function verifyCodexSubagentProbe(params: {
});
} finally {
const { testing: subagentSpawnTesting } =
await import("../agents/subagent-spawn.test-support.js");
await import("../agents/subagents/spawn/subagent-spawn.test-support.js");
subagentSpawnTesting.setDepsForTest();
unsubscribe();
}
@@ -101,7 +101,7 @@ export function resolveGatewayAgentTaskTrackingMode(params: {
return "plugin_subagent";
}
// A confirmed ACP manual-spawn child turn already owns its requester-visible
// `acp` task row from the spawn control plane (src/agents/acp-spawn.ts). The
// `acp` task row from the spawn control plane (src/agents/subagents/spawn/acp-spawn.ts). The
// Gateway CLI path runs that same childRunId, so tracking it here would emit a
// duplicate row for one run. Suppress only the CLI branch; plugin-subagent and
// normal CLI tracking stay intact.
+1 -1
View File
@@ -1,8 +1,8 @@
// Covers task registry lifecycle, delivery, notification, and query behavior.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { AcpSessionStoreEntry } from "../acp/runtime/session-meta.js";
import { startAcpSpawnParentStreamRelay } from "../agents/acp-spawn-parent-stream.js";
import { emitAcpLifecycleStart } from "../agents/command/attempt-execution.js";
import { startAcpSpawnParentStreamRelay } from "../agents/subagents/spawn/acp-spawn-parent-stream.js";
import { resetCronActiveJobs } from "../cron/active-jobs.js";
import { emitAgentEvent, resetAgentEventsForTest } from "../infra/agent-events.js";
import { registerAgentRunContext } from "../infra/agent-run-registry.js";
+16 -11
View File
@@ -54,9 +54,9 @@ describe("audit-seams cron seam classification", () => {
describe("audit-seams subagent seam classification", () => {
it("detects subagent spawn and cleanup handoff boundaries", () => {
const source = `
import { callGateway } from "../gateway/call.js";
import { emitSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js";
import { registerSubagentRun } from "./subagent-registry.js";
import { callGateway } from "../../../gateway/call.js";
import { emitSessionLifecycleEvent } from "../../../sessions/session-lifecycle-events.js";
import { registerSubagentRun } from "../../subagent-registry.js";
export async function spawnSubagentDirect() {
const response = await callGateway({ method: "agent.run", params: { task: "do it" } });
@@ -67,7 +67,7 @@ describe("audit-seams subagent seam classification", () => {
}
`;
expect(describeSeamKinds("src/agents/subagent-spawn.ts", source)).toEqual([
expect(describeSeamKinds("src/agents/subagents/spawn/subagent-spawn.ts", source)).toEqual([
"subagent-lifecycle-registry",
"subagent-session-cleanup",
"subagent-session-spawn",
@@ -112,9 +112,9 @@ describe("audit-seams subagent seam classification", () => {
it("detects parent-stream seams for ACP spawn relays", () => {
const source = `
import { onAgentEvent } from "../infra/agent-events.js";
import { requestHeartbeat } from "../infra/heartbeat-wake.js";
import { enqueueSystemEvent } from "../infra/system-events.js";
import { onAgentEvent } from "../../../infra/agent-events.js";
import { requestHeartbeat } from "../../../infra/heartbeat-wake.js";
import { enqueueSystemEvent } from "../../../infra/system-events.js";
export function startAcpSpawnParentStreamRelay() {
onAgentEvent("agent-output", () => {});
@@ -129,9 +129,9 @@ describe("audit-seams subagent seam classification", () => {
}
`;
expect(describeSeamKinds("src/agents/acp-spawn-parent-stream.ts", source)).toEqual([
"subagent-parent-stream",
]);
expect(
describeSeamKinds("src/agents/subagents/spawn/acp-spawn-parent-stream.ts", source),
).toEqual(["subagent-parent-stream"]);
});
});
@@ -153,7 +153,12 @@ describe("audit-seams status/help", () => {
expect(
determineSeamTestStatus(
["subagent-session-spawn"],
[{ file: "src/agents/subagent-spawn.workspace.test.ts", matchQuality: "direct-import" }],
[
{
file: "src/agents/subagents/spawn/subagent-spawn.workspace.test.ts",
matchQuality: "direct-import",
},
],
),
).toEqual({
status: "partial",
@@ -58,7 +58,7 @@ describe("session accessor boundary guard", () => {
"src/acp/control-plane/manager.background-task.ts",
"src/acp/control-plane/manager.core.ts",
"src/acp/runtime/session-meta.ts",
"src/agents/acp-spawn.ts",
"src/agents/subagents/spawn/acp-spawn.ts",
"src/agents/auth-profiles/session-override.ts",
"src/agents/embedded-agent-runner/compaction-successor-transcript.ts",
"src/agents/embedded-agent-runner/run/attempt.ts",