fix: add diagnostics cache trace config (#1370) (thanks @parubets)

This commit is contained in:
Peter Steinberger
2026-01-21 10:21:47 +00:00
parent 5392fa0dfa
commit 97e8f9d619
8 changed files with 160 additions and 16 deletions

View File

@@ -11,6 +11,7 @@ Docs: https://docs.clawd.bot
### Fixes
- Nodes tool: include agent/node/gateway context in tool failure logs to speed approval debugging.
- UI: remove the chat stop button and keep the composer aligned to the bottom edge.
- Agents: add diagnostics cache trace config and fix cache trace logging edge cases. (#1370) — thanks @parubets.
## 2026.1.20

View File

@@ -0,0 +1,93 @@
import { describe, expect, it } from "vitest";
import type { ClawdbotConfig } from "../config/config.js";
import { resolveUserPath } from "../utils.js";
import { createCacheTrace } from "./cache-trace.js";
describe("createCacheTrace", () => {
it("returns null when diagnostics cache tracing is disabled", () => {
const trace = createCacheTrace({
cfg: {} as ClawdbotConfig,
env: {},
});
expect(trace).toBeNull();
});
it("honors diagnostics cache trace config and expands file paths", () => {
const lines: string[] = [];
const trace = createCacheTrace({
cfg: {
diagnostics: {
cacheTrace: {
enabled: true,
filePath: "~/.clawdbot/logs/cache-trace.jsonl",
},
},
},
env: {},
writer: {
filePath: "memory",
write: (line) => lines.push(line),
},
});
expect(trace).not.toBeNull();
expect(trace?.filePath).toBe(resolveUserPath("~/.clawdbot/logs/cache-trace.jsonl"));
trace?.recordStage("session:loaded", {
messages: [],
system: "sys",
});
expect(lines.length).toBe(1);
});
it("records empty prompt/system values when enabled", () => {
const lines: string[] = [];
const trace = createCacheTrace({
cfg: {
diagnostics: {
cacheTrace: {
enabled: true,
includePrompt: true,
includeSystem: true,
},
},
},
env: {},
writer: {
filePath: "memory",
write: (line) => lines.push(line),
},
});
trace?.recordStage("prompt:before", { prompt: "", system: "" });
const event = JSON.parse(lines[0]?.trim() ?? "{}") as Record<string, unknown>;
expect(event.prompt).toBe("");
expect(event.system).toBe("");
});
it("respects env overrides for enablement", () => {
const lines: string[] = [];
const trace = createCacheTrace({
cfg: {
diagnostics: {
cacheTrace: {
enabled: true,
},
},
},
env: {
CLAWDBOT_CACHE_TRACE: "0",
},
writer: {
filePath: "memory",
write: (line) => lines.push(line),
},
});
expect(trace).toBeNull();
});
});

View File

@@ -7,8 +7,8 @@ import type { Api, Model } from "@mariozechner/pi-ai";
import type { ClawdbotConfig } from "../config/config.js";
import { resolveStateDir } from "../config/paths.js";
import { isTruthyEnvValue } from "../infra/env.js";
import { parseBooleanValue } from "../utils/boolean.js";
import { resolveUserPath } from "../utils.js";
export type CacheTraceStage =
| "session:loaded"
@@ -61,6 +61,7 @@ type CacheTraceInit = {
modelId?: string;
modelApi?: string | null;
workspaceDir?: string;
writer?: CacheTraceWriter;
};
type CacheTraceConfig = {
@@ -80,14 +81,18 @@ const writers = new Map<string, CacheTraceWriter>();
function resolveCacheTraceConfig(params: CacheTraceInit): CacheTraceConfig {
const env = params.env ?? process.env;
const enabled = isTruthyEnvValue(env.CLAWDBOT_CACHE_TRACE);
const filePath =
env.CLAWDBOT_CACHE_TRACE_FILE?.trim() ||
path.join(resolveStateDir(env), "logs", "cache-trace.jsonl");
const config = params.cfg?.diagnostics?.cacheTrace;
const envEnabled = parseBooleanValue(env.CLAWDBOT_CACHE_TRACE);
const enabled = envEnabled ?? config?.enabled ?? false;
const fileOverride = config?.filePath?.trim() || env.CLAWDBOT_CACHE_TRACE_FILE?.trim();
const filePath = fileOverride
? resolveUserPath(fileOverride)
: path.join(resolveStateDir(env), "logs", "cache-trace.jsonl");
const includeMessages = parseBooleanValue(env.CLAWDBOT_CACHE_TRACE_MESSAGES);
const includePrompt = parseBooleanValue(env.CLAWDBOT_CACHE_TRACE_PROMPT);
const includeSystem = parseBooleanValue(env.CLAWDBOT_CACHE_TRACE_SYSTEM);
const includeMessages =
parseBooleanValue(env.CLAWDBOT_CACHE_TRACE_MESSAGES) ?? config?.includeMessages;
const includePrompt = parseBooleanValue(env.CLAWDBOT_CACHE_TRACE_PROMPT) ?? config?.includePrompt;
const includeSystem = parseBooleanValue(env.CLAWDBOT_CACHE_TRACE_SYSTEM) ?? config?.includeSystem;
return {
enabled,
@@ -189,7 +194,7 @@ export function createCacheTrace(params: CacheTraceInit): CacheTrace | null {
const cfg = resolveCacheTraceConfig(params);
if (!cfg.enabled) return null;
const writer = getWriter(cfg.filePath);
const writer = params.writer ?? getWriter(cfg.filePath);
let seq = 0;
const base: Omit<CacheTraceEvent, "ts" | "seq" | "stage"> = {
@@ -210,10 +215,10 @@ export function createCacheTrace(params: CacheTraceInit): CacheTrace | null {
stage,
};
if (payload.prompt && cfg.includePrompt) {
if (payload.prompt !== undefined && cfg.includePrompt) {
event.prompt = payload.prompt;
}
if (payload.system && cfg.includeSystem) {
if (payload.system !== undefined && cfg.includeSystem) {
event.system = payload.system;
event.systemDigest = digest(payload.system);
}

View File

@@ -115,6 +115,10 @@ describe("exec approvals CLI", () => {
runtimeErrors.length = 0;
callGatewayFromCli.mockClear();
const execApprovals = await import("../infra/exec-approvals.js");
const saveExecApprovals = vi.mocked(execApprovals.saveExecApprovals);
saveExecApprovals.mockClear();
const { registerExecApprovalsCli } = await import("./exec-approvals-cli.js");
const program = new Command();
program.exitOverride();
@@ -122,9 +126,17 @@ describe("exec approvals CLI", () => {
await program.parseAsync(["approvals", "allowlist", "add", "/usr/bin/uname"], { from: "user" });
const setCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "exec.approvals.set");
expect(setCall).toBeTruthy();
const params = setCall?.[2] as { file: { agents?: Record<string, unknown> } };
expect(params.file.agents?.["*"]).toBeTruthy();
expect(callGatewayFromCli).not.toHaveBeenCalledWith(
"exec.approvals.set",
expect.anything(),
{},
);
expect(saveExecApprovals).toHaveBeenCalledWith(
expect.objectContaining({
agents: expect.objectContaining({
"*": expect.anything(),
}),
}),
);
});
});

View File

@@ -13,6 +13,7 @@ import { formatDocsLink } from "../terminal/links.js";
import { isRich, theme } from "../terminal/theme.js";
import { renderTable } from "../terminal/table.js";
import { callGatewayFromCli } from "./gateway-rpc.js";
import { describeUnknownError } from "./gateway-cli/shared.js";
import { nodesCallOpts, resolveNodeId } from "./nodes-cli/rpc.js";
import type { NodesRpcOpts } from "./nodes-cli/types.js";
@@ -96,7 +97,7 @@ async function loadSnapshotTarget(opts: ExecApprovalsCliOpts): Promise<{
}
function formatCliError(err: unknown): string {
const msg = String(err ?? "unknown error");
const msg = describeUnknownError(err);
return msg.includes("\n") ? msg.split("\n")[0] : msg;
}

View File

@@ -114,6 +114,11 @@ const FIELD_LABELS: Record<string, string> = {
"diagnostics.otel.logs": "OpenTelemetry Logs Enabled",
"diagnostics.otel.sampleRate": "OpenTelemetry Trace Sample Rate",
"diagnostics.otel.flushIntervalMs": "OpenTelemetry Flush Interval (ms)",
"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",
"gateway.remote.url": "Remote Gateway URL",
"gateway.remote.sshTarget": "Remote Gateway SSH Target",
"gateway.remote.sshIdentity": "Remote Gateway SSH Identity",
@@ -345,6 +350,14 @@ const FIELD_HELP: Record<string, string> = {
"Extra node.invoke commands to allow beyond the gateway defaults (array of command strings).",
"gateway.nodes.denyCommands":
"Commands to block even if present in node claims or default allowlist.",
"diagnostics.cacheTrace.enabled":
"Log cache trace snapshots for embedded agent runs (default: false).",
"diagnostics.cacheTrace.filePath":
"JSONL output path for cache trace logs (default: $CLAWDBOT_STATE_DIR/logs/cache-trace.jsonl).",
"diagnostics.cacheTrace.includeMessages":
"Include full message payloads in trace output (default: true).",
"diagnostics.cacheTrace.includePrompt": "Include prompt text in trace output (default: true).",
"diagnostics.cacheTrace.includeSystem": "Include system prompt in trace output (default: true).",
"tools.exec.applyPatch.enabled":
"Experimental. Enables apply_patch for OpenAI models when allowed by tool policy.",
"tools.exec.applyPatch.allowModels":

View File

@@ -117,9 +117,18 @@ export type DiagnosticsOtelConfig = {
flushIntervalMs?: number;
};
export type DiagnosticsCacheTraceConfig = {
enabled?: boolean;
filePath?: string;
includeMessages?: boolean;
includePrompt?: boolean;
includeSystem?: boolean;
};
export type DiagnosticsConfig = {
enabled?: boolean;
otel?: DiagnosticsOtelConfig;
cacheTrace?: DiagnosticsCacheTraceConfig;
};
export type WebReconnectConfig = {

View File

@@ -63,6 +63,16 @@ export const ClawdbotSchema = z
})
.strict()
.optional(),
cacheTrace: z
.object({
enabled: z.boolean().optional(),
filePath: z.string().optional(),
includeMessages: z.boolean().optional(),
includePrompt: z.boolean().optional(),
includeSystem: z.boolean().optional(),
})
.strict()
.optional(),
})
.strict()
.optional(),