mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-10 20:45:15 +00:00
refactor: keep embedded runner diagnostics in sqlite
This commit is contained in:
@@ -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`).
|
||||
|
||||
---
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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).",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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" }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -257,7 +257,6 @@ export type DiagnosticsOtelConfig = {
|
||||
|
||||
export type DiagnosticsCacheTraceConfig = {
|
||||
enabled?: boolean;
|
||||
filePath?: string;
|
||||
includeMessages?: boolean;
|
||||
includePrompt?: boolean;
|
||||
includeSystem?: boolean;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user