From e85abaca2b3e60435e0123766064f422db49302f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 24 Jan 2026 05:33:22 +0000 Subject: [PATCH] fix: wire anthropic payload log diagnostics (#1501) (thanks @parubets) --- CHANGELOG.md | 1 + docs/logging.md | 23 +++ src/agents/anthropic-payload-log.test.ts | 145 +++++++++++++++++++ src/agents/anthropic-payload-log.ts | 34 +++-- src/agents/pi-embedded-runner/run/attempt.ts | 1 + src/config/schema.ts | 6 + src/config/types.base.ts | 6 + src/config/zod-schema.ts | 7 + 8 files changed, 213 insertions(+), 10 deletions(-) create mode 100644 src/agents/anthropic-payload-log.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e93b92ffa..fcd3c1217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.clawd.bot - CLI: add live auth probes to `clawdbot models status` for per-profile verification. - CLI: add `clawdbot system` for system events + heartbeat controls; remove standalone `wake`. - Agents: add Bedrock auto-discovery defaults + config overrides. (#1553) Thanks @fal3. +- Agents: add diagnostics-configured Anthropic payload logging. (#1501) Thanks @parubets. - Docs: add cron vs heartbeat decision guide (with Lobster workflow notes). (#1533) Thanks @JustYannicc. - Docs: clarify HEARTBEAT.md empty file skips heartbeats, missing file still runs. (#1535) Thanks @JustYannicc. - Markdown: add per-channel table conversion (bullets for Signal/WhatsApp, code blocks elsewhere). (#1495) Thanks @odysseus0. diff --git a/docs/logging.md b/docs/logging.md index ad53c1164..449595763 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -95,6 +95,29 @@ Console logs are **TTY-aware** and formatted for readability: Console formatting is controlled by `logging.consoleStyle`. +## Anthropic payload log (debugging) + +For Anthropic-only runs, you can enable a dedicated JSONL log that captures the +exact request payload (as sent) plus per-run usage stats. This log includes full +prompt/message data; treat it as sensitive. + +```json +{ + "diagnostics": { + "anthropicPayloadLog": { + "enabled": true, + "filePath": "/path/to/anthropic-payload.jsonl" + } + } +} +``` + +Defaults + overrides: + +- Default path: `$CLAWDBOT_STATE_DIR/logs/anthropic-payload.jsonl` +- Env enable: `CLAWDBOT_ANTHROPIC_PAYLOAD_LOG=1` +- Env path: `CLAWDBOT_ANTHROPIC_PAYLOAD_LOG_FILE=/path/to/anthropic-payload.jsonl` + ## Configuring logging All logging configuration lives under `logging` in `~/.clawdbot/clawdbot.json`. diff --git a/src/agents/anthropic-payload-log.test.ts b/src/agents/anthropic-payload-log.test.ts new file mode 100644 index 000000000..22578f379 --- /dev/null +++ b/src/agents/anthropic-payload-log.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it } from "vitest"; + +import type { StreamFn } from "@mariozechner/pi-agent-core"; + +import type { ClawdbotConfig } from "../config/config.js"; +import { resolveUserPath } from "../utils.js"; +import { createAnthropicPayloadLogger } from "./anthropic-payload-log.js"; + +describe("createAnthropicPayloadLogger", () => { + it("returns null when diagnostics payload logging is disabled", () => { + const logger = createAnthropicPayloadLogger({ + cfg: {} as ClawdbotConfig, + env: {}, + modelApi: "anthropic-messages", + }); + + expect(logger).toBeNull(); + }); + + it("returns null when model api is not anthropic", () => { + const logger = createAnthropicPayloadLogger({ + cfg: { + diagnostics: { + anthropicPayloadLog: { + enabled: true, + }, + }, + }, + env: {}, + modelApi: "openai", + writer: { + filePath: "memory", + write: () => undefined, + }, + }); + + expect(logger).toBeNull(); + }); + + it("honors diagnostics config and expands file paths", () => { + const lines: string[] = []; + const logger = createAnthropicPayloadLogger({ + cfg: { + diagnostics: { + anthropicPayloadLog: { + enabled: true, + filePath: "~/.clawdbot/logs/anthropic-payload.jsonl", + }, + }, + }, + env: {}, + modelApi: "anthropic-messages", + writer: { + filePath: "memory", + write: (line) => lines.push(line), + }, + }); + + expect(logger).not.toBeNull(); + expect(logger?.filePath).toBe(resolveUserPath("~/.clawdbot/logs/anthropic-payload.jsonl")); + + logger?.recordUsage([ + { + role: "assistant", + usage: { + input: 12, + }, + } as unknown as { + role: string; + usage: { input: number }; + }, + ]); + + const event = JSON.parse(lines[0]?.trim() ?? "{}") as Record; + expect(event.stage).toBe("usage"); + expect(event.usage).toEqual({ input: 12 }); + }); + + it("records request payloads and forwards onPayload", async () => { + const lines: string[] = []; + let forwarded: unknown; + const logger = createAnthropicPayloadLogger({ + cfg: { + diagnostics: { + anthropicPayloadLog: { + enabled: true, + }, + }, + }, + env: {}, + modelApi: "anthropic-messages", + writer: { + filePath: "memory", + write: (line) => lines.push(line), + }, + }); + + const streamFn = ((_, __, options) => { + options?.onPayload?.({ hello: "world" }); + return Promise.resolve(undefined); + }) as StreamFn; + + const wrapped = logger?.wrapStreamFn(streamFn); + await wrapped?.( + { api: "anthropic-messages" } as unknown as { api: string }, + {}, + { + onPayload: (payload) => { + forwarded = payload; + }, + }, + ); + + const event = JSON.parse(lines[0]?.trim() ?? "{}") as Record; + expect(event.stage).toBe("request"); + expect(event.payload).toEqual({ hello: "world" }); + expect(event.payloadDigest).toBeTruthy(); + expect(forwarded).toEqual({ hello: "world" }); + }); + + it("records errors when usage is missing", () => { + const lines: string[] = []; + const logger = createAnthropicPayloadLogger({ + cfg: { + diagnostics: { + anthropicPayloadLog: { + enabled: true, + }, + }, + }, + env: {}, + modelApi: "anthropic-messages", + writer: { + filePath: "memory", + write: (line) => lines.push(line), + }, + }); + + logger?.recordUsage([], new Error("boom")); + + const event = JSON.parse(lines[0]?.trim() ?? "{}") as Record; + expect(event.stage).toBe("usage"); + expect(event.error).toContain("boom"); + }); +}); diff --git a/src/agents/anthropic-payload-log.ts b/src/agents/anthropic-payload-log.ts index 8b39d2f1a..c0afe318e 100644 --- a/src/agents/anthropic-payload-log.ts +++ b/src/agents/anthropic-payload-log.ts @@ -5,7 +5,9 @@ import path from "node:path"; import type { AgentMessage, StreamFn } from "@mariozechner/pi-agent-core"; import type { Api, Model } from "@mariozechner/pi-ai"; +import type { ClawdbotConfig } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; +import { formatErrorMessage } from "../infra/errors.js"; import { parseBooleanValue } from "../utils/boolean.js"; import { resolveUserPath } from "../utils.js"; import { createSubsystemLogger } from "../logging/subsystem.js"; @@ -33,6 +35,11 @@ type PayloadLogConfig = { filePath: string; }; +type PayloadLogInit = { + cfg?: ClawdbotConfig; + env?: NodeJS.ProcessEnv; +}; + type PayloadLogWriter = { filePath: string; write: (line: string) => void; @@ -41,9 +48,12 @@ type PayloadLogWriter = { const writers = new Map(); const log = createSubsystemLogger("agent/anthropic-payload"); -function resolvePayloadLogConfig(env: NodeJS.ProcessEnv): PayloadLogConfig { - const enabled = parseBooleanValue(env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG) ?? false; - const fileOverride = env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG_FILE?.trim(); +function resolvePayloadLogConfig(params: PayloadLogInit): PayloadLogConfig { + const env = params.env ?? process.env; + const config = params.cfg?.diagnostics?.anthropicPayloadLog; + const envEnabled = parseBooleanValue(env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG); + const enabled = envEnabled ?? config?.enabled ?? false; + const fileOverride = config?.filePath?.trim() || env.CLAWDBOT_ANTHROPIC_PAYLOAD_LOG_FILE?.trim(); const filePath = fileOverride ? resolveUserPath(fileOverride) : path.join(resolveStateDir(env), "logs", "anthropic-payload.jsonl"); @@ -112,11 +122,13 @@ function findLastAssistantUsage(messages: AgentMessage[]): Record StreamFn; recordUsage: (messages: AgentMessage[], error?: unknown) => void; }; export function createAnthropicPayloadLogger(params: { + cfg?: ClawdbotConfig; env?: NodeJS.ProcessEnv; runId?: string; sessionId?: string; @@ -125,12 +137,14 @@ export function createAnthropicPayloadLogger(params: { modelId?: string; modelApi?: string | null; workspaceDir?: string; + writer?: PayloadLogWriter; }): AnthropicPayloadLogger | null { const env = params.env ?? process.env; - const cfg = resolvePayloadLogConfig(env); + const cfg = resolvePayloadLogConfig({ env, cfg: params.cfg }); if (!cfg.enabled) return null; + if (params.modelApi !== "anthropic-messages") return null; - const writer = getWriter(cfg.filePath); + const writer = params.writer ?? getWriter(cfg.filePath); const base: Omit = { runId: params.runId, sessionId: params.sessionId, @@ -163,7 +177,7 @@ export function createAnthropicPayloadLogger(params: { options?.onPayload?.(payload); }; return streamFn(model, context, { - ...(options ?? {}), + ...options, onPayload: nextOnPayload, }); }; @@ -178,7 +192,7 @@ export function createAnthropicPayloadLogger(params: { ...base, ts: new Date().toISOString(), stage: "usage", - error: String(error), + error: formatErrorMessage(error), }); } return; @@ -188,7 +202,7 @@ export function createAnthropicPayloadLogger(params: { ts: new Date().toISOString(), stage: "usage", usage, - error: error ? String(error) : undefined, + error: error ? formatErrorMessage(error) : undefined, }); log.info("anthropic usage", { runId: params.runId, @@ -197,6 +211,6 @@ export function createAnthropicPayloadLogger(params: { }); }; - log.info("anthropic payload logger enabled", { filePath: writer.filePath }); - return { enabled: true, wrapStreamFn, recordUsage }; + log.info("anthropic payload logger enabled", { filePath: cfg.filePath }); + return { enabled: true, filePath: cfg.filePath, wrapStreamFn, recordUsage }; } diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts index c121bb42b..a88abc336 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts @@ -460,6 +460,7 @@ export async function runEmbeddedAttempt( workspaceDir: params.workspaceDir, }); const anthropicPayloadLogger = createAnthropicPayloadLogger({ + cfg: params.config, env: process.env, runId: params.runId, sessionId: activeSession.sessionId, diff --git a/src/config/schema.ts b/src/config/schema.ts index f9601962f..7dee291d2 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -121,6 +121,8 @@ const FIELD_LABELS: Record = { "diagnostics.cacheTrace.includeMessages": "Cache Trace Include Messages", "diagnostics.cacheTrace.includePrompt": "Cache Trace Include Prompt", "diagnostics.cacheTrace.includeSystem": "Cache Trace Include System", + "diagnostics.anthropicPayloadLog.enabled": "Anthropic Payload Log Enabled", + "diagnostics.anthropicPayloadLog.filePath": "Anthropic Payload Log File Path", "agents.list.*.identity.avatar": "Identity Avatar", "gateway.remote.url": "Remote Gateway URL", "gateway.remote.sshTarget": "Remote Gateway SSH Target", @@ -390,6 +392,10 @@ const FIELD_HELP: Record = { "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).", + "diagnostics.anthropicPayloadLog.enabled": + "Log Anthropic request payloads + usage for embedded runs (default: false).", + "diagnostics.anthropicPayloadLog.filePath": + "JSONL output path for Anthropic payload logs (default: $CLAWDBOT_STATE_DIR/logs/anthropic-payload.jsonl).", "tools.exec.applyPatch.enabled": "Experimental. Enables apply_patch for OpenAI models when allowed by tool policy.", "tools.exec.applyPatch.allowModels": diff --git a/src/config/types.base.ts b/src/config/types.base.ts index a84736571..b185ebbab 100644 --- a/src/config/types.base.ts +++ b/src/config/types.base.ts @@ -133,10 +133,16 @@ export type DiagnosticsCacheTraceConfig = { includeSystem?: boolean; }; +export type DiagnosticsAnthropicPayloadLogConfig = { + enabled?: boolean; + filePath?: string; +}; + export type DiagnosticsConfig = { enabled?: boolean; otel?: DiagnosticsOtelConfig; cacheTrace?: DiagnosticsCacheTraceConfig; + anthropicPayloadLog?: DiagnosticsAnthropicPayloadLogConfig; }; export type WebReconnectConfig = { diff --git a/src/config/zod-schema.ts b/src/config/zod-schema.ts index b8233d14c..743d4604c 100644 --- a/src/config/zod-schema.ts +++ b/src/config/zod-schema.ts @@ -86,6 +86,13 @@ export const ClawdbotSchema = z }) .strict() .optional(), + anthropicPayloadLog: z + .object({ + enabled: z.boolean().optional(), + filePath: z.string().optional(), + }) + .strict() + .optional(), }) .strict() .optional(),