fix: add diagnostics cache trace config (#1370) (thanks @parubets)
This commit is contained in:
@@ -11,6 +11,7 @@ Docs: https://docs.clawd.bot
|
|||||||
### Fixes
|
### Fixes
|
||||||
- Nodes tool: include agent/node/gateway context in tool failure logs to speed approval debugging.
|
- 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.
|
- 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
|
## 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 type { ClawdbotConfig } from "../config/config.js";
|
||||||
import { resolveStateDir } from "../config/paths.js";
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
import { isTruthyEnvValue } from "../infra/env.js";
|
|
||||||
import { parseBooleanValue } from "../utils/boolean.js";
|
import { parseBooleanValue } from "../utils/boolean.js";
|
||||||
|
import { resolveUserPath } from "../utils.js";
|
||||||
|
|
||||||
export type CacheTraceStage =
|
export type CacheTraceStage =
|
||||||
| "session:loaded"
|
| "session:loaded"
|
||||||
@@ -61,6 +61,7 @@ type CacheTraceInit = {
|
|||||||
modelId?: string;
|
modelId?: string;
|
||||||
modelApi?: string | null;
|
modelApi?: string | null;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
|
writer?: CacheTraceWriter;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CacheTraceConfig = {
|
type CacheTraceConfig = {
|
||||||
@@ -80,14 +81,18 @@ const writers = new Map<string, CacheTraceWriter>();
|
|||||||
|
|
||||||
function resolveCacheTraceConfig(params: CacheTraceInit): CacheTraceConfig {
|
function resolveCacheTraceConfig(params: CacheTraceInit): CacheTraceConfig {
|
||||||
const env = params.env ?? process.env;
|
const env = params.env ?? process.env;
|
||||||
const enabled = isTruthyEnvValue(env.CLAWDBOT_CACHE_TRACE);
|
const config = params.cfg?.diagnostics?.cacheTrace;
|
||||||
const filePath =
|
const envEnabled = parseBooleanValue(env.CLAWDBOT_CACHE_TRACE);
|
||||||
env.CLAWDBOT_CACHE_TRACE_FILE?.trim() ||
|
const enabled = envEnabled ?? config?.enabled ?? false;
|
||||||
path.join(resolveStateDir(env), "logs", "cache-trace.jsonl");
|
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 includeMessages =
|
||||||
const includePrompt = parseBooleanValue(env.CLAWDBOT_CACHE_TRACE_PROMPT);
|
parseBooleanValue(env.CLAWDBOT_CACHE_TRACE_MESSAGES) ?? config?.includeMessages;
|
||||||
const includeSystem = parseBooleanValue(env.CLAWDBOT_CACHE_TRACE_SYSTEM);
|
const includePrompt = parseBooleanValue(env.CLAWDBOT_CACHE_TRACE_PROMPT) ?? config?.includePrompt;
|
||||||
|
const includeSystem = parseBooleanValue(env.CLAWDBOT_CACHE_TRACE_SYSTEM) ?? config?.includeSystem;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled,
|
enabled,
|
||||||
@@ -189,7 +194,7 @@ export function createCacheTrace(params: CacheTraceInit): CacheTrace | null {
|
|||||||
const cfg = resolveCacheTraceConfig(params);
|
const cfg = resolveCacheTraceConfig(params);
|
||||||
if (!cfg.enabled) return null;
|
if (!cfg.enabled) return null;
|
||||||
|
|
||||||
const writer = getWriter(cfg.filePath);
|
const writer = params.writer ?? getWriter(cfg.filePath);
|
||||||
let seq = 0;
|
let seq = 0;
|
||||||
|
|
||||||
const base: Omit<CacheTraceEvent, "ts" | "seq" | "stage"> = {
|
const base: Omit<CacheTraceEvent, "ts" | "seq" | "stage"> = {
|
||||||
@@ -210,10 +215,10 @@ export function createCacheTrace(params: CacheTraceInit): CacheTrace | null {
|
|||||||
stage,
|
stage,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (payload.prompt && cfg.includePrompt) {
|
if (payload.prompt !== undefined && cfg.includePrompt) {
|
||||||
event.prompt = payload.prompt;
|
event.prompt = payload.prompt;
|
||||||
}
|
}
|
||||||
if (payload.system && cfg.includeSystem) {
|
if (payload.system !== undefined && cfg.includeSystem) {
|
||||||
event.system = payload.system;
|
event.system = payload.system;
|
||||||
event.systemDigest = digest(payload.system);
|
event.systemDigest = digest(payload.system);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,10 @@ describe("exec approvals CLI", () => {
|
|||||||
runtimeErrors.length = 0;
|
runtimeErrors.length = 0;
|
||||||
callGatewayFromCli.mockClear();
|
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 { registerExecApprovalsCli } = await import("./exec-approvals-cli.js");
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
program.exitOverride();
|
program.exitOverride();
|
||||||
@@ -122,9 +126,17 @@ describe("exec approvals CLI", () => {
|
|||||||
|
|
||||||
await program.parseAsync(["approvals", "allowlist", "add", "/usr/bin/uname"], { from: "user" });
|
await program.parseAsync(["approvals", "allowlist", "add", "/usr/bin/uname"], { from: "user" });
|
||||||
|
|
||||||
const setCall = callGatewayFromCli.mock.calls.find((call) => call[0] === "exec.approvals.set");
|
expect(callGatewayFromCli).not.toHaveBeenCalledWith(
|
||||||
expect(setCall).toBeTruthy();
|
"exec.approvals.set",
|
||||||
const params = setCall?.[2] as { file: { agents?: Record<string, unknown> } };
|
expect.anything(),
|
||||||
expect(params.file.agents?.["*"]).toBeTruthy();
|
{},
|
||||||
|
);
|
||||||
|
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 { isRich, theme } from "../terminal/theme.js";
|
||||||
import { renderTable } from "../terminal/table.js";
|
import { renderTable } from "../terminal/table.js";
|
||||||
import { callGatewayFromCli } from "./gateway-rpc.js";
|
import { callGatewayFromCli } from "./gateway-rpc.js";
|
||||||
|
import { describeUnknownError } from "./gateway-cli/shared.js";
|
||||||
import { nodesCallOpts, resolveNodeId } from "./nodes-cli/rpc.js";
|
import { nodesCallOpts, resolveNodeId } from "./nodes-cli/rpc.js";
|
||||||
import type { NodesRpcOpts } from "./nodes-cli/types.js";
|
import type { NodesRpcOpts } from "./nodes-cli/types.js";
|
||||||
|
|
||||||
@@ -96,7 +97,7 @@ async function loadSnapshotTarget(opts: ExecApprovalsCliOpts): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatCliError(err: unknown): string {
|
function formatCliError(err: unknown): string {
|
||||||
const msg = String(err ?? "unknown error");
|
const msg = describeUnknownError(err);
|
||||||
return msg.includes("\n") ? msg.split("\n")[0] : msg;
|
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.logs": "OpenTelemetry Logs Enabled",
|
||||||
"diagnostics.otel.sampleRate": "OpenTelemetry Trace Sample Rate",
|
"diagnostics.otel.sampleRate": "OpenTelemetry Trace Sample Rate",
|
||||||
"diagnostics.otel.flushIntervalMs": "OpenTelemetry Flush Interval (ms)",
|
"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.url": "Remote Gateway URL",
|
||||||
"gateway.remote.sshTarget": "Remote Gateway SSH Target",
|
"gateway.remote.sshTarget": "Remote Gateway SSH Target",
|
||||||
"gateway.remote.sshIdentity": "Remote Gateway SSH Identity",
|
"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).",
|
"Extra node.invoke commands to allow beyond the gateway defaults (array of command strings).",
|
||||||
"gateway.nodes.denyCommands":
|
"gateway.nodes.denyCommands":
|
||||||
"Commands to block even if present in node claims or default allowlist.",
|
"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":
|
"tools.exec.applyPatch.enabled":
|
||||||
"Experimental. Enables apply_patch for OpenAI models when allowed by tool policy.",
|
"Experimental. Enables apply_patch for OpenAI models when allowed by tool policy.",
|
||||||
"tools.exec.applyPatch.allowModels":
|
"tools.exec.applyPatch.allowModels":
|
||||||
|
|||||||
@@ -117,9 +117,18 @@ export type DiagnosticsOtelConfig = {
|
|||||||
flushIntervalMs?: number;
|
flushIntervalMs?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DiagnosticsCacheTraceConfig = {
|
||||||
|
enabled?: boolean;
|
||||||
|
filePath?: string;
|
||||||
|
includeMessages?: boolean;
|
||||||
|
includePrompt?: boolean;
|
||||||
|
includeSystem?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export type DiagnosticsConfig = {
|
export type DiagnosticsConfig = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
otel?: DiagnosticsOtelConfig;
|
otel?: DiagnosticsOtelConfig;
|
||||||
|
cacheTrace?: DiagnosticsCacheTraceConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WebReconnectConfig = {
|
export type WebReconnectConfig = {
|
||||||
|
|||||||
@@ -63,6 +63,16 @@ export const ClawdbotSchema = z
|
|||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.optional(),
|
.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()
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user