mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-21 21:56:46 +00:00
refactor: remove transcript locator test helper
This commit is contained in:
@@ -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
|
||||
{
|
||||
|
||||
@@ -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, ...
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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[]);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user