refactor: remove embedded transcript locator handoff

This commit is contained in:
Peter Steinberger
2026-05-09 09:38:41 +01:00
parent 320a4c8977
commit ff828639e8
43 changed files with 373 additions and 411 deletions

View File

@@ -47,12 +47,14 @@ This migration has one canonical runtime shape:
- Legacy `sessions.json`, transcript JSONL, `.jsonl.lock`, pruning, truncation,
and old session-path logic belong only to the doctor migration/import path.
- Runtime startup, hot reply paths, compaction, reset, recovery, diagnostics,
TTS, memory hooks, subagents, and plugin command routing must derive transcript
handles from SQLite identity or pass `{agentId, sessionId}` directly.
- `runEmbeddedPiAgent(...)` and the inner embedded attempt must not honor
file-shaped transcript locator inputs. They derive the SQLite transcript
handle from `{agentId, sessionId}` before worker dispatch or model execution,
so stale callers cannot make the runner write JSON/JSONL transcripts.
TTS, memory hooks, subagents, and plugin command routing must pass
`{agentId, sessionId}` through the runtime. Any string transcript handle is a
boundary artifact, not runtime identity.
- `runEmbeddedPiAgent(...)`, prepared worker runs, and the inner embedded
attempt must not accept transcript locators. They open the SQLite transcript
manager by `{agentId, sessionId}` and pass that manager to the internalized
PI-compatible agent session, so stale callers cannot make the runner write
JSON/JSONL transcripts.
- Runner diagnostics must store runtime/cache/payload trace records in SQLite.
Runtime diagnostics must not expose JSONL file override knobs; export/debug
commands can materialize files explicitly from database rows.
@@ -240,17 +242,17 @@ The remaining cleanup is mostly consolidation and deletion:
transcript files. Gateway chat/media/history paths read transcript rows from
SQLite; JSONL is now a legacy doctor input or in-memory export
encoding, not a runtime state file.
- Runtime transcript store APIs resolve SQLite transcript locators, not
filesystem paths. The old `resolve...ForPath` helper and unused
`transcriptPath` write options are gone from runtime callers.
- Runtime transcript store APIs resolve SQLite scope, not filesystem paths. The
old `resolve...ForPath` helper and unused `transcriptPath` write options are
gone from runtime callers.
- Runtime session resolution now uses `{agentId, sessionId}`. It may derive a
`sqlite-transcript://<agent>/<session>` handle for external boundaries, but it
must not store that derived value in active session rows. Legacy absolute
JSONL paths are doctor migration inputs only.
- `runEmbeddedPiAgent(...)` now treats caller-provided transcript locators as
ignored legacy hints. It derives the active SQLite transcript locator from the
resolved agent id and session id before worker dispatch, attempt creation, or
prepared-run serialization.
- `runEmbeddedPiAgent(...)` no longer has a transcript-locator parameter.
Prepared worker descriptors also omit transcript locators. Runtime session
persistence starts from the resolved agent id and session id, then opens the
SQLite session manager directly.
- Gateway transcript-key lookup compares derived SQLite transcript handles at
protocol boundaries and no longer realpaths or stats transcript filenames.
- Automatic compaction transcript rotation writes successor transcript rows

View File

