fix: add diagnostics cache trace config (#1370) (thanks @parubets)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
93
src/agents/cache-trace.test.ts
Normal file
93
src/agents/cache-trace.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user