mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
clawdbot-d02.1.9.1.28: add embedded run session target seam
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
60
src/agents/run-session-target.test.ts
Normal file
60
src/agents/run-session-target.test.ts
Normal 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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 } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user