refactor: remove transcript locator test helper

This commit is contained in:
Peter Steinberger
2026-05-09 17:55:44 +01:00
parent b1b6163e63
commit 880ad884a7
12 changed files with 56 additions and 116 deletions

View File

@@ -617,12 +617,12 @@ during the blocking memory sub-agent call.
By default, that transcript is internal:
- it uses a `sqlite-transcript://<agent>/<session>.jsonl` locator
- it is addressed by `{ agentId, sessionId }`
- it is used only for the blocking memory sub-agent run
- it does not create a JSONL sidecar
- it does not create a JSONL sidecar or transcript locator
If you want the blocking memory sub-agent transcript locator logged for debugging
or inspection, turn persistence on explicitly:
If you want the blocking memory sub-agent transcript retained for debugging or
inspection, turn persistence on explicitly:
```json5
{

View File

@@ -147,9 +147,9 @@ The main entry point is `runEmbeddedPiAgent()` in `pi-embedded-runner/run.ts`:
import { runEmbeddedPiAgent } from "./agents/pi-embedded-runner.js";
const result = await runEmbeddedPiAgent({
agentId: "main",
sessionId: "user-123",
sessionKey: "main:whatsapp:+1234567890",
sessionFile: "sqlite-transcript://main/user-123.jsonl",
workspaceDir: "/path/to/workspace",
config: openclawConfig,
prompt: "Hello, how are you?",
@@ -305,7 +305,10 @@ applySystemPromptOverrideToSession(session, systemPromptOverride);
Sessions are SQLite-backed event streams with tree structure (id/parentId linking). JSONL is legacy doctor-import/export/debug shape. OpenClaw owns the PI-compatible `SessionManager` shape behind `src/agents/transcript/session-transcript-contract.ts`:
```typescript
const sessionManager = openTranscriptSessionManager({ sessionFile: params.sessionFile });
const sessionManager = openTranscriptSessionManager({
agentId: params.agentId,
sessionId: params.sessionId,
});
```
OpenClaw wraps this with `guardSessionManager()` for tool result safety.
@@ -325,7 +328,7 @@ compaction:
```typescript
const compactResult = await compactEmbeddedPiSessionDirect({
sessionId, sessionFile, provider, model, ...
agentId, sessionId, provider, model, ...
});
```

View File

@@ -94,9 +94,9 @@ Provider and channel execution paths must use the active runtime config snapshot
const agentDir = api.runtime.agent.resolveAgentDir(cfg);
const sessionId = "my-plugin-task-1";
const result = await api.runtime.agent.runEmbeddedAgent({
agentId,
sessionId,
runId: crypto.randomUUID(),
sessionFile: createSqliteSessionTranscriptLocator({ agentId, sessionId }),
workspaceDir: api.runtime.agent.resolveAgentWorkspaceDir(cfg),
prompt: "Summarize the latest changes",
timeoutMs: api.runtime.agent.resolveAgentTimeoutMs(cfg),
@@ -114,8 +114,6 @@ Provider and channel execution paths must use the active runtime config snapshot
**SQLite session row helpers** are under `api.runtime.agent.session`:
```typescript
import { createSqliteSessionTranscriptLocator } from "openclaw/plugin-sdk/session-store-runtime";
const entry = api.runtime.agent.session.getSessionEntry({ agentId, sessionKey });
await api.runtime.agent.session.patchSessionEntry({
agentId,
@@ -125,7 +123,6 @@ Provider and channel execution paths must use the active runtime config snapshot
thinkingLevel: "high",
}),
});
const sessionFile = createSqliteSessionTranscriptLocator({ agentId, sessionId });
```
Prefer row helpers such as `getSessionEntry(...)`, `listSessionEntries(...)`, `patchSessionEntry(...)`, and `upsertSessionEntry(...)` for runtime writes. They route through the SQLite session row store and preserve concurrent updates. Legacy `sessions.json` parsing belongs in doctor import code, not plugin runtime paths.

View File

@@ -362,17 +362,12 @@ function assertAgentTurn() {
if (entry.modelOverride && entry.modelOverride !== modelRef) {
throw new Error(`unexpected session model override: ${entry.modelOverride}`);
}
if (typeof entry.sessionFile !== "string" || !entry.sessionFile.trim()) {
throw new Error(
`missing OpenClaw transcript key in SQLite session entry: ${entry.sessionFile}`,
);
}
const transcriptEvents = countAgentTranscriptEvents(sessionId);
if (transcriptEvents <= 0) {
throw new Error(`missing SQLite transcript events for ${sessionId}`);
}
const binding = readOpenClawStateKvJson("codex_app_server_thread_bindings", entry.sessionFile);
const binding = readOpenClawStateKvJson("codex_app_server_thread_bindings", sessionId);
if (binding.schemaVersion !== 1 || typeof binding.threadId !== "string") {
throw new Error(`invalid Codex app-server binding: ${JSON.stringify(binding)}`);
}

View File

@@ -1,5 +1,6 @@
import fs from "node:fs";
import path from "node:path";
import { DatabaseSync } from "node:sqlite";
const command = process.argv[2];
const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
@@ -24,6 +25,32 @@ function configPath() {
return process.env.OPENCLAW_CONFIG_PATH || path.join(stateDir(), "openclaw.json");
}
function agentDatabasePath(agentId = "main") {
return path.join(stateDir(), "agents", agentId, "agent", "openclaw-agent.sqlite");
}
function withSqliteDatabase(dbPath, callback) {
if (!fs.existsSync(dbPath)) {
throw new Error(`missing SQLite database: ${dbPath}`);
}
const db = new DatabaseSync(dbPath, { readOnly: true });
try {
return callback(db);
} finally {
db.close();
}
}
function readMainAgentTranscriptText() {
return withSqliteDatabase(agentDatabasePath("main"), (db) =>
db
.prepare("SELECT event_json FROM transcript_events ORDER BY session_id, seq")
.all()
.map((row) => String(row.event_json ?? ""))
.join("\n"),
);
}
function realPathMaybe(filePath) {
try {
return fs.realpathSync(filePath);
@@ -246,16 +273,9 @@ function assertAgentTurn() {
`live agent reply did not contain tool slug ${expected}:\nstdout=${stdout}\nstderr=${stderr}`,
);
}
const sessionsDir = path.join(stateDir(), "agents", "main", "sessions");
const sessionFiles = fs
.readdirSync(sessionsDir, { recursive: true })
.map((entry) => path.join(sessionsDir, String(entry)))
.filter((entry) => entry.endsWith(".jsonl") && fs.existsSync(entry));
const transcript = sessionFiles.map((file) => fs.readFileSync(file, "utf8")).join("\n");
const transcript = readMainAgentTranscriptText();
if (!transcript.includes(toolName) || !transcript.includes(expected)) {
throw new Error(
`session transcript did not show ${toolName} returning ${expected}; checked ${sessionFiles.join(", ")}`,
);
throw new Error(`SQLite session transcript did not show ${toolName} returning ${expected}`);
}
}

View File

@@ -1,7 +1,6 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { createSqliteSessionTranscriptLocator } from "../../src/config/sessions/paths.ts";
import { upsertSessionEntry } from "../../src/config/sessions/store.ts";
import { replaceSqliteSessionTranscriptEvents } from "../../src/config/sessions/transcript-store.sqlite.ts";
import { resolveOpenClawAgentSqlitePath } from "../../src/state/openclaw-agent-db.ts";
@@ -11,10 +10,6 @@ async function main() {
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || path.join(os.homedir(), ".openclaw");
const configPath =
process.env.OPENCLAW_CONFIG_PATH?.trim() || path.join(stateDir, "openclaw.json");
const transcriptPath = createSqliteSessionTranscriptLocator({
agentId: "main",
sessionId: "sess-main",
});
const now = Date.now();
await fs.mkdir(path.dirname(configPath), { recursive: true });
@@ -48,7 +43,6 @@ async function main() {
sessionKey: "agent:main:main",
entry: {
sessionId: "sess-main",
sessionFile: transcriptPath,
updatedAt: now,
deliveryContext: {
channel: "imessage",
@@ -65,7 +59,6 @@ async function main() {
replaceSqliteSessionTranscriptEvents({
agentId: "main",
sessionId: "sess-main",
transcriptPath,
now: () => now,
events: [
{ type: "session", version: 1, id: "sess-main" },

View File

@@ -4,7 +4,6 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { SessionEntry } from "../config/sessions.js";
import { listSessionEntries, upsertSessionEntry } from "../config/sessions/store.js";
import { createSqliteSessionTranscriptLocator } from "../config/sessions/test-helpers/transcript-locator.js";
import { replaceSqliteSessionTranscriptEvents } from "../config/sessions/transcript-store.sqlite.js";
import { callGateway } from "../gateway/call.js";
import { closeOpenClawAgentDatabasesForTest } from "../state/openclaw-agent-db.js";

View File

@@ -2,19 +2,18 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { expect, vi, type Mock } from "vitest";
import { createSqliteSessionTranscriptLocator } from "../../../config/sessions/test-helpers/transcript-locator.js";
import type {
AssembleResult,
BootstrapResult,
CompactResult,
ContextEngineInfo,
ContextEngineMaintenanceResult,
ContextEngineTranscriptScope,
IngestBatchResult,
IngestResult,
} from "../../../context-engine/types.js";
import { formatErrorMessage } from "../../../infra/errors.js";
import type { PluginMetadataSnapshot } from "../../../plugins/plugin-metadata-snapshot.types.js";
import { DEFAULT_AGENT_ID, resolveAgentIdFromSessionKey } from "../../../routing/session-key.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
@@ -1039,14 +1038,14 @@ export async function createContextEngineAttemptRunner(params: {
bootstrap?: (params: {
sessionId: string;
sessionKey?: string;
transcriptLocator: string;
transcriptScope?: ContextEngineTranscriptScope;
}) => Promise<BootstrapResult>;
maintain?:
| boolean
| ((params: {
sessionId: string;
sessionKey?: string;
transcriptLocator: string;
transcriptScope?: ContextEngineTranscriptScope;
runtimeContext?: Record<string, unknown>;
}) => Promise<{
changed: boolean;
@@ -1064,7 +1063,7 @@ export async function createContextEngineAttemptRunner(params: {
afterTurn?: (params: {
sessionId: string;
sessionKey?: string;
transcriptLocator: string;
transcriptScope?: ContextEngineTranscriptScope;
messages: AgentMessage[];
prePromptMessageCount: number;
tokenBudget?: number;
@@ -1083,7 +1082,7 @@ export async function createContextEngineAttemptRunner(params: {
compact?: (params: {
sessionId: string;
sessionKey?: string;
transcriptLocator: string;
transcriptScope?: ContextEngineTranscriptScope;
tokenBudget?: number;
}) => Promise<CompactResult>;
info?: Partial<ContextEngineInfo>;
@@ -1100,10 +1099,6 @@ 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 transcriptLocator = createSqliteSessionTranscriptLocator({
agentId: resolveAgentIdFromSessionKey(params.sessionKey) ?? DEFAULT_AGENT_ID,
sessionId,
});
params.tempPaths.push(workspaceDir, agentDir, stateDir);
const seedMessages: AgentMessage[] =
params.sessionMessages ?? ([{ role: "user", content: "seed", timestamp: 1 }] as AgentMessage[]);

View File

@@ -1,63 +0,0 @@
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../../../routing/session-key.js";
import { validateSessionId } from "../paths.js";
export const SQLITE_SESSION_TRANSCRIPT_LOCATOR_PREFIX = "sqlite-transcript://";
export function createSqliteSessionTranscriptLocator(params: {
agentId?: string;
sessionId: string;
topicId?: string | number;
}): string {
const agentId = normalizeAgentId(params.agentId ?? DEFAULT_AGENT_ID);
const sessionId = validateSessionId(params.sessionId);
const safeTopicId =
typeof params.topicId === "string"
? encodeURIComponent(params.topicId)
: typeof params.topicId === "number"
? String(params.topicId)
: undefined;
const topicSuffix = safeTopicId !== undefined ? `?topic=${safeTopicId}` : "";
return `${SQLITE_SESSION_TRANSCRIPT_LOCATOR_PREFIX}${encodeURIComponent(
agentId,
)}/${encodeURIComponent(sessionId)}${topicSuffix}`;
}
export function parseSqliteSessionTranscriptLocator(locator: string):
| {
agentId: string;
sessionId: string;
topicId?: string;
}
| undefined {
const trimmed = locator.trim();
if (!trimmed.startsWith(SQLITE_SESSION_TRANSCRIPT_LOCATOR_PREFIX)) {
return undefined;
}
try {
const url = new URL(trimmed);
const agentId = decodeURIComponent(url.hostname).trim();
const rawPath = decodeURIComponent(url.pathname.replace(/^\/+/u, "")).trim();
if (!rawPath) {
return undefined;
}
const topicId = url.searchParams.get("topic") ?? undefined;
return {
agentId: normalizeAgentId(agentId),
sessionId: validateSessionId(rawPath),
...(topicId ? { topicId } : {}),
};
} catch {
return undefined;
}
}
export function isSqliteSessionTranscriptLocator(locator: string | undefined): boolean {
return typeof locator === "string" && parseSqliteSessionTranscriptLocator(locator) !== undefined;
}
export function resolveSessionTranscriptLocator(
sessionId: string,
opts?: { agentId?: string },
): string {
return createSqliteSessionTranscriptLocator({ agentId: opts?.agentId, sessionId });
}

View File

@@ -7,7 +7,6 @@ import {
openOpenClawAgentDatabase,
} from "../../state/openclaw-agent-db.js";
import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js";
import { createSqliteSessionTranscriptLocator } from "./test-helpers/transcript-locator.js";
import {
appendSqliteSessionTranscriptEvent,
appendSqliteSessionTranscriptMessage,
@@ -31,7 +30,6 @@ afterEach(() => {
describe("SQLite session transcript store", () => {
it("appends transcript events with stable per-session sequence numbers", () => {
const stateDir = createTempDir();
const transcriptPath = path.join(stateDir, "session.jsonl");
expect(
appendSqliteSessionTranscriptEvent({

View File

@@ -7,7 +7,6 @@ import { closeOpenClawAgentDatabasesForTest } from "../../state/openclaw-agent-d
import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js";
import { upsertSessionEntry } from "./store.js";
import { useTempSessionsFixture } from "./test-helpers.js";
import { createSqliteSessionTranscriptLocator } from "./test-helpers/transcript-locator.js";
import { appendSessionTranscriptMessage } from "./transcript-append.js";
import {
appendSqliteSessionTranscriptEvent,

View File

@@ -99,26 +99,30 @@ describe("runCronIsolatedAgentTurn session identity", () => {
expect(res.status).toBe("ok");
const call = runEmbeddedPiAgentMock.mock.calls.at(-1)?.[0] as {
agentId?: string;
sessionId?: string;
sessionKey?: string;
workspaceDir?: string;
sessionFile?: string;
};
expect(call?.agentId).toBe("ops");
expect(call?.sessionId).toBe(res.sessionId);
expect(call?.sessionKey).toMatch(/^agent:ops:cron:job-ops:run:/);
expect(call?.workspaceDir).toBe(opsWorkspace);
expect(call?.sessionFile).toMatch(/^sqlite-transcript:\/\/ops\/.+\.jsonl$/u);
});
});
it("passes sessionFile to isolated cron runs", async () => {
it("passes session identity to isolated cron runs", async () => {
await withTempHome(async (home) => {
await runCronTurn(home, {
const { res } = await runCronTurn(home, {
jobPayload: DEFAULT_AGENT_TURN_PAYLOAD,
});
const call = runEmbeddedPiAgentMock.mock.calls.at(-1)?.[0] as {
sessionFile?: string;
agentId?: string;
sessionId?: string;
};
expect(call?.sessionFile).toMatch(/^sqlite-transcript:\/\/main\/.+\.jsonl$/u);
expect(call?.agentId).toBe("main");
expect(call?.sessionId).toBe(res.sessionId);
});
});