diff --git a/docs/gateway/configuration-reference.md b/docs/gateway/configuration-reference.md index 12f7271f12e..1303813030a 100644 --- a/docs/gateway/configuration-reference.md +++ b/docs/gateway/configuration-reference.md @@ -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`). --- diff --git a/docs/refactor/database-first.md b/docs/refactor/database-first.md index 74b7bbc96d0..bafdb326af2 100644 --- a/docs/refactor/database-first.md +++ b/docs/refactor/database-first.md @@ -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 diff --git a/docs/reference/prompt-caching.md b/docs/reference/prompt-caching.md index ecb8fee254d..001371c9edb 100644 --- a/docs/reference/prompt-caching.md +++ b/docs/reference/prompt-caching.md @@ -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. diff --git a/src/agents/anthropic-payload-log.ts b/src/agents/anthropic-payload-log.ts index 4acc5b76f3c..603741d87cf 100644 --- a/src/agents/anthropic-payload-log.ts +++ b/src/agents/anthropic-payload-log.ts @@ -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(); const stateWriters = new Map(); 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, diff --git a/src/agents/cache-trace.test.ts b/src/agents/cache-trace.test.ts index f4f84f4fba1..320ad6070c5 100644 --- a/src/agents/cache-trace.test.ts +++ b/src/agents/cache-trace.test.ts @@ -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: [], diff --git a/src/agents/cache-trace.ts b/src/agents/cache-trace.ts index 7a42bc30d18..55f8d28dac6 100644 --- a/src/agents/cache-trace.ts +++ b/src/agents/cache-trace.ts @@ -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(); const stateWriters = new Map(); 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, diff --git a/src/agents/pi-embedded-runner.cache.live.test.ts b/src/agents/pi-embedded-runner.cache.live.test.ts index 46305da50d6..2cc34c030f3 100644 --- a/src/agents/pi-embedded-runner.cache.live.test.ts +++ b/src/agents/pi-embedded-runner.cache.live.test.ts @@ -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 { - 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("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 }); } diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 08831cd9bf5..8f80dc48154 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -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); diff --git a/src/agents/pi-embedded-runner/run.worker-launch.test.ts b/src/agents/pi-embedded-runner/run.worker-launch.test.ts index d231818a696..be8eea59b38 100644 --- a/src/agents/pi-embedded-runner/run.worker-launch.test.ts +++ b/src/agents/pi-embedded-runner/run.worker-launch.test.ts @@ -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", diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index e6db2da0d7a..788a8467be3 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -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, diff --git a/src/commands/doctor-heartbeat-main-session-repair.ts b/src/commands/doctor-heartbeat-main-session-repair.ts index bcfb7986cb8..89ee135e990 100644 --- a/src/commands/doctor-heartbeat-main-session-repair.ts +++ b/src/commands/doctor-heartbeat-main-session-repair.ts @@ -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; } diff --git a/src/commands/doctor-state-integrity.test.ts b/src/commands/doctor-state-integrity.test.ts index bf9354b4ed5..09d22150e8e 100644 --- a/src/commands/doctor-state-integrity.test.ts +++ b/src/commands/doctor-state-integrity.test.ts @@ -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 () => { diff --git a/src/commands/doctor-state-integrity.ts b/src/commands/doctor-state-integrity.ts index 2b97ff5f68c..1a6922ec4b3 100644 --- a/src/commands/doctor-state-integrity.ts +++ b/src/commands/doctor-state-integrity.ts @@ -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 { diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 0b240ec85b4..b3e61e42bac 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -648,9 +648,7 @@ export const FIELD_HELP: Record = { "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).", diff --git a/src/config/schema.labels.ts b/src/config/schema.labels.ts index e04583e7105..2dd9a563e88 100644 --- a/src/config/schema.labels.ts +++ b/src/config/schema.labels.ts @@ -63,7 +63,6 @@ export const FIELD_LABELS: Record = { "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", diff --git a/src/config/sessions.test.ts b/src/config/sessions.test.ts index fec69bb3b96..7916057e0e6 100644 --- a/src/config/sessions.test.ts +++ b/src/config/sessions.test.ts @@ -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" }), ); }); diff --git a/src/config/sessions/paths.ts b/src/config/sessions/paths.ts index 8d43e13eb46..0411c1b06dc 100644 --- a/src/config/sessions/paths.ts +++ b/src/config/sessions/paths.ts @@ -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 }); } diff --git a/src/config/sessions/sessions.test.ts b/src/config/sessions/sessions.test.ts index 6f61389f064..5232f367712 100644 --- a/src/config/sessions/sessions.test.ts +++ b/src/config/sessions/sessions.test.ts @@ -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" }), + ); }); }); diff --git a/src/config/types.base.ts b/src/config/types.base.ts index 4e59e534d84..dd5e1557892 100644 --- a/src/config/types.base.ts +++ b/src/config/types.base.ts @@ -257,7 +257,6 @@ export type DiagnosticsOtelConfig = { export type DiagnosticsCacheTraceConfig = { enabled?: boolean; - filePath?: string; includeMessages?: boolean; includePrompt?: boolean; includeSystem?: boolean; diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index 51b763c7e4b..4c22d8d8405 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -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(), diff --git a/src/infra/dotenv.test.ts b/src/infra/dotenv.test.ts index dd0d4e41f6a..f8bb1b471da 100644 --- a/src/infra/dotenv.test.ts +++ b/src/infra/dotenv.test.ts @@ -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", diff --git a/src/infra/dotenv.ts b/src/infra/dotenv.ts index 9f4f302b542..95febe10914 100644 --- a/src/infra/dotenv.ts +++ b/src/infra/dotenv.ts @@ -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",