refactor: keep embedded runner diagnostics in sqlite

This commit is contained in:
Peter Steinberger
2026-05-09 08:19:04 +01:00
parent b07f0e7102
commit c73808c4d4
22 changed files with 101 additions and 142 deletions

View File

@@ -1038,7 +1038,6 @@ Notes:
cacheTrace: {
enabled: false,
filePath: "~/Desktop/cache-trace.jsonl",
includeMessages: true,
includePrompt: true,
includeSystem: true,
@@ -1064,8 +1063,7 @@ Notes:
- `OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental`: environment toggle for latest experimental GenAI span provider attributes. By default spans keep the legacy `gen_ai.system` attribute for compatibility; GenAI metrics use bounded semantic attributes.
- `OPENCLAW_OTEL_PRELOADED=1`: environment toggle for hosts that already registered a global OpenTelemetry SDK. OpenClaw then skips plugin-owned SDK startup/shutdown while keeping diagnostic listeners active.
- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`, `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`, and `OTEL_EXPORTER_OTLP_LOGS_ENDPOINT`: signal-specific endpoint env vars used when the matching config key is unset.
- `cacheTrace.enabled`: log cache trace snapshots for embedded runs (default: `false`).
- `cacheTrace.filePath`: optional JSONL export/debug path. When unset, cache trace events are stored in the SQLite state database.
- `cacheTrace.enabled`: store cache trace snapshots for embedded runs in the SQLite state database (default: `false`).
- `cacheTrace.includeMessages` / `includePrompt` / `includeSystem`: control what is included in cache trace output (all default: `true`).
---

View File

@@ -49,6 +49,13 @@ This migration has one canonical runtime shape:
- 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.
- 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.
Implementation work should keep deleting code until these statements are true
without exceptions outside doctor/import/export/debug boundaries.
@@ -322,6 +329,14 @@ The remaining cleanup is mostly consolidation and deletion:
`resolveSessionTranscriptTarget` derives any temporary boundary handle from
`agentId`, `sessionId`, and optional topic metadata; doctor is the only code
that imports legacy transcript file names.
- Embedded PI runs canonicalize any incoming transcript handle to the SQLite
`{agentId, sessionId}` identity before worker launch and again before the
attempt touches transcript state. A stale `/tmp/*.jsonl` input cannot select a
runtime write target.
- Cache trace and Anthropic payload diagnostics now write to SQLite diagnostic
KV rows only. The old `diagnostics.cacheTrace.filePath`,
`OPENCLAW_CACHE_TRACE_FILE`, and `OPENCLAW_ANTHROPIC_PAYLOAD_LOG_FILE`
JSONL override paths are removed.
- Cron persistence now reconciles SQLite `cron_jobs` rows instead of
deleting/reinserting the whole job table on each save. Plugin target
writebacks update matching cron rows directly and keep runtime cron state in

View File

@@ -308,15 +308,12 @@ Why the assertions differ:
diagnostics:
cacheTrace:
enabled: true
filePath: "~/Desktop/cache-trace.jsonl" # optional export/debug override
includeMessages: false # default true
includePrompt: false # default true
includeSystem: false # default true
```
Defaults:
- `filePath`: unset by default; cache trace events are stored in the SQLite state database. Set this only when you need an explicit JSONL export/debug file.
- Cache trace events are stored in the SQLite state database.
- `includeMessages`: `true`
- `includePrompt`: `true`
- `includeSystem`: `true`
@@ -324,7 +321,6 @@ Defaults:
### Env toggles (one-off debugging)
- `OPENCLAW_CACHE_TRACE=1` enables cache tracing.
- `OPENCLAW_CACHE_TRACE_FILE=/path/to/cache-trace.jsonl` writes an explicit JSONL export/debug file instead of the SQLite diagnostic store.
- `OPENCLAW_CACHE_TRACE_MESSAGES=0|1` toggles full message payload capture.
- `OPENCLAW_CACHE_TRACE_PROMPT=0|1` toggles prompt text capture.
- `OPENCLAW_CACHE_TRACE_SYSTEM=0|1` toggles system prompt capture.

View File

@@ -1,13 +1,11 @@
import crypto from "node:crypto";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { resolveUserPath } from "../utils.js";
import { parseBooleanValue } from "../utils/boolean.js";
import { safeJsonStringify } from "../utils/safe-json.js";
import type { AgentMessage, StreamFn } from "./agent-core-contract.js";
import { sanitizeDiagnosticPayload } from "./payload-redaction.js";
import type { Api, Model } from "./pi-ai-contract.js";
import { getQueuedFileWriter, type QueuedFileWriter } from "./queued-file-writer.js";
import { getStateDiagnosticWriter } from "./state-diagnostic-writer.js";
import { getStateDiagnosticWriter, type StateDiagnosticWriter } from "./state-diagnostic-writer.js";
type PayloadLogStage = "request" | "usage";
@@ -30,12 +28,10 @@ type PayloadLogEvent = {
type PayloadLogConfig = {
enabled: boolean;
filePath: string;
fileOverride: boolean;
};
type PayloadLogWriter = QueuedFileWriter;
type PayloadLogWriter = StateDiagnosticWriter;
const writers = new Map<string, PayloadLogWriter>();
const stateWriters = new Map<string, PayloadLogWriter>();
const log = createSubsystemLogger("agent/anthropic-payload");
const ANTHROPIC_PAYLOAD_SQLITE_LABEL = "sqlite://state/diagnostics/anthropic-payload";
@@ -43,15 +39,10 @@ const ANTHROPIC_PAYLOAD_SQLITE_SCOPE = "diagnostics.anthropic_payload";
function resolvePayloadLogConfig(env: NodeJS.ProcessEnv): PayloadLogConfig {
const enabled = parseBooleanValue(env.OPENCLAW_ANTHROPIC_PAYLOAD_LOG) ?? false;
const fileOverride = env.OPENCLAW_ANTHROPIC_PAYLOAD_LOG_FILE?.trim();
const filePath = fileOverride ? resolveUserPath(fileOverride) : ANTHROPIC_PAYLOAD_SQLITE_LABEL;
return { enabled, filePath, fileOverride: Boolean(fileOverride) };
return { enabled, filePath: ANTHROPIC_PAYLOAD_SQLITE_LABEL };
}
function getWriter(cfg: PayloadLogConfig, env: NodeJS.ProcessEnv): PayloadLogWriter {
if (cfg.fileOverride) {
return getQueuedFileWriter(writers, cfg.filePath);
}
return getStateDiagnosticWriter(stateWriters, {
env,
label: cfg.filePath,

View File

@@ -6,7 +6,6 @@ import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import { listOpenClawStateKvJson } from "../state/openclaw-state-kv.js";
import { resolveUserPath } from "../utils.js";
import { createCacheTrace } from "./cache-trace.js";
describe("createCacheTrace", () => {
@@ -39,14 +38,13 @@ describe("createCacheTrace", () => {
expect(trace).toBeNull();
});
it("honors diagnostics cache trace config and expands file paths", () => {
it("stores diagnostics cache trace output in SQLite state", () => {
const lines: string[] = [];
const trace = createCacheTrace({
cfg: {
diagnostics: {
cacheTrace: {
enabled: true,
filePath: "~/.openclaw/logs/cache-trace.jsonl",
},
},
},
@@ -59,7 +57,7 @@ describe("createCacheTrace", () => {
});
expect(typeof trace?.recordStage).toBe("function");
expect(trace?.filePath).toBe(resolveUserPath("~/.openclaw/logs/cache-trace.jsonl"));
expect(trace?.filePath).toBe("sqlite://state/diagnostics/cache-trace");
trace?.recordStage("session:loaded", {
messages: [],

View File

@@ -1,12 +1,10 @@
import crypto from "node:crypto";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveUserPath } from "../utils.js";
import { parseBooleanValue } from "../utils/boolean.js";
import { safeJsonStringify } from "../utils/safe-json.js";
import type { AgentMessage, StreamFn } from "./agent-core-contract.js";
import { sanitizeDiagnosticPayload } from "./payload-redaction.js";
import { getQueuedFileWriter, type QueuedFileWriter } from "./queued-file-writer.js";
import { getStateDiagnosticWriter } from "./state-diagnostic-writer.js";
import { getStateDiagnosticWriter, type StateDiagnosticWriter } from "./state-diagnostic-writer.js";
import { buildAgentTraceBase } from "./trace-base.js";
type CacheTraceStage =
@@ -69,15 +67,13 @@ type CacheTraceInit = {
type CacheTraceConfig = {
enabled: boolean;
filePath: string;
fileOverride: boolean;
includeMessages: boolean;
includePrompt: boolean;
includeSystem: boolean;
};
type CacheTraceWriter = QueuedFileWriter;
type CacheTraceWriter = StateDiagnosticWriter;
const writers = new Map<string, CacheTraceWriter>();
const stateWriters = new Map<string, CacheTraceWriter>();
const CACHE_TRACE_SQLITE_LABEL = "sqlite://state/diagnostics/cache-trace";
const CACHE_TRACE_SQLITE_SCOPE = "diagnostics.cache_trace";
@@ -87,8 +83,6 @@ function resolveCacheTraceConfig(params: CacheTraceInit): CacheTraceConfig {
const config = params.cfg?.diagnostics?.cacheTrace;
const envEnabled = parseBooleanValue(env.OPENCLAW_CACHE_TRACE);
const enabled = envEnabled ?? config?.enabled ?? false;
const fileOverride = config?.filePath?.trim() || env.OPENCLAW_CACHE_TRACE_FILE?.trim();
const filePath = fileOverride ? resolveUserPath(fileOverride) : CACHE_TRACE_SQLITE_LABEL;
const includeMessages =
parseBooleanValue(env.OPENCLAW_CACHE_TRACE_MESSAGES) ?? config?.includeMessages;
@@ -97,8 +91,7 @@ function resolveCacheTraceConfig(params: CacheTraceInit): CacheTraceConfig {
return {
enabled,
filePath,
fileOverride: Boolean(fileOverride),
filePath: CACHE_TRACE_SQLITE_LABEL,
includeMessages: includeMessages ?? true,
includePrompt: includePrompt ?? true,
includeSystem: includeSystem ?? true,
@@ -106,9 +99,6 @@ function resolveCacheTraceConfig(params: CacheTraceInit): CacheTraceConfig {
}
function getWriter(cfg: CacheTraceConfig, env: NodeJS.ProcessEnv): CacheTraceWriter {
if (cfg.fileOverride) {
return getQueuedFileWriter(writers, cfg.filePath);
}
return getStateDiagnosticWriter(stateWriters, {
env,
label: cfg.filePath,

View File

@@ -5,6 +5,8 @@ import { Type } from "typebox";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js";
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import { listOpenClawStateKvJson } from "../state/openclaw-state-kv.js";
import {
buildAssistantHistoryTurn as buildTypedAssistantHistoryTurn,
buildStableCachePrefix,
@@ -65,12 +67,11 @@ const NOOP_TOOL: Tool = {
};
let liveTestPngBase64 = "";
let liveRunnerRootDir: string | undefined;
let liveCacheTraceFile: string | undefined;
let previousCacheTraceEnv: {
enabled?: string;
file?: string;
messages?: string;
prompt?: string;
stateDir?: string;
system?: string;
} | null = null;
@@ -118,21 +119,9 @@ function resolveProviderBaseUrl(model: LiveResolvedModel["model"]): string | und
}
async function readCacheTraceEvents(sessionId: string): Promise<CacheTraceEvent[]> {
if (!liveCacheTraceFile) {
throw new Error("live cache trace file not initialized");
}
const raw = await fs.readFile(liveCacheTraceFile, "utf8").catch(() => "");
const events: CacheTraceEvent[] = [];
for (const rawLine of raw.split("\n")) {
const line = rawLine.trim();
if (line.length > 0) {
const event = JSON.parse(line) as CacheTraceEvent;
if (event.sessionId === sessionId) {
events.push(event);
}
}
}
return events;
return listOpenClawStateKvJson<CacheTraceEvent>("diagnostics.cache_trace")
.map((entry) => entry.value)
.filter((event) => event.sessionId === sessionId);
}
async function expectCacheTraceStages(
@@ -756,19 +745,18 @@ async function runAnthropicImageCacheProbe(params: {
describeCacheLive("pi embedded runner prompt caching (live)", () => {
beforeAll(async () => {
liveRunnerRootDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-live-cache-"));
liveCacheTraceFile = path.join(liveRunnerRootDir, "cache-trace.jsonl");
liveTestPngBase64 = (await fs.readFile(LIVE_TEST_PNG_URL)).toString("base64");
previousCacheTraceEnv = {
enabled: process.env.OPENCLAW_CACHE_TRACE,
file: process.env.OPENCLAW_CACHE_TRACE_FILE,
messages: process.env.OPENCLAW_CACHE_TRACE_MESSAGES,
prompt: process.env.OPENCLAW_CACHE_TRACE_PROMPT,
stateDir: process.env.OPENCLAW_STATE_DIR,
system: process.env.OPENCLAW_CACHE_TRACE_SYSTEM,
};
process.env.OPENCLAW_CACHE_TRACE = "1";
process.env.OPENCLAW_CACHE_TRACE_FILE = liveCacheTraceFile;
process.env.OPENCLAW_CACHE_TRACE_MESSAGES = "0";
process.env.OPENCLAW_CACHE_TRACE_PROMPT = "0";
process.env.OPENCLAW_STATE_DIR = path.join(liveRunnerRootDir, "state");
process.env.OPENCLAW_CACHE_TRACE_SYSTEM = "0";
}, 120_000);
@@ -777,9 +765,9 @@ describeCacheLive("pi embedded runner prompt caching (live)", () => {
const restore = (
key:
| "OPENCLAW_CACHE_TRACE"
| "OPENCLAW_CACHE_TRACE_FILE"
| "OPENCLAW_CACHE_TRACE_MESSAGES"
| "OPENCLAW_CACHE_TRACE_PROMPT"
| "OPENCLAW_STATE_DIR"
| "OPENCLAW_CACHE_TRACE_SYSTEM",
value: string | undefined,
) => {
@@ -790,13 +778,13 @@ describeCacheLive("pi embedded runner prompt caching (live)", () => {
}
};
restore("OPENCLAW_CACHE_TRACE", previousCacheTraceEnv.enabled);
restore("OPENCLAW_CACHE_TRACE_FILE", previousCacheTraceEnv.file);
restore("OPENCLAW_CACHE_TRACE_MESSAGES", previousCacheTraceEnv.messages);
restore("OPENCLAW_CACHE_TRACE_PROMPT", previousCacheTraceEnv.prompt);
restore("OPENCLAW_STATE_DIR", previousCacheTraceEnv.stateDir);
restore("OPENCLAW_CACHE_TRACE_SYSTEM", previousCacheTraceEnv.system);
}
closeOpenClawStateDatabaseForTest();
previousCacheTraceEnv = null;
liveCacheTraceFile = undefined;
if (liveRunnerRootDir) {
await fs.rm(liveRunnerRootDir, { recursive: true, force: true });
}

View File

@@ -4,6 +4,7 @@ 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,
@@ -468,6 +469,18 @@ export async function runEmbeddedPiAgent(
if (effectiveSessionKey !== params.sessionKey) {
params = { ...params, sessionKey: effectiveSessionKey };
}
const { sessionAgentId } = resolveSessionAgentIds({
sessionKey: params.sessionKey,
config: params.config,
agentId: params.agentId,
});
const sqliteTranscriptLocator = createSqliteSessionTranscriptLocator({
agentId: sessionAgentId,
sessionId: params.sessionId,
});
if (params.transcriptLocator !== sqliteTranscriptLocator) {
params = { ...params, transcriptLocator: sqliteTranscriptLocator };
}
const sessionLane = resolveSessionLane(params.sessionKey?.trim() || params.sessionId);
const globalLane = resolveGlobalLane(params.lane);
const laneTaskTimeoutMs = resolveEmbeddedRunLaneTimeoutMs(params.timeoutMs);

View File

@@ -28,7 +28,7 @@ function makeParams(): RunEmbeddedPiAgentParams {
model: "gpt-5.5",
prompt: "hello",
runId: "run-1",
sessionFile: "sqlite-transcript://agent-1/session-1.jsonl",
transcriptLocator: "/tmp/legacy-session.jsonl",
sessionId: "session-1",
sessionKey: "session-key-1",
timeoutMs: 1_000,
@@ -86,6 +86,7 @@ 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,
@@ -94,6 +95,7 @@ describe("runEmbeddedPiAgent worker launch", () => {
expect.objectContaining({
runId: "run-1",
sessionId: "session-1",
transcriptLocator: "sqlite-transcript://agent-1/session-1",
}),
{
runtimeId: "pi",

View File

@@ -5,6 +5,7 @@ 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,
@@ -725,6 +726,13 @@ 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,

View File

@@ -195,11 +195,7 @@ export async function repairHeartbeatPoisonedMainSession(params: {
}
let transcriptPath: string | undefined;
try {
transcriptPath = resolveSessionTranscriptLocator(
mainEntry.sessionId,
undefined,
params.sessionPathOpts,
);
transcriptPath = resolveSessionTranscriptLocator(mainEntry.sessionId, params.sessionPathOpts);
} catch {
transcriptPath = undefined;
}

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { HEARTBEAT_TRANSCRIPT_PROMPT } from "../auto-reply/heartbeat.js";
import type { OpenClawConfig } from "../config/config.js";
import { createSqliteSessionTranscriptLocator } from "../config/sessions/paths.js";
import {
deleteSessionEntry,
listSessionEntries,
@@ -734,27 +735,25 @@ describe("doctor state integrity oauth dir checks", () => {
});
it("keeps the heartbeat main-session helper conservative", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-heartbeat-main-helper-"));
try {
const transcriptPath = path.join(tempDir, "session.jsonl");
replaceSqliteSessionTranscriptEvents({
agentId: "main",
sessionId: "session",
transcriptPath,
events: [
{ message: { role: "user", content: HEARTBEAT_TRANSCRIPT_PROMPT } },
{ message: { role: "assistant", content: "HEARTBEAT_OK" } },
],
});
const entry: SessionEntry = { sessionId: "session", updatedAt: 1 };
expect(resolveHeartbeatMainSessionRepairCandidate({ entry, transcriptPath })?.reason).toBe(
"transcript",
);
entry.lastInteractionAt = 2;
expect(resolveHeartbeatMainSessionRepairCandidate({ entry, transcriptPath })).toBeNull();
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
const transcriptPath = createSqliteSessionTranscriptLocator({
agentId: "main",
sessionId: "session",
});
replaceSqliteSessionTranscriptEvents({
agentId: "main",
sessionId: "session",
transcriptPath,
events: [
{ message: { role: "user", content: HEARTBEAT_TRANSCRIPT_PROMPT } },
{ message: { role: "assistant", content: "HEARTBEAT_OK" } },
],
});
const entry: SessionEntry = { sessionId: "session", updatedAt: 1 };
expect(resolveHeartbeatMainSessionRepairCandidate({ entry, transcriptPath })).toMatchObject({
reason: "transcript",
});
entry.lastInteractionAt = 2;
expect(resolveHeartbeatMainSessionRepairCandidate({ entry, transcriptPath })).toBeNull();
});
it("moves store entries and clears matching TUI pointers without touching others", async () => {

View File

@@ -884,7 +884,7 @@ export async function noteStateIntegrity(
if (!sessionId) {
return false;
}
const transcriptPath = resolveSessionTranscriptLocator(sessionId, entry, sessionPathOpts);
const transcriptPath = resolveSessionTranscriptLocator(sessionId, sessionPathOpts);
return !hasSessionTranscript({ agentId, sessionId, transcriptPath });
});
if (missing.length > 0) {
@@ -972,11 +972,7 @@ export async function noteStateIntegrity(
const mainKey = resolveMainSessionKey(cfg);
const mainEntry = store[mainKey];
if (mainEntry?.sessionId) {
const transcriptPath = resolveSessionTranscriptLocator(
mainEntry.sessionId,
mainEntry,
sessionPathOpts,
);
const transcriptPath = resolveSessionTranscriptLocator(mainEntry.sessionId, sessionPathOpts);
if (!hasSessionTranscript({ agentId, sessionId: mainEntry.sessionId, transcriptPath })) {
warnings.push(
`- Main session transcript missing (${shortenHomePath(transcriptPath)}). History will appear to reset.`,
@@ -1005,7 +1001,7 @@ export async function noteStateIntegrity(
try {
referencedTranscriptPaths.add(
resolveComparableTranscriptPath(
resolveSessionTranscriptLocator(entry.sessionId, entry, sessionPathOpts),
resolveSessionTranscriptLocator(entry.sessionId, sessionPathOpts),
),
);
} catch {

View File

@@ -648,9 +648,7 @@ export const FIELD_HELP: Record<string, string> = {
"diagnostics.otel.captureContent.systemPrompt":
"Capture system prompt text on OTEL spans when content capture is enabled. This remains off unless explicitly enabled.",
"diagnostics.cacheTrace.enabled":
"Log cache trace snapshots for embedded agent runs (default: false).",
"diagnostics.cacheTrace.filePath":
"Optional JSONL export path for cache trace logs. When unset, cache trace events are stored in SQLite state.",
"Store cache trace snapshots for embedded agent runs in SQLite state (default: false).",
"diagnostics.cacheTrace.includeMessages":
"Include full message payloads in trace output (default: true).",
"diagnostics.cacheTrace.includePrompt": "Include prompt text in trace output (default: true).",

View File

@@ -63,7 +63,6 @@ export const FIELD_LABELS: Record<string, string> = {
"diagnostics.otel.captureContent.toolOutputs": "OpenTelemetry Tool Outputs Capture",
"diagnostics.otel.captureContent.systemPrompt": "OpenTelemetry System Prompt Capture",
"diagnostics.cacheTrace.enabled": "Cache Trace Enabled",
"diagnostics.cacheTrace.filePath": "Cache Trace File Path",
"diagnostics.cacheTrace.includeMessages": "Cache Trace Include Messages",
"diagnostics.cacheTrace.includePrompt": "Cache Trace Include Prompt",
"diagnostics.cacheTrace.includeSystem": "Cache Trace Include System",

View File

@@ -600,27 +600,21 @@ describe("sessions", () => {
expect(entry.lastProvider).toBeUndefined();
});
it("uses agent id when resolving session file fallback paths", () => {
it("uses agent id when resolving transcript locator fallback paths", () => {
withStateDir("/custom/state", () => {
const sessionFile = resolveSessionTranscriptLocator("sess-2", undefined, {
const transcriptLocator = resolveSessionTranscriptLocator("sess-2", {
agentId: "codex",
});
expect(sessionFile).toBe(
expect(transcriptLocator).toBe(
createSqliteSessionTranscriptLocator({ agentId: "codex", sessionId: "sess-2" }),
);
});
});
it("does not reuse legacy cross-agent absolute sessionFile paths", () => {
it("derives transcript locators from the requested agent", () => {
withStateDir(path.resolve("/different/state"), () => {
const originalBase = path.resolve("/original/state");
const bot2Session = path.join(originalBase, "agents", "bot2", "sessions", "sess-1.jsonl");
const sessionFile = resolveSessionTranscriptLocator(
"sess-1",
{ sessionFile: bot2Session },
{ agentId: "bot1" },
);
expect(sessionFile).toBe(
const transcriptLocator = resolveSessionTranscriptLocator("sess-1", { agentId: "bot1" });
expect(transcriptLocator).toBe(
createSqliteSessionTranscriptLocator({ agentId: "bot1", sessionId: "sess-1" }),
);
});
@@ -640,33 +634,21 @@ describe("sessions", () => {
});
});
it("keeps matching SQLite transcript locators", () => {
it("derives stable matching SQLite transcript locators", () => {
withStateDir(path.resolve("/different/state"), () => {
const locator = createSqliteSessionTranscriptLocator({
agentId: "bot1",
sessionId: "sess-1",
});
const sessionFile = resolveSessionTranscriptLocator(
"sess-1",
{ sessionFile: locator },
{ agentId: "bot1" },
);
expect(sessionFile).toBe(locator);
const transcriptLocator = resolveSessionTranscriptLocator("sess-1", { agentId: "bot1" });
expect(transcriptLocator).toBe(locator);
});
});
it("does not reuse SQLite transcript locators for a different agent", () => {
it("does not consult a previous SQLite transcript locator for a different agent", () => {
withStateDir(path.resolve("/different/state"), () => {
const bot2Locator = createSqliteSessionTranscriptLocator({
agentId: "bot2",
sessionId: "sess-1",
});
const sessionFile = resolveSessionTranscriptLocator(
"sess-1",
{ sessionFile: bot2Locator },
{ agentId: "bot1" },
);
expect(sessionFile).toBe(
const transcriptLocator = resolveSessionTranscriptLocator("sess-1", { agentId: "bot1" });
expect(transcriptLocator).toBe(
createSqliteSessionTranscriptLocator({ agentId: "bot1", sessionId: "sess-1" }),
);
});

View File

@@ -73,9 +73,7 @@ export function isSqliteSessionTranscriptLocator(locator: string | undefined): b
export function resolveSessionTranscriptLocator(
sessionId: string,
entry?: unknown,
opts?: SessionTranscriptLocatorOptions,
): string {
void entry;
return createSqliteSessionTranscriptLocator({ agentId: opts?.agentId, sessionId });
}

View File

@@ -38,18 +38,14 @@ describe("session path safety", () => {
});
it("ignores invalid transcript locators", () => {
const resolved = resolveSessionTranscriptLocator("sess-1", {
transcriptLocator: "not-a-transcript-locator",
});
const resolved = resolveSessionTranscriptLocator("sess-1");
expect(resolved).toBe(createSqliteSessionTranscriptLocator({ sessionId: "sess-1" }));
});
it("uses extensionless SQLite transcript locators by default", () => {
expect(
resolveSessionTranscriptLocator("sess-1", {
transcriptLocator: createSqliteSessionTranscriptLocator({ sessionId: "other-session" }),
}),
).toBe(createSqliteSessionTranscriptLocator({ sessionId: "sess-1" }));
expect(resolveSessionTranscriptLocator("sess-1")).toBe(
createSqliteSessionTranscriptLocator({ sessionId: "sess-1" }),
);
});
});

View File

@@ -257,7 +257,6 @@ export type DiagnosticsOtelConfig = {
export type DiagnosticsCacheTraceConfig = {
enabled?: boolean;
filePath?: string;
includeMessages?: boolean;
includePrompt?: boolean;
includeSystem?: boolean;

View File

@@ -435,7 +435,6 @@ export const OpenClawSchema = z
cacheTrace: z
.object({
enabled: z.boolean().optional(),
filePath: z.string().optional(),
includeMessages: z.boolean().optional(),
includePrompt: z.boolean().optional(),
includeSystem: z.boolean().optional(),

View File

@@ -752,7 +752,6 @@ describe("workspace .env blocklist completeness", () => {
"OPENCLAW_RAW_STREAM",
"OPENCLAW_RAW_STREAM_PATH",
"OPENCLAW_CACHE_TRACE",
"OPENCLAW_CACHE_TRACE_FILE",
"OPENCLAW_CACHE_TRACE_MESSAGES",
"OPENCLAW_CACHE_TRACE_PROMPT",
"OPENCLAW_CACHE_TRACE_SYSTEM",

View File

@@ -48,7 +48,6 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([
"OPENCLAW_BUNDLED_PLUGINS_DIR",
"OPENCLAW_BUNDLED_SKILLS_DIR",
"OPENCLAW_CACHE_TRACE",
"OPENCLAW_CACHE_TRACE_FILE",
"OPENCLAW_CACHE_TRACE_MESSAGES",
"OPENCLAW_CACHE_TRACE_PROMPT",
"OPENCLAW_CACHE_TRACE_SYSTEM",