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 { resolveUserPath } from "../../../utils.js"; import { inheritedToolAllowPatch, inheritedToolDenyPatch, normalizeInheritedToolAllowlist, normalizeInheritedToolDenylist, } from "../../inherited-tool-deny.js"; import type { PreparedSessionPermissionPolicy } from "../../tool-fs-policy.types.js"; import { getSubagentSpawnDeps } from "./subagent-spawn-deps.js"; import { splitModelRef } from "./subagent-spawn-plan.js"; import { loadSessionEntry, resolveGatewaySessionStoreTarget, upsertSessionEntryCore, } from "./subagent-spawn.runtime.js"; function buildDirectChildSessionPatch(patch: Record): Partial { const entry: Partial = {}; const spawnDepth = patch.spawnDepth; if (typeof spawnDepth === "number" && Number.isFinite(spawnDepth) && spawnDepth >= 0) { entry.spawnDepth = Math.floor(spawnDepth); } if (patch.subagentRole === "orchestrator" || patch.subagentRole === "leaf") { entry.subagentRole = patch.subagentRole; } if (patch.subagentControlScope === "children" || patch.subagentControlScope === "none") { entry.subagentControlScope = patch.subagentControlScope; } if (patch.inheritedToolPolicyVersion === 1) { entry.inheritedToolPolicyVersion = 1; } if (patch.incognito === true) { entry.incognito = true; } if (typeof patch.spawnedBy === "string" && patch.spawnedBy.trim()) { entry.spawnedBy = patch.spawnedBy.trim(); } if ( typeof patch.completionOwnerSessionKey === "string" && patch.completionOwnerSessionKey.trim() ) { entry.completionOwnerSessionKey = patch.completionOwnerSessionKey.trim(); } if (typeof patch.parentSessionKey === "string" && patch.parentSessionKey.trim()) { entry.parentSessionKey = patch.parentSessionKey.trim(); } if (typeof patch.spawnedWorkspaceDir === "string" && patch.spawnedWorkspaceDir.trim()) { entry.spawnedWorkspaceDir = patch.spawnedWorkspaceDir.trim(); } if (typeof patch.spawnedCwd === "string" && patch.spawnedCwd.trim()) { entry.spawnedCwd = patch.spawnedCwd.trim(); } const inheritedToolDeny = normalizeInheritedToolDenylist(patch.inheritedToolDeny); if (inheritedToolDeny.length > 0) { entry.inheritedToolDeny = inheritedToolDeny; } const inheritedToolAllow = normalizeInheritedToolAllowlist(patch.inheritedToolAllow); if (inheritedToolAllow.length > 0) { entry.inheritedToolAllow = inheritedToolAllow; } if (typeof patch.thinkingLevel === "string" && patch.thinkingLevel.trim()) { entry.thinkingLevel = patch.thinkingLevel.trim(); } const authProfileOverride = normalizeOptionalString(patch.authProfileOverride); if (authProfileOverride) { entry.authProfileOverride = authProfileOverride; entry.authProfileOverrideSource = patch.authProfileOverrideSource === "auto" ? "auto" : "user"; } if (patch.fastMode === true || patch.fastMode === false || patch.fastMode === "auto") { entry.fastMode = patch.fastMode; } if (typeof patch.swarmGroupId === "string" && patch.swarmGroupId.trim()) { entry.swarmGroupId = patch.swarmGroupId.trim(); } if (patch.swarmCollector === true) { entry.swarmCollector = true; } if (patch.swarmOutputSchema && typeof patch.swarmOutputSchema === "object") { entry.swarmOutputSchema = patch.swarmOutputSchema as Record; } if (typeof patch.model === "string" && patch.model.trim()) { const { provider, model } = splitModelRef(patch.model.trim()); if (model) { entry.model = model; entry.modelOverride = model; entry.modelOverrideSource = patch.modelOverrideSource === "auto" ? "auto" : "user"; entry.modelOverrideRouteResolution = "resolved"; const fallbackOriginProvider = normalizeOptionalString( patch.modelOverrideFallbackOriginProvider, ); const fallbackOriginModel = normalizeOptionalString(patch.modelOverrideFallbackOriginModel); if (fallbackOriginProvider && fallbackOriginModel) { entry.modelOverrideFallbackOriginProvider = fallbackOriginProvider; entry.modelOverrideFallbackOriginModel = fallbackOriginModel; } if (provider) { entry.modelProvider = provider; entry.providerOverride = provider; } } } return entry; } export function loadSubagentConfig() { return getSubagentSpawnDeps().getRuntimeConfig(); } export async function createInitialSubagentSession(params: { cfg: OpenClawConfig; targetAgentId: string; childSessionKey: string; label?: string; incognito: boolean; requesterInternalKey: string; assertActive?: () => void; creationPolicy: Pick[0], "actor" | "sandbox">; completionOwnerSessionKey: string; spawnedWorkspaceDir?: string; spawnedCwd?: string; sessionPermissionPolicy?: PreparedSessionPermissionPolicy; admissionPatch?: Record; inheritedToolAllowlist?: string[]; inheritedToolDenylist?: string[]; modelPatch: Record; swarmGroupId?: string; collect: boolean; outputSchema?: Record; }): Promise<{ status: "ok"; entry?: SessionEntry } | { status: "error"; error: string }> { const initialChildSessionPatch: Record = { spawnedBy: params.requesterInternalKey, completionOwnerSessionKey: params.completionOwnerSessionKey, // Navigation and control lineage commit with the creation stamp so a // launch failure cannot leave a durable but parentless child row. parentSessionKey: params.requesterInternalKey, ...(params.spawnedWorkspaceDir ? { spawnedWorkspaceDir: params.spawnedWorkspaceDir } : {}), ...(params.spawnedCwd ? { spawnedCwd: params.spawnedCwd } : {}), ...params.admissionPatch, inheritedToolPolicyVersion: 1, ...inheritedToolAllowPatch(params.inheritedToolAllowlist), ...inheritedToolDenyPatch(params.inheritedToolDenylist), ...params.modelPatch, ...(params.swarmGroupId ? { swarmGroupId: params.swarmGroupId } : {}), ...(params.collect ? { swarmCollector: true } : {}), ...(params.outputSchema ? { swarmOutputSchema: params.outputSchema } : {}), ...(params.incognito ? { incognito: true } : {}), }; try { const parentTarget = resolveGatewaySessionStoreTarget({ cfg: params.cfg, key: params.requesterInternalKey, }); const parentEntry = loadSessionEntry({ storePath: parentTarget.storePath, sessionKey: parentTarget.canonicalKey, }); // Spawn owns a fresh child lifecycle. Cleanup freezes both fields before // launch so it cannot delete a reset successor that reuses the session id. const childSessionIdentity = { sessionId: randomUUID(), lifecycleRevision: randomUUID(), }; const target = params.incognito ? { agentId: params.targetAgentId, canonicalKey: params.childSessionKey, storeKeys: [params.childSessionKey], storePath: resolveIncognitoOpenClawAgentSqlitePath({ agentId: params.targetAgentId }), } : resolveGatewaySessionStoreTarget({ cfg: params.cfg, key: params.childSessionKey, }); const entry = await upsertSessionEntryCore( { storePath: target.storePath, sessionKey: target.canonicalKey, }, { ...buildDirectChildSessionPatch(initialChildSessionPatch), // Native spawn keeps agent RPC label semantics, not sessions.patch's uniqueness policy. ...(params.label ? { label: params.label } : {}), ...(params.sessionPermissionPolicy ? { permissionMode: params.sessionPermissionPolicy.mode, sessionRoot: resolveUserPath( params.spawnedWorkspaceDir ?? params.sessionPermissionPolicy.root, ), } : {}), ...childSessionIdentity, ...(parentEntry?.skillLibrarySelections ? { skillLibrarySelections: parentEntry.skillLibrarySelections.map((selection) => ({ ...selection, })), } : {}), ...buildSessionCreationStamp({ via: "spawn", ...params.creationPolicy, }), }, { assertCommitAllowed: () => { params.assertActive?.(); if (parentEntry?.skillLibrarySelections) { const latest = loadSessionEntry({ storePath: parentTarget.storePath, sessionKey: parentTarget.canonicalKey, }); if ( latest?.sessionId !== parentEntry.sessionId || latest.lifecycleRevision !== parentEntry.lifecycleRevision || JSON.stringify(latest.skillLibrarySelections) !== JSON.stringify(parentEntry.skillLibrarySelections) ) { throw new Error( "Parent skill selection changed before spawn; retry from the current turn.", ); } } }, }, ); return { status: "ok", entry: entry ?? undefined }; } catch (err) { const message = err instanceof Error ? err.message : typeof err === "string" ? err : "error"; return { status: "error", error: `child session patch failed: ${message}` }; } }