clawdbot-d02.1.9.1.28: add embedded run session target seam

This commit is contained in:
Josh Lehman
2026-06-04 08:46:09 -07:00
parent 4e0fde2627
commit 2abd7f1a22
19 changed files with 277 additions and 74 deletions

View File

@@ -14,7 +14,7 @@ import {
import type {
CliPreparedBackend,
PreparedCliRunContext,
RunCliAgentParams,
PreparedRunCliAgentParams,
} from "./cli-runner/types.js";
// This e2e spins a real stdio MCP server plus a spawned CLI process. Keep the
@@ -151,7 +151,7 @@ async function prepareBundleMcpExecutionContext(params: {
workspaceDir: params.workspaceDir,
config: params.config,
})) as CliPreparedBackend;
const runParams: RunCliAgentParams = {
const runParams: PreparedRunCliAgentParams = {
sessionId: params.sessionId,
sessionFile: params.sessionFile,
workspaceDir: params.workspaceDir,

View File

@@ -202,7 +202,9 @@ async function runCliAgentEndHook(
runAgentEndSideEffects(hookParams);
}
async function persistApprovedCliUserTurnTranscript(params: RunCliAgentParams): Promise<void> {
async function persistApprovedCliUserTurnTranscript(
params: RunCliAgentParamsWithSessionFile,
): Promise<void> {
if (params.suppressNextUserMessagePersistence === true || !params.userTurnTranscriptRecorder) {
return;
}

View File

@@ -1,6 +1,6 @@
import type { CompactEmbeddedAgentSessionParams } from "./compact.types.js";
import type { CompactEmbeddedAgentSessionRuntimeParams } from "./compact.types.js";
import type { EmbeddedAgentCompactResult } from "./types.js";
export type CompactEmbeddedAgentSessionDirect = (
params: CompactEmbeddedAgentSessionParams,
params: CompactEmbeddedAgentSessionRuntimeParams,
) => Promise<EmbeddedAgentCompactResult>;

View File

@@ -95,6 +95,10 @@ import { ensureOpenClawModelsJson } from "../models-config.js";
import { wrapStreamFnTextTransforms } from "../plugin-text-transforms.js";
import { resolveAgentPromptSurfaceForSessionKey } from "../prompt-surface.js";
import { registerProviderStreamForModel } from "../provider-stream.js";
import {
applyAgentRunSessionTargetIdentity,
resolveAgentRunSessionTarget,
} from "../run-session-target.js";
import { collectRuntimeChannelCapabilities } from "../runtime-capabilities.js";
import { buildAgentRuntimePlan } from "../runtime-plan/build.js";
import type { AgentRuntimePlan } from "../runtime-plan/types.js";
@@ -123,6 +127,7 @@ import {
} from "./compact-reasons.js";
import type {
CompactEmbeddedAgentSessionParams,
CompactEmbeddedAgentSessionRuntimeParams,
CompactionMessageMetrics,
} from "./compact.types.js";
import { dedupeDuplicateUserMessagesForCompaction } from "./compaction-duplicate-user-messages.js";
@@ -175,6 +180,10 @@ import { mapThinkingLevel, normalizeContextTokenBudget } from "./utils.js";
import { flushPendingToolResultsAfterIdle } from "./wait-for-idle-before-flush.js";
export type { CompactEmbeddedAgentSessionParams } from "./compact.types.js";
type CompactEmbeddedAgentSessionParamsWithSessionFile = CompactEmbeddedAgentSessionRuntimeParams & {
sessionFile: string;
};
function hasRealConversationContent(
msg: AgentMessage,
messages: AgentMessage[],
@@ -417,8 +426,17 @@ function fallbackFailureToCompactionResult(err: unknown): EmbeddedAgentCompactRe
* Use this when already inside a session/global lane to avoid deadlocks.
*/
export async function compactEmbeddedAgentSessionDirect(
params: CompactEmbeddedAgentSessionParams,
paramsInput: CompactEmbeddedAgentSessionRuntimeParams,
): Promise<EmbeddedAgentCompactResult> {
const paramsBase = applyAgentRunSessionTargetIdentity(paramsInput);
const runSessionTarget = await resolveAgentRunSessionTarget(paramsBase);
const params: CompactEmbeddedAgentSessionParamsWithSessionFile = {
...paramsBase,
agentId: paramsBase.agentId ?? runSessionTarget.agentId,
sessionId: runSessionTarget.sessionId,
sessionKey: paramsBase.sessionKey ?? runSessionTarget.sessionKey,
sessionFile: runSessionTarget.sessionFile,
};
if (hasExplicitCompactionModel(params) || !hasCompactionModelFallbackCandidates(params)) {
return await compactEmbeddedAgentSessionDirectOnce(params);
}
@@ -483,7 +501,7 @@ export async function compactEmbeddedAgentSessionDirect(
}
async function compactEmbeddedAgentSessionDirectOnce(
params: CompactEmbeddedAgentSessionParams,
params: CompactEmbeddedAgentSessionParamsWithSessionFile,
): Promise<EmbeddedAgentCompactResult> {
const startedAt = Date.now();
const diagId = params.diagId?.trim() || createCompactionDiagId();

View File

@@ -5,6 +5,7 @@ import type { ContextEngine, ContextEngineRuntimeContext } from "../../context-e
import type { CommandQueueEnqueueFn } from "../../process/command-queue.types.js";
import type { SkillSnapshot } from "../../skills/types.js";
import type { ExecElevatedDefaults, ExecToolDefaults } from "../bash-tools.exec-types.js";
import type { AgentRunSessionTarget } from "../run-session-target.js";
import type { AgentRuntimePlan } from "../runtime-plan/types.js";
export type CompactEmbeddedAgentSessionParams = {
@@ -35,6 +36,9 @@ export type CompactEmbeddedAgentSessionParams = {
groupSpace?: string | null;
/** Parent session key for subagent policy inheritance. */
spawnedBy?: string | null;
/** Storage-neutral transcript/session target. Defaults to sessionId/sessionKey/agentId. */
sessionTarget?: AgentRunSessionTarget;
/** Active file-backed artifact for current compaction internals. */
sessionFile: string;
/** Optional caller-observed live prompt tokens used for compaction diagnostics. */
currentTokenCount?: number;
@@ -97,6 +101,14 @@ export type CompactEmbeddedAgentSessionParams = {
allowGatewaySubagentBinding?: boolean;
};
export type CompactEmbeddedAgentSessionRuntimeParams = Omit<
CompactEmbeddedAgentSessionParams,
"sessionFile"
> & {
/** @deprecated Use sessionTarget plus sessionId/sessionKey/agentId for runtime identity. */
sessionFile?: string;
};
export type CompactionMessageMetrics = {
messages: number;
historyTextChars: number;

View File

@@ -0,0 +1,60 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { loadSessionStore } from "../config/sessions/store.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveAgentRunSessionTarget } from "./run-session-target.js";
describe("agent run session target", () => {
let tempDir: string;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-run-session-target-"));
});
afterEach(() => {
fs.rmSync(tempDir, { recursive: true, force: true });
});
it("resolves runtime identity through the run config store", async () => {
const storePath = path.join(tempDir, "custom-sessions", "sessions.json");
const sessionKey = "agent:helper:commitments:test-run";
const target = await resolveAgentRunSessionTarget({
agentId: "helper",
config: { session: { store: storePath } } as OpenClawConfig,
sessionId: "test-run",
sessionKey,
});
expect(target).toMatchObject({
agentId: "helper",
sessionId: "test-run",
sessionKey,
targetKind: "runtime-session",
});
expect(path.dirname(target.sessionFile)).toBe(path.dirname(storePath));
expect(loadSessionStore(storePath, { skipCache: true })[sessionKey]?.sessionFile).toBe(
target.sessionFile,
);
});
it("uses the agent from an agent-scoped session key when agentId is omitted", async () => {
const storeRoot = path.join(tempDir, "agents", "{agentId}", "sessions.json");
const sessionKey = "agent:helper:main";
const target = await resolveAgentRunSessionTarget({
config: { session: { store: storeRoot } } as OpenClawConfig,
sessionId: "helper-session",
sessionKey,
});
const helperStorePath = path.join(tempDir, "agents", "helper", "sessions.json");
expect(target.agentId).toBe("helper");
expect(path.dirname(target.sessionFile)).toBe(path.dirname(helperStorePath));
expect(loadSessionStore(helperStorePath, { skipCache: true })[sessionKey]?.sessionFile).toBe(
target.sessionFile,
);
});
});

View File

@@ -1,9 +1,11 @@
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { resolveStorePath } from "../config/sessions/paths.js";
import {
resolveSessionTranscriptRuntimeTarget,
type SessionTranscriptRuntimeTarget,
} from "../config/sessions/session-accessor.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveAgentIdFromSessionKey } from "../routing/session-key.js";
/** Identifies a run transcript target without naming the current storage artifact. */
export type AgentRunSessionTarget = {
@@ -30,10 +32,11 @@ export async function resolveAgentRunSessionTarget(params: {
const agentId = normalizeOptionalString(sessionTarget?.agentId) ?? params.agentId;
const sessionId = normalizeOptionalString(sessionTarget?.sessionId) ?? params.sessionId;
const sessionKey = normalizeOptionalString(sessionTarget?.sessionKey) ?? params.sessionKey;
const effectiveAgentId = agentId ?? resolveAgentIdFromSessionKey(sessionKey);
const sessionFile = normalizeOptionalString(params.sessionFile);
if (sessionFile) {
return {
agentId: agentId ?? "",
agentId: effectiveAgentId ?? "",
sessionFile,
sessionId,
sessionKey: sessionKey ?? "",
@@ -43,11 +46,14 @@ export async function resolveAgentRunSessionTarget(params: {
if (!sessionKey) {
throw new Error(`Cannot resolve run session target without a session key: ${sessionId}`);
}
const storePath =
normalizeOptionalString(sessionTarget?.storePath) ??
resolveStorePath(params.config?.session?.store, { agentId: effectiveAgentId });
return await resolveSessionTranscriptRuntimeTarget({
...(agentId ? { agentId } : {}),
...(effectiveAgentId ? { agentId: effectiveAgentId } : {}),
sessionId,
sessionKey,
...(sessionTarget?.storePath ? { storePath: sessionTarget.storePath } : {}),
storePath,
...(sessionTarget?.threadId !== undefined ? { threadId: sessionTarget.threadId } : {}),
});
}

View File

@@ -1,4 +1,7 @@
import path from "node:path";
import { resolveStorePath } from "../../config/sessions/paths.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { createLazyImportLoader } from "../../shared/lazy-promise.js";
/**
@@ -23,6 +26,20 @@ export type ParentForkDecision =
message: string;
};
type ParentForkDecisionParams = {
parentEntry: SessionEntry;
agentId?: string;
config?: OpenClawConfig;
storePath?: string;
};
type ForkSessionFromParentParams = {
parentEntry: SessionEntry;
agentId: string;
config?: OpenClawConfig;
sessionsDir?: string;
};
function loadSessionForkRuntime(): Promise<typeof import("./session-fork.runtime.js")> {
return sessionForkRuntimeLoader.load();
}
@@ -37,14 +54,31 @@ function formatParentForkTooLargeMessage(params: {
);
}
export async function resolveParentForkDecision(params: {
parentEntry: SessionEntry;
storePath: string;
}): Promise<ParentForkDecision> {
function resolveParentForkStorePath(params: {
agentId?: string;
config?: OpenClawConfig;
storePath?: string;
}): string {
return (
params.storePath ?? resolveStorePath(params.config?.session?.store, { agentId: params.agentId })
);
}
function resolveParentForkSessionsDir(params: {
agentId: string;
config?: OpenClawConfig;
sessionsDir?: string;
}): string {
return params.sessionsDir ?? path.dirname(resolveParentForkStorePath(params));
}
export async function resolveParentForkDecision(
params: ParentForkDecisionParams,
): Promise<ParentForkDecision> {
const maxTokens = DEFAULT_PARENT_FORK_MAX_TOKENS;
const parentTokens = await resolveParentForkTokenCount({
parentEntry: params.parentEntry,
storePath: params.storePath,
storePath: resolveParentForkStorePath(params),
});
if (typeof parentTokens === "number" && parentTokens > maxTokens) {
return {
@@ -62,13 +96,14 @@ export async function resolveParentForkDecision(params: {
};
}
export async function forkSessionFromParent(params: {
parentEntry: SessionEntry;
agentId: string;
sessionsDir: string;
}): Promise<{ sessionId: string; sessionFile: string } | null> {
export async function forkSessionFromParent(
params: ForkSessionFromParentParams,
): Promise<{ sessionId: string; sessionFile: string } | null> {
const runtime = await loadSessionForkRuntime();
return runtime.forkSessionFromParentRuntime(params);
return runtime.forkSessionFromParentRuntime({
...params,
sessionsDir: resolveParentForkSessionsDir(params),
});
}
async function resolveParentForkTokenCount(params: {

View File

@@ -25,6 +25,7 @@ vi.mock("./model-selection.runtime.js", () => ({
}));
function requireFirstEmbeddedAgentRequest(): {
config?: OpenClawConfig;
provider?: string;
model?: string;
disableTools?: boolean;
@@ -224,6 +225,9 @@ describe("commitment extraction runtime", () => {
expect(request.provider).toBe("openai");
expect(request.model).toBe("gpt-5.5");
expect(request.disableTools).toBe(true);
expect(request.config?.session?.store).toMatch(
/commitments\/extractor-sessions\/main\/sessions\.json$/,
);
});
it("backs off hidden extraction after terminal model or auth failures", async () => {

View File

@@ -191,16 +191,6 @@ function openTerminalFailureCooldown(
});
}
function resolveExtractionSessionFile(agentId: string, runId: string): string {
return path.join(
resolveStateDir(),
"commitments",
"extractor-sessions",
agentId,
`${runId}.jsonl`,
);
}
function joinPayloadText(result: EmbeddedAgentPayloadResult): string {
return (
result.payloads
@@ -211,6 +201,16 @@ function joinPayloadText(result: EmbeddedAgentPayloadResult): string {
);
}
function resolveExtractionSessionStore(agentId: string): string {
return path.join(
resolveStateDir(),
"commitments",
"extractor-sessions",
agentId,
"sessions.json",
);
}
async function resolveDefaultModel(params: {
cfg: OpenClawConfig;
agentId?: string;
@@ -234,15 +234,21 @@ async function defaultExtractBatch(params: {
const resolved = resolveCommitmentsConfig(cfg);
const runId = `commitments-${randomUUID()}`;
const modelRef = await resolveDefaultModel({ cfg, agentId: first.agentId });
const helperConfig = {
...cfg,
session: {
...cfg.session,
store: resolveExtractionSessionStore(first.agentId),
},
} as OpenClawConfig;
const { runEmbeddedAgent } = await import("../agents/embedded-agent.js");
const result = await runEmbeddedAgent({
sessionId: runId,
sessionKey: `agent:${first.agentId}:commitments:${runId}`,
agentId: first.agentId,
trigger: "manual",
sessionFile: resolveExtractionSessionFile(first.agentId, runId),
workspaceDir: resolveAgentWorkspaceDir(cfg, first.agentId),
config: cfg,
config: helperConfig,
provider: modelRef.provider,
model: modelRef.model,
prompt: buildCommitmentExtractionPrompt({ cfg, items: params.items }),

View File

@@ -14,6 +14,7 @@ import {
import type { DeliveryContext } from "../../utils/delivery-context.types.js";
import { getFileStatSnapshot } from "../cache-utils.js";
import { getRuntimeConfig } from "../io.js";
import type { OpenClawConfig } from "../types.openclaw.js";
import { enforceSessionDiskBudget, type SessionDiskBudgetSweepResult } from "./disk-budget.js";
import { deriveSessionMetaPatch } from "./metadata.js";
import { resolveStorePath } from "./paths.js";
@@ -184,6 +185,7 @@ type SingleEntryPersistencePatch = {
type SessionEntryWorkflowOptions = {
agentId?: string;
config?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
hydrateSkillPromptRefs?: boolean;
storePath?: string;
@@ -200,7 +202,8 @@ function resolveSessionWorkflowStorePath(
return options.storePath;
}
const agentId = options.agentId ?? resolveAgentIdFromSessionKey(options.sessionKey);
return resolveStorePath(getRuntimeConfig().session?.store, {
const storeConfig = options.config?.session?.store ?? getRuntimeConfig().session?.store;
return resolveStorePath(storeConfig, {
agentId,
env: options.env,
});

View File

@@ -447,6 +447,27 @@ describe("Engine contract tests", () => {
});
});
it("delegateCompactionToRuntime uses runtime session identity when available", async () => {
const compactRuntimeSpy = installCompactRuntimeSpy();
await delegateCompactionToRuntime({
sessionId: "s3",
sessionFile: "/tmp/session.json",
runtimeContext: {
agentId: "main",
sessionKey: "agent:main:main",
sessionFile: "/tmp/runtime-context-session.json",
workspaceDir: "/tmp/workspace",
},
});
expect(compactRuntimeSpy).toHaveBeenCalledTimes(1);
const compactRuntimeParams = requireCompactRuntimeParams(0);
expect(compactRuntimeParams.agentId).toBe("main");
expect(compactRuntimeParams.sessionKey).toBe("agent:main:main");
expect(compactRuntimeParams.sessionFile).toBeUndefined();
});
it("builds a normalized memory system prompt addition from the active memory prompt path", () => {
registerMemoryPromptSection(({ citationsMode }) => [
"## Memory Recall",

View File

@@ -41,8 +41,9 @@ export async function delegateCompactionToRuntime(
// runtimeContext carries the full CompactEmbeddedAgentSessionParams fields set
// by runtime callers. We spread them and override the fields that come from
// the public ContextEngine compact() signature directly.
const runtimeContext = (params.runtimeContext ?? {}) as ContextEngineRuntimeContext &
const runtimeContextWithFile = (params.runtimeContext ?? {}) as ContextEngineRuntimeContext &
Partial<RuntimeCompactionParams>;
const { sessionFile: _runtimeSessionFile, ...runtimeContext } = runtimeContextWithFile;
const currentTokenCount =
params.currentTokenCount ??
(typeof runtimeContext.currentTokenCount === "number" &&
@@ -50,11 +51,21 @@ export async function delegateCompactionToRuntime(
runtimeContext.currentTokenCount > 0
? Math.floor(runtimeContext.currentTokenCount)
: undefined);
const runtimeSessionKey =
typeof runtimeContext.sessionKey === "string" && runtimeContext.sessionKey.trim()
? runtimeContext.sessionKey.trim()
: undefined;
const sessionKey = params.sessionKey ?? runtimeSessionKey;
const agentId =
typeof runtimeContext.agentId === "string" && runtimeContext.agentId.trim()
? runtimeContext.agentId.trim()
: undefined;
const result = await compactEmbeddedAgentSessionDirect({
...runtimeContext,
sessionId: params.sessionId,
sessionFile: params.sessionFile,
...(sessionKey ? { sessionKey } : { sessionFile: params.sessionFile }),
...(agentId ? { agentId } : {}),
tokenBudget: params.tokenBudget,
...(currentTokenCount !== undefined ? { currentTokenCount } : {}),
force: params.force,

View File

@@ -149,8 +149,10 @@ describe("Crestodian assistant", () => {
expect(firstCliCall.model).toBe("claude-opus-4-8");
expect(firstCliCall.cleanupCliLiveSessionOnRunEnd).toBe(true);
const firstCliConfig = requireRecord(firstCliCall.config);
const firstCliSession = requireRecord(firstCliConfig.session);
const firstCliAgents = requireRecord(firstCliConfig.agents);
const firstCliDefaults = requireRecord(firstCliAgents.defaults);
expect(firstCliSession.store).toBe("/tmp/crestodian-planner/sessions.json");
expect(firstCliDefaults.cliBackends).toBeUndefined();
expect(firstCliCall.extraSystemPrompt).toBeTypeOf("string");
expect(firstCliCall.extraSystemPrompt).toContain("Do not use tools, shell commands");
@@ -225,12 +227,14 @@ describe("Crestodian assistant", () => {
expect(firstEmbeddedCall.disableTools).toBe(true);
expect(firstEmbeddedCall.toolsAllow).toEqual([]);
const embeddedConfig = requireRecord(firstEmbeddedCall.config);
const embeddedSession = requireRecord(embeddedConfig.session);
const embeddedAgents = requireRecord(embeddedConfig.agents);
const embeddedDefaults = requireRecord(embeddedAgents.defaults);
const embeddedModel = requireRecord(embeddedDefaults.model);
const embeddedPlugins = requireRecord(embeddedConfig.plugins);
const embeddedEntries = requireRecord(embeddedPlugins.entries);
const embeddedCodexEntry = requireRecord(embeddedEntries.codex);
expect(embeddedSession.store).toBe("/tmp/crestodian-planner/sessions.json");
expect(embeddedModel.primary).toBe("openai/gpt-5.5");
expect(embeddedCodexEntry.enabled).toBe(true);
});

View File

@@ -181,9 +181,16 @@ async function runLocalRuntimePlanner(
const tempDir = await (params.deps?.createTempDir ?? createTempPlannerDir)();
try {
const runId = `crestodian-planner-${randomUUID()}`;
const sessionFile = path.join(tempDir, "session.jsonl");
const sessionId = `${runId}-session`;
const sessionKey = `temp:crestodian-planner:${runId}`;
const backendConfig = backend.buildConfig(tempDir);
const helperConfig = {
...backendConfig,
session: {
...backendConfig.session,
store: path.join(tempDir, "sessions.json"),
},
};
switch (backend.runner) {
case "cli": {
const runCli = params.deps?.runCliAgent ?? (await loadRunCliAgent());
@@ -192,9 +199,8 @@ async function runLocalRuntimePlanner(
sessionKey,
agentId: "crestodian",
trigger: "manual",
sessionFile,
workspaceDir: tempDir,
config: backend.buildConfig(tempDir),
config: helperConfig,
prompt: params.prompt,
provider: backend.provider,
model: backend.model,
@@ -215,9 +221,8 @@ async function runLocalRuntimePlanner(
sessionKey,
agentId: "crestodian",
trigger: "manual",
sessionFile,
workspaceDir: tempDir,
config: backend.buildConfig(tempDir),
config: helperConfig,
prompt: params.prompt,
provider: backend.provider,
model: backend.model,

View File

@@ -52,6 +52,10 @@ describe("generateSlugViaLLM", () => {
const options = requireFirstRunOptions();
expect(options.timeoutMs).toBe(15_000);
expect(options.cleanupBundleMcpOnRunEnd).toBe(true);
expect(options.sessionKey).toBe("temp:slug-generator");
expect(options.sessionId).toMatch(/^slug-generator-/);
expect(options.sessionFile).toBeUndefined();
expect((options.config as OpenClawConfig).session?.store).toContain("openclaw-slug-");
});
it("marks the run lane-local so internal-helper failures do not poison shared profile health (#71709)", async () => {

View File

@@ -2,6 +2,7 @@
* LLM-based slug generator for session memory filenames
*/
import { randomUUID } from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -35,16 +36,20 @@ export async function generateSlugViaLLM(params: {
sessionContent: string;
cfg: OpenClawConfig;
}): Promise<string | null> {
let tempSessionFile: string | null = null;
let tempDir: string | undefined;
try {
const agentId = resolveDefaultAgentId(params.cfg);
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, agentId);
const agentDir = resolveAgentDir(params.cfg, agentId);
// Create a temporary session file for this one-off LLM call
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-slug-"));
tempSessionFile = path.join(tempDir, "session.jsonl");
const sessionId = `slug-generator-${randomUUID()}`;
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-slug-"));
const helperConfig = {
...params.cfg,
session: {
...params.cfg.session,
store: path.join(tempDir, "sessions.json"),
},
} as OpenClawConfig;
const prompt = `Based on this conversation, generate a short 1-2 word filename slug (lowercase, hyphen-separated, no file extension).
@@ -60,13 +65,12 @@ Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design",
const timeoutMs = resolveSlugGeneratorTimeoutMs(params.cfg);
const result = await runEmbeddedAgent({
sessionId: `slug-generator-${Date.now()}`,
sessionId,
sessionKey: "temp:slug-generator",
agentId,
sessionFile: tempSessionFile,
workspaceDir,
agentDir,
config: params.cfg,
config: helperConfig,
prompt,
provider,
model,
@@ -99,12 +103,11 @@ Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design",
log.error(`Failed to generate slug: ${message}`);
return null;
} finally {
// Clean up temporary session file
if (tempSessionFile) {
if (tempDir) {
try {
await fs.rm(path.dirname(tempSessionFile), { recursive: true, force: true });
await fs.rm(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
// Ignore cleanup errors for one-off helper storage.
}
}
}

View File

@@ -294,13 +294,21 @@ describe("realtime voice agent consult runtime", () => {
expect(resolveParentForkDecision).toHaveBeenCalledWith({
parentEntry: sessionStore["agent:main:main"],
storePath: "/tmp/sessions.json",
agentId: "main",
config: {},
});
expect(forkSessionFromParent).toHaveBeenCalledWith({
parentEntry: sessionStore["agent:main:main"],
agentId: "main",
sessionsDir: "/tmp",
config: {},
});
expect(runtime.session.patchSessionEntry).toHaveBeenCalledWith(
expect.objectContaining({
agentId: "main",
config: {},
sessionKey: "agent:main:subagent:google-meet:meet-1",
}),
);
const forkedEntry = sessionStore["agent:main:subagent:google-meet:meet-1"];
if (!forkedEntry) {
throw new Error("Expected forked consult session entry");
@@ -315,7 +323,7 @@ describe("realtime voice agent consult runtime", () => {
expectPositiveTimestamp(forkedEntry.updatedAt);
const call = requireEmbeddedAgentCall(runEmbeddedAgent);
expect(call.sessionId).toBe("forked-session");
expect(call.sessionFile).toBe("/tmp/forked.jsonl");
expect(call.sessionFile).toBeUndefined();
expect(call.spawnedBy).toBe("agent:main:main");
});

View File

@@ -1,5 +1,4 @@
import { randomUUID } from "node:crypto";
import path from "node:path";
import type { RunEmbeddedAgentParams } from "../agents/embedded-agent-runner/run/params.js";
import {
forkSessionFromParent,
@@ -81,7 +80,8 @@ function resolveDeliverySessionFields(context?: DeliveryContext): Partial<Sessio
function resolveRealtimeVoiceAgentDeliveryContext(params: {
agentRuntime: RealtimeVoiceAgentConsultRuntime;
storePath: string;
agentId: string;
cfg: OpenClawConfig;
sessionKey: string;
spawnedBy?: string | null;
}): DeliveryContext | undefined {
@@ -96,8 +96,10 @@ function resolveRealtimeVoiceAgentDeliveryContext(params: {
}
candidates.push(params.sessionKey);
for (const key of candidates) {
const parsed = parseAgentSessionKey(key);
const entry = params.agentRuntime.session.getSessionEntry({
storePath: params.storePath,
agentId: parsed?.agentId ?? params.agentId,
config: params.cfg,
sessionKey: key,
});
const context = deliveryContextFromSession(entry);
@@ -113,11 +115,11 @@ function resolveRealtimeVoiceAgentDeliveryContext(params: {
async function resolveRealtimeVoiceAgentConsultSessionEntry(params: {
agentId: string;
cfg: OpenClawConfig;
sessionKey: string;
spawnedBy?: string | null;
contextMode?: RealtimeVoiceAgentConsultContextMode;
deliveryContext?: DeliveryContext;
storePath: string;
agentRuntime: RealtimeVoiceAgentConsultRuntime;
logger: Pick<RuntimeLogger, "warn">;
}): Promise<SessionEntry> {
@@ -132,7 +134,8 @@ async function resolveRealtimeVoiceAgentConsultSessionEntry(params: {
let forkDecisionWarning: string | undefined;
const patched = await params.agentRuntime.session.patchSessionEntry({
storePath: params.storePath,
agentId: params.agentId,
config: params.cfg,
sessionKey: params.sessionKey,
fallbackEntry: {
sessionId: "",
@@ -144,24 +147,28 @@ async function resolveRealtimeVoiceAgentConsultSessionEntry(params: {
}
if (shouldFork) {
const parentEntry = params.agentRuntime.session.getSessionEntry({
storePath: params.storePath,
agentId: requesterAgentId ?? params.agentId,
config: params.cfg,
sessionKey: requesterSessionKey,
});
if (parentEntry?.sessionId?.trim()) {
const decision = await realtimeVoiceAgentConsultDeps.resolveParentForkDecision({
parentEntry,
storePath: params.storePath,
agentId: params.agentId,
config: params.cfg,
});
if (decision.status === "fork") {
const fork = await realtimeVoiceAgentConsultDeps.forkSessionFromParent({
parentEntry,
agentId: params.agentId,
sessionsDir: path.dirname(params.storePath),
config: params.cfg,
});
if (fork) {
return {
...deliveryFields,
sessionId: fork.sessionId,
// Current fork storage is file-backed; persist the artifact on
// the entry so the run target resolver reuses the forked branch.
sessionFile: fork.sessionFile,
spawnedBy: requesterSessionKey,
forkedFromParent: true,
@@ -221,22 +228,20 @@ export async function consultRealtimeVoiceAgent(params: {
const workspaceDir = params.agentRuntime.resolveAgentWorkspaceDir(params.cfg, agentId);
await params.agentRuntime.ensureAgentWorkspace({ dir: workspaceDir });
const storePath = params.agentRuntime.session.resolveStorePath(params.cfg.session?.store, {
agentId,
});
const resolvedDeliveryContext = resolveRealtimeVoiceAgentDeliveryContext({
agentRuntime: params.agentRuntime,
storePath,
agentId,
cfg: params.cfg,
sessionKey: params.sessionKey,
spawnedBy: params.spawnedBy,
});
const sessionEntry = await resolveRealtimeVoiceAgentConsultSessionEntry({
agentId,
cfg: params.cfg,
sessionKey: params.sessionKey,
spawnedBy: params.spawnedBy,
contextMode: params.contextMode,
deliveryContext: resolvedDeliveryContext,
storePath,
agentRuntime: params.agentRuntime,
logger: params.logger,
});
@@ -244,9 +249,6 @@ export async function consultRealtimeVoiceAgent(params: {
resolvedDeliveryContext ?? deliveryContextFromSession(sessionEntry);
const sessionId = sessionEntry.sessionId;
const sessionFile = params.agentRuntime.session.resolveSessionFilePath(sessionId, sessionEntry, {
agentId,
});
const result = await params.agentRuntime.runEmbeddedAgent({
sessionId,
sessionKey: params.sessionKey,
@@ -262,7 +264,6 @@ export async function consultRealtimeVoiceAgent(params: {
consultDeliveryContext?.threadId != null
? String(consultDeliveryContext.threadId)
: undefined,
sessionFile,
workspaceDir,
config: params.cfg,
prompt: buildRealtimeVoiceAgentConsultPrompt({