openclaw / src /agents /subagents /spawn /subagent-spawn-session-patch.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
4c76b0d verified
Raw History Blame Contribute Delete
9.74 kB
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<string, unknown>): Partial<SessionEntry> {
const entry: Partial<SessionEntry> = {};
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<string, unknown>;
}
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<Parameters<typeof buildSessionCreationStamp>[0], "actor" | "sandbox">;
completionOwnerSessionKey: string;
spawnedWorkspaceDir?: string;
spawnedCwd?: string;
sessionPermissionPolicy?: PreparedSessionPermissionPolicy;
admissionPatch?: Record<string, unknown>;
inheritedToolAllowlist?: string[];
inheritedToolDenylist?: string[];
modelPatch: Record<string, unknown>;
swarmGroupId?: string;
collect: boolean;
outputSchema?: Record<string, unknown>;
}): Promise<{ status: "ok"; entry?: SessionEntry } | { status: "error"; error: string }> {
const initialChildSessionPatch: Record<string, unknown> = {
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}` };
}
}