@@ -5,7 +5,6 @@
import crypto from "node:crypto";
import { applyModelOverrideToSessionEntry } from "openclaw/plugin-sdk/model-session-runtime";
import { createSqliteSessionTranscriptLocator } from "openclaw/plugin-sdk/session-store-runtime";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
import type { SessionEntry } from "../api.js";
import { resolveVoiceCallSessionKey, type VoiceCallConfig } from "./config.js";
@@ -256,11 +255,6 @@ export async function generateVoiceResponse(
}
const sessionId = sessionEntry.sessionId;
const sessionFile = createSqliteSessionTranscriptLocator({
agentId,
sessionId,
});
// Resolve thinking level
const thinkLevel = agentRuntime.resolveThinkingDefault({ cfg, provider, model });
@@ -293,7 +287,6 @@ export async function generateVoiceResponse(
sandboxSessionKey: resolveVoiceSandboxSessionKey(agentId, resolvedSessionKey),
agentId,
messageProvider: "voice",
sessionFile,
workspaceDir,
config: cfg,
prompt: userMessage,

View File

@@ -66,7 +66,19 @@ export async function hasCompletedBootstrapTranscriptTurn(
if (!scope) {
return false;
}
const records = loadSqliteSessionTranscriptEvents(scope)
return hasCompletedBootstrapSessionTurn(scope);
}
export async function hasCompletedBootstrapSessionTurn(params: {
agentId: string;
sessionId: string;
}): Promise<boolean> {
const agentId = params.agentId.trim();
const sessionId = params.sessionId.trim();
if (!agentId || !sessionId) {
return false;
}
const records = loadSqliteSessionTranscriptEvents({ agentId, sessionId })
.map((entry) => entry.event)
.slice(-CONTINUATION_SCAN_MAX_RECORDS);
let compactedAfterLatestAssistant = false;

View File

@@ -588,7 +588,6 @@ export function runAgentAttempt(params: {
replyToMode: params.runContext.replyToMode,
hasRepliedRef: params.runContext.hasRepliedRef,
senderIsOwner: params.opts.senderIsOwner,
transcriptLocator: params.transcriptLocator,
workspaceDir: params.workspaceDir,
config: params.cfg,
agentHarnessId: requestedAgentHarnessId,

View File

@@ -13,7 +13,6 @@ const BASE_PARAMS = {
sessionKey: "session-1",
model: "gpt-5.5",
prompt: "hello",
sessionFile: "sqlite-transcript://agent-1/session-1.jsonl",
timeoutMs: 1_000,
workspaceDir: "/tmp/openclaw-workspace",
} satisfies RunEmbeddedPiAgentParams;

View File

@@ -9,7 +9,6 @@ function createPreparedRun(overrides: Partial<PreparedAgentRun> = {}): PreparedA
agentId: "main",
sessionId: "session-pi-worker",
sessionKey: "agent:main:thread",
sessionFile: "sqlite-transcript://main/session-pi-worker.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
provider: "openai",

View File

@@ -13,7 +13,6 @@ function createPreparedRun(overrides: Partial<PreparedAgentRun> = {}): PreparedA
agentId: "main",
sessionId: "session-rehydrate",
sessionKey: "agent:main:thread",
sessionFile: "sqlite-transcript://main/session-rehydrate.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
provider: "openai",
@@ -48,7 +47,6 @@ describe("createRunParamsFromPreparedAgentRun", () => {
runId: "run-rehydrate",
sessionId: "session-rehydrate",
sessionKey: "agent:main:thread",
sessionFile: "sqlite-transcript://main/session-rehydrate.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
provider: "openai",

View File

@@ -108,7 +108,6 @@ export function createRunParamsFromPreparedAgentRun(
runId: preparedRun.runId,
sessionId: preparedRun.sessionId,
...(preparedRun.sessionKey ? { sessionKey: preparedRun.sessionKey } : {}),
sessionFile: preparedRun.sessionFile,
workspaceDir: preparedRun.workspaceDir,
...(preparedRun.agentDir ? { agentDir: preparedRun.agentDir } : {}),
...(preparedRun.config ? { config: preparedRun.config } : {}),

View File

@@ -14,7 +14,6 @@ function createAttempt(
runId: "run-prepared",
sessionId: "session-prepared",
sessionKey: "agent:ops:thread",
sessionFile: "sqlite-transcript://ops/session-prepared.jsonl",
workspaceDir: "/tmp/workspace",
agentDir: "/tmp/agent",
prompt: "hello",
@@ -43,7 +42,6 @@ describe("createPreparedAgentRunFromAttempt", () => {
agentId: "ops",
sessionId: "session-prepared",
sessionKey: "agent:ops:thread",
sessionFile: "sqlite-transcript://ops/session-prepared.jsonl",
workspaceDir: "/tmp/workspace",
agentDir: "/tmp/agent",
prompt: "hello",
@@ -88,7 +86,6 @@ describe("createPreparedAgentRunFromRunParams", () => {
runId: "run-high-level",
sessionId: "session-high-level",
sessionKey: "agent:ops:thread",
sessionFile: "sqlite-transcript://ops/session-high-level.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
provider: "openai",
@@ -136,7 +133,6 @@ describe("createPreparedAgentRunFromRunParams", () => {
runId: "run-high-level",
sessionId: "session-high-level",
sessionKey: "agent:ops:thread",
sessionFile: "sqlite-transcript://ops/session-high-level.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
provider: "openai",
@@ -172,7 +168,6 @@ describe("createPreparedAgentRunFromRunParams", () => {
createPreparedAgentRunFromRunParams({
runId: "run-high-level",
sessionId: "session-high-level",
sessionFile: "sqlite-transcript://main/session-high-level.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 1000,
@@ -187,7 +182,6 @@ describe("createSerializableRunParamsSnapshot", () => {
const snapshot = createSerializableRunParamsSnapshot({
runId: "run-snapshot",
sessionId: "session-snapshot",
sessionFile: "sqlite-transcript://main/session-snapshot.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 1000,

View File

@@ -1,4 +1,3 @@
import { createSqliteSessionTranscriptLocator } from "../../config/sessions/paths.js";
import { resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
import type { RunEmbeddedPiAgentParams } from "../pi-embedded-runner/run/params.js";
import type { EmbeddedRunAttemptParams } from "../pi-embedded-runner/run/types.js";
@@ -24,7 +23,6 @@ type PreparedRunAttemptShape = Pick<
| "provider"
| "replyOperation"
| "runId"
| "transcriptLocator"
| "sessionId"
| "sessionKey"
| "shouldEmitToolOutput"
@@ -45,7 +43,6 @@ type PreparedRunParamsShape = Pick<
| "initialVfsEntries"
| "replyOperation"
| "runId"
| "transcriptLocator"
| "sessionId"
| "sessionKey"
| "shouldEmitToolOutput"
@@ -100,10 +97,6 @@ function createPreparedAgentRun(
agentId,
sessionId: source.sessionId,
...(source.sessionKey ? { sessionKey: source.sessionKey } : {}),
transcriptLocator: createSqliteSessionTranscriptLocator({
agentId,
sessionId: source.sessionId,
}),
workspaceDir: source.workspaceDir,
...(source.agentDir ? { agentDir: source.agentDir } : {}),
prompt: source.prompt,

View File

@@ -12,7 +12,6 @@ function createAttempt(
return {
sessionId: "session-worker-launch",
sessionKey: "agent:main:thread",
sessionFile: "sqlite-transcript://main/session-worker-launch.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 1000,
@@ -80,7 +79,6 @@ describe("PI run worker launch request", () => {
{
sessionId: "session-pi-run",
sessionKey: "agent:main:thread",
sessionFile: "sqlite-transcript://main/session-pi-run.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 1000,

View File

@@ -3,8 +3,6 @@ import os from "node:os";
import path from "node:path";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js";
import { resolveAgentIdFromSessionKey } from "../routing/session-key.js";
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import type { AuthProfileFailureReason } from "./auth-profiles.js";
import {
@@ -82,16 +80,6 @@ const OVERLOADED_ERROR_PAYLOAD =
const RATE_LIMIT_ERROR_MESSAGE = "rate limit exceeded";
const NO_ENDPOINTS_FOUND_ERROR_MESSAGE = "404 No endpoints found for deepseek/deepseek-r1:free.";
function createTestSessionTranscriptLocator(params: {
sessionKey: string;
sessionId: string;
}): string {
return createSqliteSessionTranscriptLocator({
agentId: resolveAgentIdFromSessionKey(params.sessionKey),
sessionId: params.sessionId,
});
}
function createTestSessionId(raw: string): string {
return raw.replace(/[^a-z0-9._-]/gi, "-").slice(0, 128);
}
@@ -276,10 +264,6 @@ async function runEmbeddedFallback(params: {
runEmbeddedPiAgent({
sessionId,
sessionKey: params.sessionKey,
sessionFile: createTestSessionTranscriptLocator({
sessionKey: params.sessionKey,
sessionId,
}),
workspaceDir: params.workspaceDir,
agentDir: params.agentDir,
config: cfg,
@@ -429,10 +413,6 @@ describe("runWithModelFallback + runEmbeddedPiAgent failover behavior", () => {
const result = await runEmbeddedPiAgent({
sessionId: "tool-side-effect-terminal",
sessionKey: "agent:test:tool-side-effect-terminal",
sessionFile: createTestSessionTranscriptLocator({
sessionKey: "agent:test:tool-side-effect-terminal",
sessionId: "tool-side-effect-terminal",
}),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -533,10 +513,6 @@ describe("runWithModelFallback + runEmbeddedPiAgent failover behavior", () => {
name: "undici-terminated",
message: "terminated",
},
{
name: "stream-read-error",
message: "stream_read_error",
},
{
name: "codex-empty-transport-response",
message: "Request failed",

View File

@@ -108,7 +108,7 @@ function buildRunnerSessionPaths(sessionId: string) {
}
return {
agentDir: liveRunnerRootDir,
sessionFile: createSqliteSessionTranscriptLocator({ agentId: "main", sessionId }),
transcriptLocator: createSqliteSessionTranscriptLocator({ agentId: "main", sessionId }),
workspaceDir: path.join(liveRunnerRootDir, `${sessionId}-workspace`),
};
}
@@ -303,7 +303,6 @@ async function runEmbeddedCacheProbe(params: {
runEmbeddedPiAgent({
sessionId: params.sessionId,
sessionKey: `live-cache:${params.providerTag}:${params.sessionId}`,
sessionFile: sessionPaths.sessionFile,
workspaceDir: sessionPaths.workspaceDir,
agentDir: sessionPaths.agentDir,
config: buildEmbeddedRunnerConfig({
@@ -347,7 +346,7 @@ async function compactLiveCacheSession(params: {
compactEmbeddedPiSessionDirect({
sessionId: params.sessionId,
sessionKey: `live-cache:${params.providerTag}:${params.sessionId}`,
sessionFile: sessionPaths.sessionFile,
transcriptLocator: sessionPaths.transcriptLocator,
workspaceDir: sessionPaths.workspaceDir,
agentDir: sessionPaths.agentDir,
config: buildEmbeddedRunnerConfig({

View File

@@ -208,20 +208,20 @@ beforeEach(() => {
});
});
const nextSessionFile = () => {
const nextTranscriptLocator = () => {
sessionCounter += 1;
return createSqliteSessionTranscriptLocator({
agentId: "test",
sessionId: `session-${sessionCounter}`,
});
};
const sessionIdFromLocator = (sessionFile: string) =>
parseSqliteSessionTranscriptLocator(sessionFile)?.sessionId ?? "session:test";
const appendTestSessionMessage = async (sessionFile: string, message: unknown) =>
const sessionIdFromLocator = (transcriptLocator: string) =>
parseSqliteSessionTranscriptLocator(transcriptLocator)?.sessionId ?? "session:test";
const appendTestSessionMessage = async (transcriptLocator: string, message: unknown) =>
await appendSessionTranscriptMessage({
agentId: "test",
sessionId: sessionIdFromLocator(sessionFile),
transcriptPath: sessionFile,
sessionId: sessionIdFromLocator(transcriptLocator),
transcriptPath: transcriptLocator,
cwd: workspaceDir,
message,
});
@@ -229,8 +229,8 @@ const nextRunId = (prefix = "run-embedded-test") => `${prefix}-${++runCounter}`;
const nextSessionKey = () => `agent:test:embedded:${nextRunId("session-key")}`;
const runWithOrphanedSingleUserMessage = async (text: string, sessionKey: string) => {
const sessionFile = nextSessionFile();
await appendTestSessionMessage(sessionFile, {
const transcriptLocator = nextTranscriptLocator();
await appendTestSessionMessage(transcriptLocator, {
role: "user",
content: [{ type: "text", text }],
timestamp: Date.now(),
@@ -247,9 +247,8 @@ const runWithOrphanedSingleUserMessage = async (text: string, sessionKey: string
const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]);
return await runEmbeddedPiAgent({
sessionId: sessionIdFromLocator(sessionFile),
sessionId: sessionIdFromLocator(transcriptLocator),
sessionKey,
sessionFile,
workspaceDir,
config: cfg,
prompt: "hello",
@@ -273,7 +272,7 @@ const textFromContent = (content: unknown) => {
};
const readSessionEntries = async (
sessionFile: string,
transcriptLocator: string,
): Promise<
Array<{
type?: string;
@@ -282,7 +281,7 @@ const readSessionEntries = async (
}>
> => {
try {
return (await readTranscriptState(sessionFile)).getEntries() as Array<{
return (await readTranscriptState(transcriptLocator)).getEntries() as Array<{
type?: string;
customType?: string;
data?: unknown;
@@ -295,8 +294,8 @@ const readSessionEntries = async (
}
};
const readSessionMessages = async (sessionFile: string) => {
const entries = await readSessionEntries(sessionFile);
const readSessionMessages = async (transcriptLocator: string) => {
const entries = await readSessionEntries(transcriptLocator);
return entries
.filter((entry) => entry.type === "message")
.map(
@@ -304,7 +303,11 @@ const readSessionMessages = async (sessionFile: string) => {
) as Array<{ role?: string; content?: unknown }>;
};
const runDefaultEmbeddedTurn = async (sessionFile: string, prompt: string, sessionKey: string) => {
const runDefaultEmbeddedTurn = async (
transcriptLocator: string,
prompt: string,
sessionKey: string,
) => {
const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-error"]);
runEmbeddedAttemptMock.mockResolvedValueOnce(
makeEmbeddedRunnerAttempt({
@@ -315,9 +318,8 @@ const runDefaultEmbeddedTurn = async (sessionFile: string, prompt: string, sessi
}),
);
await runEmbeddedPiAgent({
sessionId: sessionIdFromLocator(sessionFile),
sessionId: sessionIdFromLocator(transcriptLocator),
sessionKey,
sessionFile,
workspaceDir,
config: cfg,
prompt,
@@ -332,7 +334,7 @@ const runDefaultEmbeddedTurn = async (sessionFile: string, prompt: string, sessi
describe("runEmbeddedPiAgent", () => {
it("skips models.json generation when dynamic model resolution succeeds", async () => {
const sessionFile = nextSessionFile();
const transcriptLocator = nextTranscriptLocator();
const cfg = createEmbeddedPiRunnerOpenAiConfig([]);
runEmbeddedAttemptMock.mockResolvedValueOnce(
makeEmbeddedRunnerAttempt({
@@ -345,7 +347,6 @@ describe("runEmbeddedPiAgent", () => {
await runEmbeddedPiAgent({
sessionId: "dynamic-model",
sessionFile,
workspaceDir,
config: cfg,
prompt: "hello",
@@ -370,7 +371,7 @@ describe("runEmbeddedPiAgent", () => {
});
it("backfills a trimmed session key from sessionId when the embedded run omits it", async () => {
const sessionFile = nextSessionFile();
const transcriptLocator = nextTranscriptLocator();
const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]);
resolveSessionKeyForRequestMock.mockReturnValue({
sessionKey: "agent:test:resolved",
@@ -388,7 +389,6 @@ describe("runEmbeddedPiAgent", () => {
await runEmbeddedPiAgent({
sessionId: "resume-123",
sessionKey: " ",
sessionFile,
workspaceDir,
config: cfg,
prompt: "hello",
@@ -410,7 +410,7 @@ describe("runEmbeddedPiAgent", () => {
});
it("drops whitespace-only session keys when backfill cannot resolve a session key", async () => {
const sessionFile = nextSessionFile();
const transcriptLocator = nextTranscriptLocator();
const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]);
resolveSessionKeyForRequestMock.mockReturnValue({
sessionKey: undefined,
@@ -428,7 +428,6 @@ describe("runEmbeddedPiAgent", () => {
await runEmbeddedPiAgent({
sessionId: "resume-124",
sessionKey: " ",
sessionFile,
workspaceDir,
config: cfg,
prompt: "hello",
@@ -450,7 +449,7 @@ describe("runEmbeddedPiAgent", () => {
});
it("logs when embedded session-key backfill resolution fails", async () => {
const sessionFile = nextSessionFile();
const transcriptLocator = nextTranscriptLocator();
const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]);
resolveSessionKeyForRequestMock.mockImplementation(() => {
throw new Error("resolver exploded");
@@ -466,7 +465,6 @@ describe("runEmbeddedPiAgent", () => {
await runEmbeddedPiAgent({
sessionId: "resume-456",
sessionFile,
workspaceDir,
config: cfg,
prompt: "hello",
@@ -486,7 +484,7 @@ describe("runEmbeddedPiAgent", () => {
});
it("passes the current agentId when backfilling a session key", async () => {
const sessionFile = nextSessionFile();
const transcriptLocator = nextTranscriptLocator();
const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]);
resolveStoredSessionKeyForSessionIdMock.mockReturnValue({
sessionKey: "agent:test:resolved",
@@ -504,7 +502,6 @@ describe("runEmbeddedPiAgent", () => {
await runEmbeddedPiAgent({
sessionId: "resume-agent-1",
sessionKey: undefined,
sessionFile,
workspaceDir,
config: cfg,
prompt: "hello",
@@ -526,8 +523,8 @@ describe("runEmbeddedPiAgent", () => {
});
it("disposes bundle MCP once when a one-shot local run completes", async () => {
const sessionFile = nextSessionFile();
const sessionId = sessionIdFromLocator(sessionFile);
const transcriptLocator = nextTranscriptLocator();
const sessionId = sessionIdFromLocator(transcriptLocator);
const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]);
const sessionKey = nextSessionKey();
runEmbeddedAttemptMock.mockResolvedValueOnce(
@@ -542,7 +539,6 @@ describe("runEmbeddedPiAgent", () => {
await runEmbeddedPiAgent({
sessionId,
sessionKey,
sessionFile,
workspaceDir,
config: cfg,
prompt: "hello",
@@ -562,8 +558,8 @@ describe("runEmbeddedPiAgent", () => {
it("preserves bundle MCP state across retries within one local run", async () => {
refreshRuntimeAuthOnFirstPromptError = true;
const sessionFile = nextSessionFile();
const sessionId = sessionIdFromLocator(sessionFile);
const transcriptLocator = nextTranscriptLocator();
const sessionId = sessionIdFromLocator(transcriptLocator);
const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-1"]);
const sessionKey = nextSessionKey();
runEmbeddedAttemptMock
@@ -586,7 +582,6 @@ describe("runEmbeddedPiAgent", () => {
const result = await runEmbeddedPiAgent({
sessionId,
sessionKey,
sessionFile,
workspaceDir,
config: cfg,
prompt: "hello",
@@ -606,7 +601,7 @@ describe("runEmbeddedPiAgent", () => {
});
it("retries a planning-only GPT turn once with an act-now steer", async () => {
const sessionFile = nextSessionFile();
const transcriptLocator = nextTranscriptLocator();
const cfg = createEmbeddedPiRunnerOpenAiConfig(["gpt-5.4"]);
const sessionKey = nextSessionKey();
@@ -640,9 +635,8 @@ describe("runEmbeddedPiAgent", () => {
});
const result = await runEmbeddedPiAgent({
sessionId: sessionIdFromLocator(sessionFile),
sessionId: sessionIdFromLocator(transcriptLocator),
sessionKey,
sessionFile,
workspaceDir,
config: cfg,
prompt: "ship it",
@@ -659,7 +653,7 @@ describe("runEmbeddedPiAgent", () => {
});
it("handles prompt error paths without dropping user state", async () => {
const sessionFile = nextSessionFile();
const transcriptLocator = nextTranscriptLocator();
const cfg = createEmbeddedPiRunnerOpenAiConfig(["mock-error"]);
const sessionKey = nextSessionKey();
runEmbeddedAttemptMock.mockResolvedValueOnce(
@@ -669,9 +663,8 @@ describe("runEmbeddedPiAgent", () => {
);
await expect(
runEmbeddedPiAgent({
sessionId: sessionIdFromLocator(sessionFile),
sessionId: sessionIdFromLocator(transcriptLocator),
sessionKey,
sessionFile,
workspaceDir,
config: cfg,
prompt: "boom",
@@ -684,7 +677,7 @@ describe("runEmbeddedPiAgent", () => {
}),
).rejects.toThrow("boom");
const messages = await readSessionMessages(sessionFile);
const messages = await readSessionMessages(transcriptLocator);
if (messages.length > 0) {
const userIndex = messages.findIndex(
(message) => message?.role === "user" && textFromContent(message.content) === "boom",
@@ -697,15 +690,15 @@ describe("runEmbeddedPiAgent", () => {
"preserves existing transcript entries across an additional turn",
{ timeout: 7_000 },
async () => {
const sessionFile = nextSessionFile();
const transcriptLocator = nextTranscriptLocator();
const sessionKey = nextSessionKey();
await appendTestSessionMessage(sessionFile, {
await appendTestSessionMessage(transcriptLocator, {
role: "user",
content: [{ type: "text", text: "seed user" }],
timestamp: Date.now(),
});
await appendTestSessionMessage(sessionFile, {
await appendTestSessionMessage(transcriptLocator, {
role: "assistant",
content: [{ type: "text", text: "seed assistant" }],
stopReason: "stop",
@@ -715,9 +708,9 @@ describe("runEmbeddedPiAgent", () => {
usage: createMockUsage(1, 1),
timestamp: Date.now(),
});
await runDefaultEmbeddedTurn(sessionFile, "hello", sessionKey);
await runDefaultEmbeddedTurn(transcriptLocator, "hello", sessionKey);
const messages = await readSessionMessages(sessionFile);
const messages = await readSessionMessages(transcriptLocator);
const seedUserIndex = messages.findIndex(
(message) => message?.role === "user" && textFromContent(message.content) === "seed user",
);

View File

@@ -3,9 +3,7 @@ import os from "node:os";
import path from "node:path";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js";
import { redactIdentifier } from "../logging/redact-identifier.js";
import { resolveAgentIdFromSessionKey } from "../routing/session-key.js";
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import type { AuthProfileFailureReason } from "./auth-profiles.js";
import {
@@ -35,13 +33,6 @@ const { computeBackoffMock, sleepWithAbortMock } = vi.hoisted(() => ({
const TEST_SESSION_ID = "session-test";
function createTestSessionTranscriptLocator(sessionKey?: string): string {
return createSqliteSessionTranscriptLocator({
agentId: resolveAgentIdFromSessionKey(sessionKey),
sessionId: TEST_SESSION_ID,
});
}
const installRunEmbeddedMocks = () => {
installEmbeddedRunnerBaseE2eMocks();
installEmbeddedRunnerFastRunE2eMocks({
@@ -467,7 +458,6 @@ async function runAutoPinnedOpenAiTurn(params: {
await runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: params.sessionKey,
sessionFile: createTestSessionTranscriptLocator(params.sessionKey),
workspaceDir: params.workspaceDir,
agentDir: params.agentDir,
config: params.config ?? makeConfig(),
@@ -621,7 +611,6 @@ async function runTurnWithCooldownSeed(params: {
await runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: params.sessionKey,
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -686,7 +675,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
await runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:copilot-auth-error",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeCopilotConfig(),
@@ -771,7 +759,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
await runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:copilot-auth-repeat",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeCopilotConfig(),
@@ -819,7 +806,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
const runPromise = runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:copilot-shutdown",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeCopilotConfig(),
@@ -1024,7 +1010,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
const result = await runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:compaction-timeout",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -1063,7 +1048,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
const result = await runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:compaction-wait-abort",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -1092,7 +1076,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:user",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -1142,7 +1125,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
await runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:user-order-excluded",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -1171,7 +1153,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
await runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:user-auth-alias",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -1210,7 +1191,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
await runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:mismatch",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -1252,7 +1232,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:cooldown-failover",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeConfig({ fallbacks: ["openai/mock-2"] }),
@@ -1296,7 +1275,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
const result = await runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:cooldown-probe",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeConfig({ fallbacks: ["openai/mock-2"] }),
@@ -1344,7 +1322,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
const result = await runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:overloaded-cooldown-probe",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeConfig({ fallbacks: ["openai/mock-2"] }),
@@ -1392,7 +1369,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
const result = await runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:billing-cooldown-probe-no-fallbacks",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeConfig(),
@@ -1423,7 +1399,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:support:cooldown-failover",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeAgentOverrideOnlyFallbackConfig("support"),
@@ -1468,7 +1443,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:disabled-failover",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeConfig({ fallbacks: ["openai/mock-2"] }),
@@ -1503,7 +1477,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:auth-unavailable",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeConfig({ fallbacks: ["openai/mock-2"], apiKey: "" }),
@@ -1541,7 +1514,6 @@ describe("runEmbeddedPiAgent auth profile rotation", () => {
await runEmbeddedPiAgentInline({
sessionId: TEST_SESSION_ID,
sessionKey: "agent:test:billing-failover-active-model",
sessionFile: createTestSessionTranscriptLocator(),
workspaceDir,
agentDir,
config: makeConfig({ fallbacks: ["openai/mock-2"] }),

View File

@@ -24,7 +24,7 @@ type MockCompactionResult =
tokensBefore?: number;
tokensAfter?: number;
sessionId?: string;
sessionFile?: string;
transcriptLocator?: string;
};
reason?: string;
}
@@ -228,7 +228,6 @@ export const mockedShouldPreferExplicitConfigApiKeyAuth = vi.fn(() => false);
export const overflowBaseRunParams = {
sessionId: "test-session",
sessionKey: "test-key",
sessionFile: "sqlite-transcript://main/test-session.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 30000,

View File

@@ -166,7 +166,6 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
await runEmbeddedPiAgent({
sessionId: "test-session",
sessionKey: "test-key",
sessionFile: "sqlite-transcript://main/test-session.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 30000,
@@ -583,7 +582,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
expect(mockedCompactDirect).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "test-session",
sessionFile: "sqlite-transcript://main/test-session.jsonl",
transcriptLocator: "sqlite-transcript://main/test-session",
runtimeContext: expect.objectContaining({
trigger: "overflow",
authProfileId: "test-profile",
@@ -736,7 +735,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
await runEmbeddedPiAgent(overflowBaseRunParams);
expect(mockedGlobalHookRunner.runBeforeCompaction).toHaveBeenCalledWith(
{ messageCount: -1, sessionFile: "sqlite-transcript://main/test-session.jsonl" },
{ messageCount: -1, transcriptLocator: "sqlite-transcript://main/test-session" },
expect.objectContaining({
sessionKey: "test-key",
}),
@@ -746,7 +745,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
messageCount: -1,
compactedCount: -1,
tokenCount: 50,
sessionFile: "sqlite-transcript://main/test-session.jsonl",
transcriptLocator: "sqlite-transcript://main/test-session",
},
expect.objectContaining({
sessionKey: "test-key",
@@ -775,7 +774,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
contextEngine: mockedContextEngine,
sessionId: "test-session",
sessionKey: "test-key",
sessionFile: "sqlite-transcript://main/test-session.jsonl",
transcriptLocator: "sqlite-transcript://main/test-session",
reason: "compaction",
runtimeContext: expect.objectContaining({
trigger: "overflow",
@@ -792,7 +791,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
makeAttemptResult({
promptError: null,
sessionIdUsed: "rotated-session",
sessionFileUsed: "/tmp/rotated-session.json",
transcriptLocatorUsed: "sqlite-transcript://main/rotated-session",
}),
);
mockedCompactDirect.mockResolvedValueOnce(
@@ -800,7 +799,7 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
summary: "rotated overflow compaction",
tokensAfter: 50,
sessionId: "rotated-session",
sessionFile: "/tmp/rotated-session.json",
transcriptLocator: "sqlite-transcript://main/rotated-session",
}),
);
@@ -810,13 +809,12 @@ describe("runEmbeddedPiAgent overflow compaction trigger routing", () => {
2,
expect.objectContaining({
sessionId: "rotated-session",
sessionFile: "/tmp/rotated-session.json",
}),
);
expect(mockedRunContextEngineMaintenance).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "rotated-session",
sessionFile: "/tmp/rotated-session.json",
transcriptLocator: "sqlite-transcript://main/rotated-session",
}),
);
});

View File

@@ -4,7 +4,6 @@ import type { ReplyPayload } from "../../auto-reply/reply-payload.js";
import type { ReplyBackendHandle } from "../../auto-reply/reply/reply-run-registry.js";
import type { ThinkLevel } from "../../auto-reply/thinking.js";
import { SILENT_REPLY_TOKEN } from "../../auto-reply/tokens.js";
import { createSqliteSessionTranscriptLocator } from "../../config/sessions/paths.js";
import { ensureContextEnginesInitialized } from "../../context-engine/init.js";
import {
resolveContextEngine,
@@ -93,6 +92,7 @@ import { ensureRuntimePluginsLoaded } from "../runtime-plugins.js";
import type { AgentWorkerPermissionMode } from "../runtime-worker-permissions.js";
import { resolveSessionSuspensionReason, suspendSession } from "../session-suspension.js";
import { resolveToolLoopDetectionConfig } from "../tool-loop-detection-config.js";
import { openTranscriptSessionManagerForSession } from "../transcript/session-manager.js";
import { derivePromptTokens, normalizeUsage, type UsageLike } from "../usage.js";
import { redactRunIdentifier, resolveRunWorkspaceDir } from "../workspace-run.js";
import { runPostCompactionSideEffects } from "./compaction-hooks.js";
@@ -455,15 +455,17 @@ export async function runEmbeddedPiAgent(
config: params.config,
agentId: params.agentId,
});
const sqliteTranscriptLocator = createSqliteSessionTranscriptLocator({
const initialSessionManager = openTranscriptSessionManagerForSession({
agentId: sessionAgentId,
sessionId: params.sessionId,
cwd: params.workspaceDir,
});
const normalizedParams: RunEmbeddedPiAgentParams & { transcriptLocator: string } = {
...params,
transcriptLocator: sqliteTranscriptLocator,
};
params = normalizedParams;
const initialTranscriptLocator = initialSessionManager.getTranscriptLocator();
if (!initialTranscriptLocator) {
throw new Error(
`SQLite transcript scope did not produce a runtime transcript handle: agentId=${sessionAgentId} sessionId=${params.sessionId}`,
);
}
const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId);
const globalLane = resolveGlobalLane(params.lane);
const laneTaskTimeoutMs = resolveEmbeddedRunLaneTimeoutMs(params.timeoutMs);
@@ -1002,9 +1004,9 @@ export async function runEmbeddedPiAgent(
const overloadProfileRotationLimit = resolveOverloadProfileRotationLimit(params.config);
const rateLimitProfileRotationLimit = resolveRateLimitProfileRotationLimit(params.config);
let activeSessionId = params.sessionId;
let activeTranscriptLocator = sqliteTranscriptLocator;
let activeTranscriptLocator = initialTranscriptLocator;
let suppressNextUserMessagePersistence = params.suppressNextUserMessagePersistence ?? false;
// Pi owns transcript persistence; this marker only lets the outer retry avoid
// OpenClaw owns transcript persistence; this marker only lets the outer retry avoid
// replaying the same inbound channel message after overflow compaction.
let lastPersistedCurrentMessageId: string | number | undefined;
const onUserMessagePersisted: RunEmbeddedPiAgentParams["onUserMessagePersisted"] = (
@@ -1310,7 +1312,6 @@ export async function runEmbeddedPiAgent(
currentMessageId: params.currentMessageId,
replyToMode: params.replyToMode,
hasRepliedRef: params.hasRepliedRef,
transcriptLocator: activeTranscriptLocator,
workspaceDir: resolvedWorkspace,
agentDir,
config: params.config,

View File

@@ -28,7 +28,6 @@ function makeParams(): RunEmbeddedPiAgentParams {
model: "gpt-5.5",
prompt: "hello",
runId: "run-1",
transcriptLocator: "/tmp/stale-session.jsonl",
sessionId: "session-1",
sessionKey: "session-key-1",
timeoutMs: 1_000,
@@ -86,7 +85,6 @@ describe("runEmbeddedPiAgent worker launch", () => {
runParams: expect.objectContaining({
sessionId: "session-1",
sessionKey: "session-key-1",
transcriptLocator: "sqlite-transcript://agent-1/session-1",
}),
mode: "worker",
workerChild: false,
@@ -95,7 +93,6 @@ describe("runEmbeddedPiAgent worker launch", () => {
expect.objectContaining({
runId: "run-1",
sessionId: "session-1",
transcriptLocator: "sqlite-transcript://agent-1/session-1",
}),
{
runtimeId: "pi",
@@ -105,7 +102,7 @@ describe("runEmbeddedPiAgent worker launch", () => {
);
});
it("does not forward caller-provided filesystem transcript locators", async () => {
it("does not include transcript locators in worker handoff", async () => {
const workerResult = {
payloads: [{ text: "worker-ok" }],
meta: { durationMs: 12 },
@@ -117,17 +114,10 @@ describe("runEmbeddedPiAgent worker launch", () => {
runPiRunInWorkerMock.mockResolvedValue(workerResult);
vi.stubEnv("OPENCLAW_AGENT_WORKER_MODE", "worker");
await expect(
runEmbeddedPiAgent({
...makeParams(),
transcriptLocator: "/tmp/old-runtime-session.jsonl",
}),
).resolves.toBe(workerResult);
await expect(runEmbeddedPiAgent(makeParams())).resolves.toBe(workerResult);
expect(runPiRunInWorkerMock).toHaveBeenCalledWith(
expect.objectContaining({
transcriptLocator: "sqlite-transcript://agent-1/session-1",
}),
expect.not.objectContaining({ transcriptLocator: expect.anything() }),
expect.anything(),
);
});

View File

@@ -23,8 +23,12 @@ export async function resolveAttemptBootstrapContext<TBootstrapFile, TContextFil
bootstrapContextMode?: string;
bootstrapContextRunKind?: string;
bootstrapMode?: BootstrapMode;
sessionFile: string;
hasCompletedBootstrapTranscriptTurn: (transcriptLocator: string) => Promise<boolean>;
agentId: string;
sessionId: string;
hasCompletedBootstrapSessionTurn: (scope: {
agentId: string;
sessionId: string;
}) => Promise<boolean>;
resolveBootstrapContextForRun: () => Promise<
AttemptBootstrapContext<TBootstrapFile, TContextFile>
>;
@@ -38,7 +42,10 @@ export async function resolveAttemptBootstrapContext<TBootstrapFile, TContextFil
params.bootstrapMode !== "full" &&
params.contextInjectionMode === "continuation-skip" &&
params.bootstrapContextRunKind !== "heartbeat" &&
(await params.hasCompletedBootstrapTranscriptTurn(params.sessionFile));
(await params.hasCompletedBootstrapSessionTurn({
agentId: params.agentId,
sessionId: params.sessionId,
}));
const shouldSkipBootstrapInjection =
params.contextInjectionMode === "never" || isContinuationTurn;
const shouldRecordCompletedBootstrapTurn =

View File

@@ -11,7 +11,7 @@ import {
} from "./attempt.context-engine-helpers.js";
import { resetEmbeddedAttemptHarness } from "./attempt.spawn-workspace.test-support.js";
const TEST_TRANSCRIPT_LOCATOR = "sqlite-transcript://main/session-context-injection";
const TEST_BOOTSTRAP_SCOPE = { agentId: "main", sessionId: "session-context-injection" };
async function resolveBootstrapContext(params: {
contextInjectionMode?: "always" | "continuation-skip" | "never";
@@ -21,7 +21,7 @@ async function resolveBootstrapContext(params: {
completed?: boolean;
resolver?: () => Promise<{ bootstrapFiles: unknown[]; contextFiles: unknown[] }>;
}) {
const hasCompletedBootstrapTranscriptTurn = vi.fn(async () => params.completed ?? false);
const hasCompletedBootstrapSessionTurn = vi.fn(async () => params.completed ?? false);
const resolveBootstrapContextForRun =
params.resolver ??
vi.fn(async () => ({
@@ -34,12 +34,12 @@ async function resolveBootstrapContext(params: {
bootstrapContextMode: params.bootstrapContextMode ?? "full",
bootstrapContextRunKind: params.bootstrapContextRunKind ?? "default",
bootstrapMode: params.bootstrapMode ?? "none",
transcriptLocator: TEST_TRANSCRIPT_LOCATOR,
hasCompletedBootstrapTranscriptTurn,
...TEST_BOOTSTRAP_SCOPE,
hasCompletedBootstrapSessionTurn,
resolveBootstrapContextForRun,
});
return { result, hasCompletedBootstrapTranscriptTurn, resolveBootstrapContextForRun };
return { result, hasCompletedBootstrapSessionTurn, resolveBootstrapContextForRun };
}
describe("embedded attempt context injection", () => {
@@ -48,7 +48,7 @@ describe("embedded attempt context injection", () => {
});
it("skips bootstrap reinjection on safe continuation turns when configured", async () => {
const { result, hasCompletedBootstrapTranscriptTurn, resolveBootstrapContextForRun } =
const { result, hasCompletedBootstrapSessionTurn, resolveBootstrapContextForRun } =
await resolveBootstrapContext({
contextInjectionMode: "continuation-skip",
completed: true,
@@ -57,7 +57,7 @@ describe("embedded attempt context injection", () => {
expect(result.isContinuationTurn).toBe(true);
expect(result.bootstrapFiles).toEqual([]);
expect(result.contextFiles).toEqual([]);
expect(hasCompletedBootstrapTranscriptTurn).toHaveBeenCalledWith(TEST_TRANSCRIPT_LOCATOR);
expect(hasCompletedBootstrapSessionTurn).toHaveBeenCalledWith(TEST_BOOTSTRAP_SCOPE);
expect(resolveBootstrapContextForRun).not.toHaveBeenCalled();
});
@@ -80,7 +80,7 @@ describe("embedded attempt context injection", () => {
});
it("disables bootstrap injection without marking the turn as a continuation", async () => {
const { result, hasCompletedBootstrapTranscriptTurn, resolveBootstrapContextForRun } =
const { result, hasCompletedBootstrapSessionTurn, resolveBootstrapContextForRun } =
await resolveBootstrapContext({
contextInjectionMode: "never",
bootstrapMode: "full",
@@ -91,7 +91,7 @@ describe("embedded attempt context injection", () => {
expect(result.shouldRecordCompletedBootstrapTurn).toBe(false);
expect(result.bootstrapFiles).toEqual([]);
expect(result.contextFiles).toEqual([]);
expect(hasCompletedBootstrapTranscriptTurn).not.toHaveBeenCalled();
expect(hasCompletedBootstrapSessionTurn).not.toHaveBeenCalled();
expect(resolveBootstrapContextForRun).not.toHaveBeenCalled();
});
@@ -101,7 +101,7 @@ describe("embedded attempt context injection", () => {
contextFiles: [{ path: "BOOTSTRAP.md" }],
}));
const { result, hasCompletedBootstrapTranscriptTurn } = await resolveBootstrapContext({
const { result, hasCompletedBootstrapSessionTurn } = await resolveBootstrapContext({
contextInjectionMode: "continuation-skip",
bootstrapMode: "full",
completed: true,
@@ -111,7 +111,7 @@ describe("embedded attempt context injection", () => {
expect(result.isContinuationTurn).toBe(false);
expect(result.bootstrapFiles).toEqual([{ name: "BOOTSTRAP.md" }]);
expect(result.contextFiles).toEqual([{ path: "BOOTSTRAP.md" }]);
expect(hasCompletedBootstrapTranscriptTurn).not.toHaveBeenCalled();
expect(hasCompletedBootstrapSessionTurn).not.toHaveBeenCalled();
expect(resolver).toHaveBeenCalledTimes(1);
});
@@ -143,7 +143,7 @@ describe("embedded attempt context injection", () => {
});
it("never skips heartbeat bootstrap filtering", async () => {
const { result, hasCompletedBootstrapTranscriptTurn, resolveBootstrapContextForRun } =
const { result, hasCompletedBootstrapSessionTurn, resolveBootstrapContextForRun } =
await resolveBootstrapContext({
contextInjectionMode: "continuation-skip",
bootstrapContextMode: "lightweight",
@@ -153,7 +153,7 @@ describe("embedded attempt context injection", () => {
expect(result.isContinuationTurn).toBe(false);
expect(result.shouldRecordCompletedBootstrapTurn).toBe(false);
expect(hasCompletedBootstrapTranscriptTurn).not.toHaveBeenCalled();
expect(hasCompletedBootstrapSessionTurn).not.toHaveBeenCalled();
expect(resolveBootstrapContextForRun).toHaveBeenCalledTimes(1);
});
@@ -185,7 +185,7 @@ describe("embedded attempt context injection", () => {
});
it("allows continuation skip again for limited bootstrap mode", async () => {
const { result, hasCompletedBootstrapTranscriptTurn, resolveBootstrapContextForRun } =
const { result, hasCompletedBootstrapSessionTurn, resolveBootstrapContextForRun } =
await resolveBootstrapContext({
contextInjectionMode: "continuation-skip",
bootstrapMode: "limited",
@@ -193,7 +193,7 @@ describe("embedded attempt context injection", () => {
});
expect(result.isContinuationTurn).toBe(true);
expect(hasCompletedBootstrapTranscriptTurn).toHaveBeenCalledWith(TEST_TRANSCRIPT_LOCATOR);
expect(hasCompletedBootstrapSessionTurn).toHaveBeenCalledWith(TEST_BOOTSTRAP_SCOPE);
expect(resolveBootstrapContextForRun).not.toHaveBeenCalled();
expect(result.shouldRecordCompletedBootstrapTurn).toBe(false);
});

View File

@@ -68,6 +68,7 @@ type AttemptSpawnWorkspaceHoisted = {
installToolResultContextGuardMock: UnknownMock;
installContextEngineLoopHookMock: UnknownMock;
flushPendingToolResultsAfterIdleMock: AsyncUnknownMock;
releaseWsSessionMock: UnknownMock;
resolveBootstrapFilesForRunMock: Mock<(...args: unknown[]) => Promise<WorkspaceBootstrapFile[]>>;
resolveBootstrapContextForRunMock: Mock<() => Promise<BootstrapContext>>;
isWorkspaceBootstrapPendingMock: Mock<(workspaceDir: string) => Promise<boolean>>;
@@ -131,6 +132,7 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => {
const installToolResultContextGuardMock = vi.fn(() => () => {});
const installContextEngineLoopHookMock = vi.fn(() => () => {});
const flushPendingToolResultsAfterIdleMock = vi.fn(async () => {});
const releaseWsSessionMock = vi.fn(() => {});
const subscribeEmbeddedPiSessionMock = vi.fn<SubscribeEmbeddedPiSessionFn>(() =>
createSubscriptionMock(),
);
@@ -197,6 +199,7 @@ const hoisted = vi.hoisted((): AttemptSpawnWorkspaceHoisted => {
installToolResultContextGuardMock,
installContextEngineLoopHookMock,
flushPendingToolResultsAfterIdleMock,
releaseWsSessionMock,
resolveBootstrapFilesForRunMock,
resolveBootstrapContextForRunMock,
isWorkspaceBootstrapPendingMock,
@@ -510,6 +513,12 @@ vi.mock("../extra-params.js", async () => {
};
});
vi.mock("../../openai-ws-stream.js", () => ({
createOpenAIWebSocketStreamFn: vi.fn(),
releaseWsSession: (...args: unknown[]) =>
(hoisted.releaseWsSessionMock as (...args: unknown[]) => unknown)(...args),
}));
vi.mock("../../anthropic-payload-log.js", () => ({
createAnthropicPayloadLogger: () => undefined,
}));
@@ -879,6 +888,7 @@ export function resetEmbeddedAttemptHarness(
hoisted.installToolResultContextGuardMock.mockReset().mockReturnValue(() => {});
hoisted.installContextEngineLoopHookMock.mockReset().mockReturnValue(() => {});
hoisted.flushPendingToolResultsAfterIdleMock.mockReset().mockResolvedValue(undefined);
hoisted.releaseWsSessionMock.mockReset().mockReturnValue(undefined);
hoisted.resolveBootstrapContextForRunMock.mockReset().mockResolvedValue({
bootstrapFiles: [],
contextFiles: [],
@@ -1016,14 +1026,14 @@ export async function createContextEngineAttemptRunner(params: {
bootstrap?: (params: {
sessionId: string;
sessionKey?: string;
sessionFile: string;
transcriptLocator: string;
}) => Promise<BootstrapResult>;
maintain?:
| boolean
| ((params: {
sessionId: string;
sessionKey?: string;
sessionFile: string;
transcriptLocator: string;
runtimeContext?: Record<string, unknown>;
}) => Promise<{
changed: boolean;
@@ -1041,7 +1051,7 @@ export async function createContextEngineAttemptRunner(params: {
afterTurn?: (params: {
sessionId: string;
sessionKey?: string;
sessionFile: string;
transcriptLocator: string;
messages: AgentMessage[];
prePromptMessageCount: number;
tokenBudget?: number;
@@ -1060,7 +1070,7 @@ export async function createContextEngineAttemptRunner(params: {
compact?: (params: {
sessionId: string;
sessionKey?: string;
sessionFile: string;
transcriptLocator: string;
tokenBudget?: number;
}) => Promise<CompactResult>;
info?: Partial<ContextEngineInfo>;
@@ -1077,7 +1087,7 @@ export async function createContextEngineAttemptRunner(params: {
const agentDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ctx-engine-agent-"));
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-ctx-engine-state-"));
const sessionId = "embedded-session";
const sessionFile = createSqliteSessionTranscriptLocator({
const transcriptLocator = createSqliteSessionTranscriptLocator({
agentId: resolveAgentIdFromSessionKey(params.sessionKey) ?? DEFAULT_AGENT_ID,
sessionId,
});
@@ -1122,7 +1132,6 @@ export async function createContextEngineAttemptRunner(params: {
)({
sessionId,
sessionKey: params.sessionKey,
sessionFile,
workspaceDir,
agentDir,
config: {},

View File

@@ -5,7 +5,6 @@ import { isAcpRuntimeSpawnAvailable } from "../../../acp/runtime/availability.js
import { buildHierarchyReinforcementMessage } from "../../../auto-reply/handoff-summarizer.js";
import { filterHeartbeatPairs } from "../../../auto-reply/heartbeat-filter.js";
import { getRuntimeConfig } from "../../../config/config.js";
import { createSqliteSessionTranscriptLocator } from "../../../config/sessions/paths.js";
import {
getSessionEntry,
listSessionEntries,
@@ -67,7 +66,7 @@ import {
import {
FULL_BOOTSTRAP_COMPLETED_CUSTOM_TYPE,
buildBootstrapContextForFiles,
hasCompletedBootstrapTranscriptTurn,
hasCompletedBootstrapSessionTurn,
isWorkspaceBootstrapPending,
makeBootstrapWarn,
resolveBootstrapFilesForRun,
@@ -163,9 +162,9 @@ import {
} from "../../tool-allowlist-guard.js";
import { UNKNOWN_TOOL_THRESHOLD } from "../../tool-loop-detection.js";
import { shouldAllowProviderOwnedThinkingReplay } from "../../transcript-policy.js";
import { repairTranscriptStateIfNeeded } from "../../transcript-state-repair.js";
import { repairTranscriptSessionStateIfNeeded } from "../../transcript-state-repair.js";
import { removeSessionManagerTailEntries } from "../../transcript/session-manager-tail.js";
import { openTranscriptSessionManager } from "../../transcript/session-manager.js";
import { openTranscriptSessionManagerForSession } from "../../transcript/session-manager.js";
import { normalizeUsage, type NormalizedUsage } from "../../usage.js";
import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js";
import { isRunnerAbortError } from "../abort.js";
@@ -722,13 +721,6 @@ export async function runEmbeddedAttempt(
config: params.config,
agentId: params.agentId,
});
const sqliteTranscriptLocator = createSqliteSessionTranscriptLocator({
agentId: sessionAgentId,
sessionId: params.sessionId,
});
if (params.transcriptLocator !== sqliteTranscriptLocator) {
params = { ...params, transcriptLocator: sqliteTranscriptLocator };
}
const runArtifactStore = createRunArtifactStoreBestEffort({
agentId: sessionAgentId,
runId: params.runId,
@@ -974,8 +966,9 @@ export async function runEmbeddedAttempt(
bootstrapContextMode: params.bootstrapContextMode,
bootstrapContextRunKind: params.bootstrapContextRunKind ?? "default",
bootstrapMode,
transcriptLocator: params.transcriptLocator,
hasCompletedBootstrapTranscriptTurn,
agentId: sessionAgentId,
sessionId: params.sessionId,
hasCompletedBootstrapSessionTurn,
resolveBootstrapContextForRun: async () => {
const bootstrapFiles =
preloadedBootstrapFiles ??
@@ -1397,12 +1390,13 @@ export async function runEmbeddedAttempt(
let trajectoryRecorder: ReturnType<typeof createTrajectoryRuntimeRecorder> | null = null;
let trajectoryEndRecorded = false;
try {
await repairTranscriptStateIfNeeded({
transcriptLocator: params.transcriptLocator,
await repairTranscriptSessionStateIfNeeded({
agentId: sessionAgentId,
sessionId: params.sessionId,
debug: (message) => log.debug(message),
warn: (message) => log.warn(message),
});
const hadTranscriptLocator = hasSqliteSessionTranscriptEvents({
const hadTranscriptEvents = hasSqliteSessionTranscriptEvents({
agentId: sessionAgentId,
sessionId: params.sessionId,
});
@@ -1417,8 +1411,8 @@ export async function runEmbeddedAttempt(
});
sessionManager = guardSessionManager(
openTranscriptSessionManager({
transcriptLocator: params.transcriptLocator,
openTranscriptSessionManagerForSession({
agentId: sessionAgentId,
sessionId: params.sessionId,
cwd: effectiveWorkspace,
}),
@@ -1443,12 +1437,18 @@ export async function runEmbeddedAttempt(
},
},
);
const sessionTranscriptLocator = sessionManager.getTranscriptLocator();
if (!sessionTranscriptLocator) {
throw new Error(
`SQLite transcript scope did not produce a runtime transcript handle: agentId=${sessionAgentId} sessionId=${params.sessionId}`,
);
}
await runAttemptContextEngineBootstrap({
hadTranscriptLocator,
hadTranscriptLocator: hadTranscriptEvents,
contextEngine: activeContextEngine,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
transcriptLocator: params.transcriptLocator,
transcriptLocator: sessionTranscriptLocator,
sessionManager,
runtimeContext: buildAfterTurnRuntimeContext({
attempt: params,
@@ -1736,7 +1736,7 @@ export async function runEmbeddedAttempt(
contextEngine: activeContextEngine,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
transcriptLocator: params.transcriptLocator,
transcriptLocator: sessionTranscriptLocator,
tokenBudget: params.contextTokenBudget,
modelId: params.modelId,
getPrePromptMessageCount: () => prePromptMessageCount,
@@ -2532,7 +2532,7 @@ export async function runEmbeddedAttempt(
let messagesSnapshot: AgentMessage[] = [];
let sessionIdUsed = activeSession.sessionId;
let transcriptLocatorUsed: string | undefined = params.transcriptLocator;
let transcriptLocatorUsed: string | undefined = sessionTranscriptLocator;
const onAbort = () => {
externalAbort = true;
const reason = params.abortSignal ? getAbortReason(params.abortSignal) : undefined;
@@ -2576,7 +2576,7 @@ export async function runEmbeddedAttempt(
`effectiveReserveTokens=${request.effectiveReserveTokens} ` +
`prePromptMessageCount=${prePromptMessageCount} ` +
(extra ? `${extra} ` : "") +
`transcriptLocator=${params.transcriptLocator}`,
`transcriptLocator=${sessionTranscriptLocator}`,
);
};
if (request.route === "truncate_tool_results_only") {
@@ -2591,7 +2591,7 @@ export async function runEmbeddedAttempt(
contextWindowTokens: contextTokenBudget,
maxCharsOverride: toolResultMaxChars,
agentId: sessionAgentId,
transcriptLocator: params.transcriptLocator,
transcriptLocator: sessionTranscriptLocator,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
});
@@ -3063,7 +3063,7 @@ export async function runEmbeddedAttempt(
`historyImageBlocks=${sessionSummary.totalImageBlocks} ` +
`systemPromptChars=${systemLen} promptChars=${promptLen} ` +
`promptImages=${imageResult.images.length} ` +
`provider=${params.provider}/${params.modelId} transcriptLocator=${params.transcriptLocator}`,
`provider=${params.provider}/${params.modelId} transcriptLocator=${sessionTranscriptLocator}`,
);
}
@@ -3126,7 +3126,7 @@ export async function runEmbeddedAttempt(
contextWindowTokens: contextTokenBudget,
maxCharsOverride: toolResultMaxChars,
agentId: sessionAgentId,
transcriptLocator: params.transcriptLocator,
transcriptLocator: sessionTranscriptLocator,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
});
@@ -3145,7 +3145,7 @@ export async function runEmbeddedAttempt(
`overflowTokens=${preemptiveCompaction.overflowTokens} ` +
`toolResultReducibleChars=${preemptiveCompaction.toolResultReducibleChars} ` +
`effectiveReserveTokens=${preemptiveCompaction.effectiveReserveTokens} ` +
`transcriptLocator=${params.transcriptLocator}`,
`transcriptLocator=${sessionTranscriptLocator}`,
);
skipPromptSubmission = true;
}
@@ -3153,7 +3153,7 @@ export async function runEmbeddedAttempt(
log.warn(
`[context-overflow-precheck] early tool-result truncation did not help for ` +
`${params.provider}/${params.modelId}; falling back to compaction ` +
`reason=${truncationResult.reason ?? "unknown"} transcriptLocator=${params.transcriptLocator}`,
`reason=${truncationResult.reason ?? "unknown"} transcriptLocator=${sessionTranscriptLocator}`,
);
preflightRecovery = { route: "compact_only" };
promptError = new Error(PREEMPTIVE_OVERFLOW_ERROR_TEXT);
@@ -3178,7 +3178,7 @@ export async function runEmbeddedAttempt(
`toolResultReducibleChars=${preemptiveCompaction.toolResultReducibleChars} ` +
`reserveTokens=${reserveTokens} ` +
`effectiveReserveTokens=${preemptiveCompaction.effectiveReserveTokens} ` +
`transcriptLocator=${params.transcriptLocator}`,
`transcriptLocator=${sessionTranscriptLocator}`,
);
skipPromptSubmission = true;
}
@@ -3446,7 +3446,7 @@ export async function runEmbeddedAttempt(
yieldAborted,
sessionIdUsed,
sessionKey: params.sessionKey,
transcriptLocator: params.transcriptLocator,
transcriptLocator: sessionTranscriptLocator,
messagesSnapshot,
prePromptMessageCount,
tokenBudget: params.contextTokenBudget,
@@ -3503,7 +3503,7 @@ export async function runEmbeddedAttempt(
const rotation = await rotateTranscriptAfterCompaction({
sessionManager,
agentId: params.agentId,
transcriptLocator: params.transcriptLocator,
transcriptLocator: sessionTranscriptLocator,
});
if (rotation.rotated) {
sessionIdUsed = rotation.sessionId ?? sessionIdUsed;

View File

@@ -99,12 +99,6 @@ export type RunEmbeddedPiAgentParams = {
forceHeartbeatTool?: boolean;
/** Allow runtime plugins for this run to late-bind the gateway subagent. */
allowGatewaySubagentBinding?: boolean;
/**
* Ignored legacy boundary hint. `runEmbeddedPiAgent()` always derives the
* active SQLite transcript locator from `{agentId, sessionId}` before it
* writes, so callers cannot route runtime transcript writes to JSON files.
*/
transcriptLocator?: string;
workspaceDir: string;
agentDir?: string;
config?: OpenClawConfig;

View File

@@ -21,18 +21,10 @@ import type { PreemptiveCompactionRoute } from "./preemptive-compaction.types.js
type EmbeddedRunAttemptBase = Omit<
RunEmbeddedPiAgentParams,
| "provider"
| "model"
| "authProfileId"
| "authProfileIdSource"
| "thinkLevel"
| "lane"
| "enqueue"
| "transcriptLocator"
"provider" | "model" | "authProfileId" | "authProfileIdSource" | "thinkLevel" | "lane" | "enqueue"
>;
export type EmbeddedRunAttemptParams = EmbeddedRunAttemptBase & {
transcriptLocator: string;
initialReplayState?: EmbeddedRunReplayState;
/** Pluggable context engine for ingest/assemble/compact lifecycle. */
contextEngine?: ContextEngine;

View File

@@ -10,7 +10,6 @@ import {
import type { EmbeddedRunAttemptResult } from "./run/types.js";
let runEmbeddedPiAgent: typeof import("./run.js").runEmbeddedPiAgent;
const TEST_TRANSCRIPT_LOCATOR = "sqlite-transcript://main/test-session";
function makeAssistantMessage(
overrides: Partial<AssistantMessage> = {},
@@ -48,7 +47,6 @@ describe("runEmbeddedPiAgent usage reporting", () => {
await runEmbeddedPiAgent({
sessionId: "test-session",
sessionKey: "test-key",
transcriptLocator: TEST_TRANSCRIPT_LOCATOR,
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 30000,
@@ -71,7 +69,6 @@ describe("runEmbeddedPiAgent usage reporting", () => {
await runEmbeddedPiAgent({
sessionId: "test-session",
sessionKey: "test-key",
transcriptLocator: TEST_TRANSCRIPT_LOCATOR,
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 30000,
@@ -100,7 +97,6 @@ describe("runEmbeddedPiAgent usage reporting", () => {
await runEmbeddedPiAgent({
sessionId: "test-session",
sessionKey: "test-key",
transcriptLocator: TEST_TRANSCRIPT_LOCATOR,
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 30000,
@@ -135,7 +131,6 @@ describe("runEmbeddedPiAgent usage reporting", () => {
await runEmbeddedPiAgent({
sessionId: "test-session",
sessionKey: "test-key",
transcriptLocator: TEST_TRANSCRIPT_LOCATOR,
workspaceDir: "/tmp/workspace",
prompt: "flush",
timeoutMs: 30000,
@@ -180,7 +175,6 @@ describe("runEmbeddedPiAgent usage reporting", () => {
const result = await runEmbeddedPiAgent({
sessionId: "test-session",
sessionKey: "test-key",
transcriptLocator: TEST_TRANSCRIPT_LOCATOR,
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 30000,
@@ -227,7 +221,6 @@ describe("runEmbeddedPiAgent usage reporting", () => {
const result = await runEmbeddedPiAgent({
sessionId: "test-session",
sessionKey: "test-key",
transcriptLocator: TEST_TRANSCRIPT_LOCATOR,
workspaceDir: "/tmp/workspace",
prompt: "hello",
provider: "openrouter",

View File

@@ -8,7 +8,6 @@ function createPreparedRun(overrides: Partial<PreparedAgentRun> = {}): PreparedA
agentId: "main",
sessionId: "session-1",
sessionKey: "agent:main:main",
sessionFile: "sqlite-transcript://main/session-1.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 1000,

View File

@@ -16,7 +16,6 @@ export type PreparedAgentRun = {
agentId: string;
sessionId: string;
sessionKey?: string;
sessionFile: string;
workspaceDir: string;
agentDir?: string;
prompt: string;
@@ -96,7 +95,6 @@ export function assertPreparedAgentRunSerializable(run: PreparedAgentRun): Prepa
"runId",
"agentId",
"sessionId",
"sessionFile",
"workspaceDir",
"prompt",
] satisfies (keyof PreparedAgentRun)[];

View File

@@ -13,7 +13,6 @@ function createPreparedRun(overrides: Partial<PreparedAgentRun> = {}): PreparedA
agentId: "main",
sessionId: "session-permissions",
sessionKey: "agent:main:main",
sessionFile: "sqlite-transcript://main/session-permissions.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 1000,

View File

@@ -23,7 +23,6 @@ function createPreparedRun(
agentId: "main",
sessionId: "session-worker",
sessionKey: "agent:main:main",
sessionFile: "sqlite-transcript://main/session-worker.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 1000,

View File

@@ -40,7 +40,6 @@ function createPreparedRun(overrides: Partial<PreparedAgentRun> = {}): PreparedA
agentId: "main",
sessionId: "session-worker",
sessionKey: "agent:main:main",
sessionFile: "sqlite-transcript://main/session-worker.jsonl",
workspaceDir: "/tmp/workspace",
prompt: "hello",
timeoutMs: 1000,

View File

@@ -27,6 +27,11 @@ type SessionMessageEntry = {
message: { role: string; content?: unknown } & Record<string, unknown>;
} & Record<string, unknown>;
type TranscriptRepairScope = {
agentId: string;
sessionId: string;
};
function isSessionHeader(entry: unknown): entry is { type: string; id: string } {
if (!entry || typeof entry !== "object") {
return false;
@@ -184,24 +189,13 @@ function buildRepairSummaryParts(params: {
return parts.length > 0 ? parts.join(", ") : "no changes";
}
export async function repairTranscriptStateIfNeeded(params: {
transcriptLocator: string;
async function repairTranscriptEntries(params: {
scope: TranscriptRepairScope;
label: string;
debug?: (message: string) => void;
warn?: (message: string) => void;
}): Promise<RepairReport> {
const transcriptLocator = params.transcriptLocator.trim();
if (!transcriptLocator) {
return { repaired: false, droppedLines: 0, reason: "missing session transcript" };
}
const scope = resolveSqliteSessionTranscriptScopeForLocator({
transcriptLocator: transcriptLocator,
});
if (!scope) {
return { repaired: false, droppedLines: 0, reason: "missing SQLite transcript" };
}
const storedEntries = loadSqliteSessionTranscriptEvents(scope).map((entry) => entry.event);
const storedEntries = loadSqliteSessionTranscriptEvents(params.scope).map((entry) => entry.event);
const entries: unknown[] = [];
let droppedLines = 0;
let rewrittenAssistantMessages = 0;
@@ -246,9 +240,7 @@ export async function repairTranscriptStateIfNeeded(params: {
}
if (!isSessionHeader(entries[0])) {
params.warn?.(
`session transcript repair skipped: invalid session header (${transcriptLocator})`,
);
params.warn?.(`session transcript repair skipped: invalid session header (${params.label})`);
return { repaired: false, droppedLines, reason: "invalid session header" };
}
@@ -263,7 +255,7 @@ export async function repairTranscriptStateIfNeeded(params: {
try {
replaceSqliteSessionTranscriptEvents({
...scope,
...params.scope,
events: entries,
});
} catch (err) {
@@ -283,7 +275,7 @@ export async function repairTranscriptStateIfNeeded(params: {
rewrittenAssistantMessages,
droppedBlankUserMessages,
rewrittenUserMessages,
})} (${transcriptLocator})`,
})} (${params.label})`,
);
return {
repaired: true,
@@ -293,3 +285,48 @@ export async function repairTranscriptStateIfNeeded(params: {
rewrittenUserMessages,
};
}
export async function repairTranscriptStateIfNeeded(params: {
transcriptLocator: string;
debug?: (message: string) => void;
warn?: (message: string) => void;
}): Promise<RepairReport> {
const transcriptLocator = params.transcriptLocator.trim();
if (!transcriptLocator) {
return { repaired: false, droppedLines: 0, reason: "missing session transcript" };
}
const scope = resolveSqliteSessionTranscriptScopeForLocator({
transcriptLocator: transcriptLocator,
});
if (!scope) {
return { repaired: false, droppedLines: 0, reason: "missing SQLite transcript" };
}
return repairTranscriptEntries({
scope,
label: transcriptLocator,
debug: params.debug,
warn: params.warn,
});
}
export async function repairTranscriptSessionStateIfNeeded(params: {
agentId: string;
sessionId: string;
debug?: (message: string) => void;
warn?: (message: string) => void;
}): Promise<RepairReport> {
const agentId = params.agentId.trim();
const sessionId = params.sessionId.trim();
if (!agentId || !sessionId) {
return { repaired: false, droppedLines: 0, reason: "missing SQLite transcript scope" };
}
return repairTranscriptEntries({
scope: { agentId, sessionId },
label: `agentId=${agentId} sessionId=${sessionId}`,
debug: params.debug,
warn: params.warn,
});
}

View File

@@ -63,6 +63,27 @@ function normalizeTranscriptLocator(transcriptLocator: string): string {
);
}
function normalizeTranscriptScopeId(value: string, label: string): string {
const trimmed = value.trim();
if (!trimmed) {
throw new Error(`SQLite transcript ${label} is required`);
}
return trimmed;
}
function createTranscriptScope(params: {
agentId: string;
sessionId: string;
}): TranscriptSqliteScope {
const agentId = normalizeTranscriptScopeId(params.agentId, "agent id");
const sessionId = normalizeTranscriptScopeId(params.sessionId, "session id");
return {
agentId,
sessionId,
transcriptLocator: createSqliteSessionTranscriptLocator({ agentId, sessionId }),
};
}
function createTranscriptLocator(header: SessionHeader, agentId = DEFAULT_AGENT_ID): string {
return createSqliteSessionTranscriptLocator({
agentId,
@@ -141,6 +162,32 @@ function loadTranscriptState(params: {
return { state, scope: headerScope };
}
function loadTranscriptStateForSession(params: {
agentId: string;
sessionId: string;
cwd?: string;
}): {
state: TranscriptState;
scope: TranscriptSqliteScope;
} {
const scope = createTranscriptScope({
agentId: params.agentId,
sessionId: params.sessionId,
});
const sqliteEvents = loadSqliteSessionTranscriptEvents(scope).map((entry) => entry.event);
if (sqliteEvents.length > 0) {
return { state: createTranscriptStateFromEvents(sqliteEvents), scope };
}
const header = createSessionHeader({
id: scope.sessionId,
cwd: params.cwd ?? process.cwd(),
});
const state = new TranscriptState({ header, entries: [] });
persistFullTranscriptStateToSqlite(scope, state);
return { state, scope };
}
function isMessageWithContent(
message: unknown,
): message is { role: string; content: unknown; timestamp?: unknown } {
@@ -315,6 +362,20 @@ export class TranscriptSessionManager implements SessionManager {
});
}
static openForSession(params: {
agentId: string;
sessionId: string;
cwd?: string;
}): TranscriptSessionManager {
const loaded = loadTranscriptStateForSession(params);
return new TranscriptSessionManager({
transcriptLocator: loaded.scope.transcriptLocator,
persist: true,
state: loaded.state,
sqliteScope: loaded.scope,
});
}
static create(cwd: string): TranscriptSessionManager {
const header = createSessionHeader({ cwd });
const transcriptLocator = createTranscriptLocator(header);
@@ -620,6 +681,14 @@ export function openTranscriptSessionManager(params: {
return TranscriptSessionManager.open(params);
}
export function openTranscriptSessionManagerForSession(params: {
agentId: string;
sessionId: string;
cwd?: string;
}): SessionManager {
return TranscriptSessionManager.openForSession(params);
}
export const SessionManagerValue = {
create: (cwd: string) => TranscriptSessionManager.create(cwd),
open: (transcriptLocator: string, cwdOverride?: string) => {

View File

@@ -289,7 +289,6 @@ export function createFollowupRunner(params: {
senderUsername: run.senderUsername,
senderE164: run.senderE164,
senderIsOwner: run.senderIsOwner,
transcriptLocator: run.transcriptLocator,
agentDir: run.agentDir,
workspaceDir: run.workspaceDir,
config: runtimeConfig,

View File

@@ -127,7 +127,6 @@ vi.mock("../agents/command/attempt-execution.runtime.js", () => {
messageTo: opts.replyTo ?? opts.to,
messageThreadId: opts.threadId,
senderIsOwner: opts.senderIsOwner,
sessionFile: params.sessionFile,
workspaceDir: params.workspaceDir,
config: params.cfg,
skillsSnapshot: params.skillsSnapshot,
@@ -238,16 +237,16 @@ vi.mock("../config/sessions/transcript-resolve.runtime.js", () => {
async (params: {
sessionId: string;
sessionKey: string;
sessionEntry?: { sessionFile?: string; sessionId?: string };
sessionStore?: Record<string, { sessionFile?: string; sessionId?: string }>;
sessionEntry?: { transcriptLocator?: string; sessionId?: string };
sessionStore?: Record<string, { transcriptLocator?: string; sessionId?: string }>;
agentId: string;
threadId?: string | number;
}) => {
const sessionFileFromStorePath =
params.sessionEntry?.sessionFile ??
const transcriptLocatorFromStorePath =
params.sessionEntry?.transcriptLocator ??
resolveTranscriptLocator(params.sessionId, params.agentId);
const sessionFile = params.sessionEntry?.sessionFile
? sessionFileFromStorePath
const transcriptLocator = params.sessionEntry?.transcriptLocator
? transcriptLocatorFromStorePath
: resolveTranscriptLocator(params.sessionId, params.agentId);
let sessionEntry = params.sessionEntry;
if (params.sessionStore && params.sessionKey) {
@@ -258,12 +257,12 @@ vi.mock("../config/sessions/transcript-resolve.runtime.js", () => {
sessionEntry = {
...existingEntry,
sessionId: params.sessionId,
sessionFile,
transcriptLocator,
};
params.sessionStore[params.sessionKey] = sessionEntry;
await replaceTestSessionRows(params.agentId, params.sessionStore as never);
}
return { sessionFile, sessionEntry };
return { transcriptLocator, sessionEntry };
},
),
};
@@ -702,12 +701,7 @@ describe("agentCommand", () => {
const callArgs = vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0];
expect(callArgs?.sessionId).toBe("session-123");
expect(callArgs?.sessionFile).toContain(
path.join("agents", "main", "sessions", "session-123.jsonl"),
);
expect(callArgs?.sessionFile).not.toContain(
`${path.sep}sessions${path.sep}agents${path.sep}main${path.sep}sessions${path.sep}`,
);
expect(callArgs).not.toHaveProperty("transcriptLocator");
});
});
@@ -1096,7 +1090,7 @@ describe("agentCommand", () => {
);
let callArgs = getLastEmbeddedCall();
expect(callArgs?.sessionKey).toBe("agent:ops:main");
expect(callArgs?.sessionFile).toContain(`${path.sep}agents${path.sep}ops${path.sep}sessions`);
expect(callArgs).not.toHaveProperty("transcriptLocator");
expect(callArgs?.messageChannel).toBe("slack");
expect(runtime.log).toHaveBeenCalledWith("ok");

View File

@@ -24,7 +24,6 @@ import {
parseModelRef,
} from "../../agents/model-selection.js";
import { resolveDefaultAgentWorkspaceDir } from "../../agents/workspace.js";
import { createSqliteSessionTranscriptLocator } from "../../config/sessions/paths.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import { coerceSecretRef, normalizeSecretInputString } from "../../config/types.secrets.js";
import { type SecretRefResolveCache, resolveSecretRefString } from "../../secrets/resolve.js";
@@ -457,7 +456,6 @@ async function probeTarget(params: {
const model = target.model;
const sessionId = `probe-${target.provider}-${crypto.randomUUID()}`;
const sessionFile = createSqliteSessionTranscriptLocator({ sessionId, agentId });
const start = Date.now();
const buildResult = (status: AuthProbeResult["status"], error?: string): AuthProbeResult => ({
@@ -475,7 +473,6 @@ async function probeTarget(params: {
const { runEmbeddedPiAgent } = await loadEmbeddedRunnerModule();
await runEmbeddedPiAgent({
sessionId,
sessionFile,
agentId,
workspaceDir,
agentDir,

View File

@@ -203,11 +203,13 @@ describe("commitment extraction runtime", () => {
await expect(drainCommitmentExtractionQueue()).resolves.toBe(1);
expect(resolveDefaultModelMock).toHaveBeenCalledWith({ cfg, agentId: "main" });
expect(runEmbeddedPiAgentMock).toHaveBeenCalledTimes(1);
const request = runEmbeddedPiAgentMock.mock.calls[0]?.[0];
expect(request.provider).toBe("openai-codex");
expect(request.model).toBe("gpt-5.5");
expect(request.disableTools).toBe(true);
expect(runEmbeddedPiAgentMock).toHaveBeenCalledWith(
expect.objectContaining({
provider: "openai-codex",
model: "gpt-5.5",
disableTools: true,
}),
);
});
it("backs off hidden extraction after terminal model or auth failures", async () => {

View File

@@ -2,7 +2,6 @@ import { randomUUID } from "node:crypto";
import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
import { runEmbeddedPiAgent, type EmbeddedPiRunResult } from "../agents/pi-embedded.js";
import type { OpenClawConfig } from "../config/config.js";
import { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import { resolveCommitmentTimezone, resolveCommitmentsConfig } from "./config.js";
@@ -180,10 +179,6 @@ function openTerminalFailureCooldown(agentId: string, error: unknown): void {
});
}
function resolveExtractionSessionFile(agentId: string, runId: string): string {
return createSqliteSessionTranscriptLocator({ agentId, sessionId: runId });
}
function joinPayloadText(result: EmbeddedPiRunResult): string {
return (
result.payloads
@@ -222,7 +217,6 @@ async function defaultExtractBatch(params: {
sessionKey: `agent:${first.agentId}:commitments:${runId}`,
agentId: first.agentId,
trigger: "manual",
sessionFile: resolveExtractionSessionFile(first.agentId, runId),
workspaceDir: resolveAgentWorkspaceDir(cfg, first.agentId),
config: cfg,
provider: modelRef.provider,

View File

@@ -183,7 +183,7 @@ async function runLocalRuntimePlanner(
try {
const runId = `crestodian-planner-${randomUUID()}`;
const sessionId = `${runId}-session`;
const sessionFile = createSqliteSessionTranscriptLocator({
const transcriptLocator = createSqliteSessionTranscriptLocator({
agentId: "crestodian",
sessionId,
});
@@ -196,7 +196,7 @@ async function runLocalRuntimePlanner(
sessionKey,
agentId: "crestodian",
trigger: "manual",
sessionFile,
transcriptLocator,
workspaceDir: tempDir,
config: backend.buildConfig(tempDir),
prompt: params.prompt,
@@ -220,7 +220,6 @@ async function runLocalRuntimePlanner(
sessionKey,
agentId: "crestodian",
trigger: "manual",
sessionFile,
workspaceDir: tempDir,
config: backend.buildConfig(tempDir),
prompt: params.prompt,

View File

@@ -199,7 +199,6 @@ export function createCronPromptExecutor(params: {
messageTo: params.resolvedDelivery.to,
messageThreadId: params.resolvedDelivery.threadId,
currentChannelId,
transcriptLocator,
agentDir: params.agentDir,
workspaceDir: params.workspaceDir,
config: params.cfgWithAgentDefaults,

View File

@@ -11,7 +11,6 @@ import {
import { resolveDefaultModelForAgent } from "../agents/model-selection.js";
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
import { resolveAgentTimeoutMs } from "../agents/timeout.js";
import { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
@@ -39,7 +38,6 @@ export async function generateSlugViaLLM(params: {
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, agentId);
const agentDir = resolveAgentDir(params.cfg, agentId);
const sessionId = `slug-generator-${randomUUID()}`;
const sessionFile = createSqliteSessionTranscriptLocator({ agentId, sessionId });
const prompt = `Based on this conversation, generate a short 1-2 word filename slug (lowercase, hyphen-separated, no file extension).
@@ -58,7 +56,6 @@ Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design",
sessionId,
sessionKey: "temp:slug-generator",
agentId,
sessionFile,
workspaceDir,
agentDir,
config: params.cfg,

View File

@@ -1,12 +1,12 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import type { RunEmbeddedPiAgentParams } from "../agents/pi-embedded-runner/run/params.js";
import { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js";
import {
__setRealtimeVoiceAgentConsultDepsForTest,
consultRealtimeVoiceAgent,
resolveRealtimeVoiceAgentConsultTools,
resolveRealtimeVoiceAgentConsultToolsAllow,
} from "./agent-consult-runtime.js";
import { REALTIME_VOICE_AGENT_CONSULT_TOOL } from "./agent-consult-tool.js";
import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME } from "./agent-consult-tool.js";
function createAgentRuntime(payloads: unknown[] = [{ text: "Speak this." }]) {
const sessionStore: Record<
@@ -14,7 +14,7 @@ function createAgentRuntime(payloads: unknown[] = [{ text: "Speak this." }]) {
{
sessionId?: string;
updatedAt?: number;
sessionFile?: string;
transcriptLocator?: string;
spawnedBy?: string;
forkedFromParent?: boolean;
totalTokens?: number;
@@ -88,36 +88,16 @@ function createAgentRuntime(payloads: unknown[] = [{ text: "Speak this." }]) {
};
}
function requireEmbeddedPiAgentCall(runEmbeddedPiAgent: {
mock: { calls: unknown[][] };
}): RunEmbeddedPiAgentParams {
const call = runEmbeddedPiAgent.mock.calls[0]?.[0] as RunEmbeddedPiAgentParams | undefined;
if (!call) {
throw new Error("Expected embedded PI agent call");
}
return call;
}
function expectPositiveTimestamp(value: unknown) {
expect(typeof value).toBe("number");
expect(value as number).toBeGreaterThan(0);
}
function expectNonEmptyString(value: unknown) {
expect(typeof value).toBe("string");
expect((value as string).trim()).not.toBe("");
}
describe("realtime voice agent consult runtime", () => {
afterEach(() => {
__setRealtimeVoiceAgentConsultDepsForTest(null);
});
it("exposes the shared consult tool based on policy", () => {
expect(resolveRealtimeVoiceAgentConsultTools("safe-read-only")).toStrictEqual([
REALTIME_VOICE_AGENT_CONSULT_TOOL,
expect(resolveRealtimeVoiceAgentConsultTools("safe-read-only")).toEqual([
expect.objectContaining({ name: REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME }),
]);
expect(resolveRealtimeVoiceAgentConsultTools("none")).toStrictEqual([]);
expect(resolveRealtimeVoiceAgentConsultTools("none")).toEqual([]);
expect(resolveRealtimeVoiceAgentConsultToolsAllow("safe-read-only")).toEqual([
"read",
"web_search",
@@ -127,7 +107,7 @@ describe("realtime voice agent consult runtime", () => {
"memory_get",
]);
expect(resolveRealtimeVoiceAgentConsultToolsAllow("owner")).toBeUndefined();
expect(resolveRealtimeVoiceAgentConsultToolsAllow("none")).toStrictEqual([]);
expect(resolveRealtimeVoiceAgentConsultToolsAllow("none")).toEqual([]);
});
it("runs an embedded agent using the shared session and prompt contract", async () => {
@@ -150,7 +130,6 @@ describe("realtime voice agent consult runtime", () => {
provider: "openai",
model: "gpt-5.4",
thinkLevel: "high",
fastMode: true,
timeoutMs: 10_000,
});
@@ -159,24 +138,23 @@ describe("realtime voice agent consult runtime", () => {
if (!voiceSession) {
throw new Error("Expected voice consult session entry");
}
expect(Object.keys(voiceSession).toSorted()).toStrictEqual(["sessionId", "updatedAt"]);
expectNonEmptyString(voiceSession.sessionId);
expectPositiveTimestamp(voiceSession.updatedAt);
const call = requireEmbeddedPiAgentCall(runEmbeddedPiAgent);
expect(call.sessionId).toBe(voiceSession.sessionId);
expect(call.sessionKey).toBe("voice:15550001234");
expect(call.sandboxSessionKey).toBe("agent:main:voice:15550001234");
expect(call.agentId).toBe("main");
expect(call.messageProvider).toBe("voice");
expect(call.lane).toBe("voice");
expect(call.toolsAllow).toStrictEqual(["read"]);
expect(call.provider).toBe("openai");
expect(call.model).toBe("gpt-5.4");
expect(call.thinkLevel).toBe("high");
expect(call.fastMode).toBe(true);
expect(call.timeoutMs).toBe(10_000);
expect(call.prompt).toContain("Caller: Can you check this?");
expect(call.extraSystemPrompt).toContain("delegated requests");
expect(voiceSession.sessionId).toEqual(expect.stringMatching(/\S/));
expect(runEmbeddedPiAgent).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: "voice:15550001234",
sandboxSessionKey: "agent:main:voice:15550001234",
agentId: "main",
messageProvider: "voice",
lane: "voice",
toolsAllow: ["read"],
provider: "openai",
model: "gpt-5.4",
thinkLevel: "high",
timeoutMs: 10_000,
prompt: expect.stringContaining("Caller: Can you check this?"),
extraSystemPrompt: expect.stringContaining("delegated requests"),
}),
);
});
it("scopes sandbox resolution to the configured consult agent", async () => {
@@ -197,10 +175,13 @@ describe("realtime voice agent consult runtime", () => {
userLabel: "Caller",
});
const call = requireEmbeddedPiAgentCall(runEmbeddedPiAgent);
expect(call.sessionKey).toBe("voice:15550001234");
expect(call.sandboxSessionKey).toBe("agent:voice:voice:15550001234");
expect(call.agentId).toBe("voice");
expect(runEmbeddedPiAgent).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: "voice:15550001234",
sandboxSessionKey: "agent:voice:voice:15550001234",
agentId: "voice",
}),
);
});
it("returns a speakable fallback when the embedded agent has no visible text", async () => {
@@ -232,7 +213,10 @@ describe("realtime voice agent consult runtime", () => {
const { runtime, runEmbeddedPiAgent, sessionStore } = createAgentRuntime();
sessionStore["agent:main:main"] = {
sessionId: "parent-session",
sessionFile: "/tmp/parent.jsonl",
transcriptLocator: createSqliteSessionTranscriptLocator({
agentId: "main",
sessionId: "parent-session",
}),
totalTokens: 100,
updatedAt: 1,
};
@@ -243,7 +227,7 @@ describe("realtime voice agent consult runtime", () => {
}));
const forkSessionFromParent = vi.fn(async () => ({
sessionId: "forked-session",
sessionFile: "sqlite-transcript://main/forked-session.jsonl",
transcriptLocator: "sqlite-transcript://main/forked-session",
}));
__setRealtimeVoiceAgentConsultDepsForTest({
resolveParentForkDecision,
@@ -275,19 +259,17 @@ describe("realtime voice agent consult runtime", () => {
parentEntry: sessionStore["agent:main:main"],
agentId: "main",
});
const forkedEntry = sessionStore["agent:main:subagent:google-meet:meet-1"];
if (!forkedEntry) {
throw new Error("Expected forked consult session entry");
}
expect(forkedEntry).toMatchObject({
expect(sessionStore["agent:main:subagent:google-meet:meet-1"]).toMatchObject({
sessionId: "forked-session",
spawnedBy: "agent:main:main",
forkedFromParent: true,
});
expectPositiveTimestamp(forkedEntry.updatedAt);
const call = requireEmbeddedPiAgentCall(runEmbeddedPiAgent);
expect(call.sessionId).toBe("forked-session");
expect(call.spawnedBy).toBe("agent:main:main");
expect(runEmbeddedPiAgent).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "forked-session",
spawnedBy: "agent:main:main",
}),
);
});
it("inherits requester message routing for forked consult sessions", async () => {
@@ -319,20 +301,17 @@ describe("realtime voice agent consult runtime", () => {
userLabel: "Caller",
});
const call = requireEmbeddedPiAgentCall(runEmbeddedPiAgent);
expect(call.sessionKey).toBe("voice:google-meet:meet-1");
expect(call.spawnedBy).toBe("agent:main:discord:channel:123");
expect(call.messageProvider).toBe("discord");
expect(call.agentAccountId).toBe("default");
expect(call.messageTo).toBe("channel:123");
expect(call.currentChannelId).toBe("channel:123");
const voiceEntry = sessionStore["voice:google-meet:meet-1"];
if (!voiceEntry) {
throw new Error("Expected voice consult session entry");
}
expect(voiceEntry).toStrictEqual({
sessionId: voiceEntry.sessionId,
spawnedBy: "agent:main:discord:channel:123",
expect(runEmbeddedPiAgent).toHaveBeenCalledWith(
expect.objectContaining({
sessionKey: "voice:google-meet:meet-1",
spawnedBy: "agent:main:discord:channel:123",
messageProvider: "discord",
agentAccountId: "default",
messageTo: "channel:123",
currentChannelId: "channel:123",
}),
);
expect(sessionStore["voice:google-meet:meet-1"]).toMatchObject({
deliveryContext: {
channel: "discord",
to: "channel:123",
@@ -341,11 +320,7 @@ describe("realtime voice agent consult runtime", () => {
lastChannel: "discord",
lastTo: "channel:123",
lastAccountId: "default",
lastThreadId: undefined,
updatedAt: voiceEntry.updatedAt,
});
expectNonEmptyString(voiceEntry.sessionId);
expectPositiveTimestamp(voiceEntry.updatedAt);
});
it("reuses the call session delivery context when requester metadata is absent", async () => {
@@ -376,14 +351,17 @@ describe("realtime voice agent consult runtime", () => {
userLabel: "Caller",
});
const call = requireEmbeddedPiAgentCall(runEmbeddedPiAgent);
expect(call.sessionId).toBe("call-session");
expect(call.sessionKey).toBe("voice:google-meet:meet-1");
expect(call.messageProvider).toBe("discord");
expect(call.agentAccountId).toBe("default");
expect(call.messageTo).toBe("channel:123");
expect(call.messageThreadId).toBe("thread-456");
expect(call.currentChannelId).toBe("channel:123");
expect(call.currentThreadTs).toBe("thread-456");
expect(runEmbeddedPiAgent).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: "call-session",
sessionKey: "voice:google-meet:meet-1",
messageProvider: "discord",
agentAccountId: "default",
messageTo: "channel:123",
messageThreadId: "thread-456",
currentChannelId: "channel:123",
currentThreadTs: "thread-456",
}),
);
});
});

View File

@@ -4,7 +4,6 @@ import {
forkSessionFromParent,
resolveParentForkDecision,
} from "../auto-reply/reply/session-fork.js";
import { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js";
import { parseSessionThreadInfoFast } from "../config/sessions/thread-info.js";
import type { SessionEntry } from "../config/sessions/types.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
@@ -245,10 +244,6 @@ export async function consultRealtimeVoiceAgent(params: {
resolvedDeliveryContext ?? deliveryContextFromSession(sessionEntry);
const sessionId = sessionEntry.sessionId;
const transcriptLocator = createSqliteSessionTranscriptLocator({
agentId,
sessionId,
});
const result = await params.agentRuntime.runEmbeddedPiAgent({
sessionId,
sessionKey: params.sessionKey,
@@ -264,7 +259,6 @@ export async function consultRealtimeVoiceAgent(params: {
consultDeliveryContext?.threadId != null
? String(consultDeliveryContext.threadId)
: undefined,
transcriptLocator,
workspaceDir,
config: params.cfg,
prompt: buildRealtimeVoiceAgentConsultPrompt({