feat(observe): record real tool failures from PostToolUseFailure

Claude Code sends failed tool calls to the PostToolUseFailure event.
ADAM registered only PostToolUse, so it never saw a real failure, and
every tool_error_loop came from ERROR_RE matching successful output.

Handle PostToolUseFailure in adam-observe.mjs through the PostToolUse
detectors. Shape the error string as an is_error response. An interrupt
is an abort, so it counts as a tool event but gets no fingerprint.
Register the event in settings.json.example.

Tests 9b and 9c added: 150 passed, 0 failed.

Claude-Session: https://claude.ai/code/session_014prTNuJnKuwcAd28vUZAnN
This commit is contained in:
2026-09-25 03:16:01 +01:00
parent e3fe691b45
commit f418afa275
3 changed files with 40 additions and 5 deletions
+10 -5
View File
@@ -465,12 +465,17 @@ function main() {
state.subagent_dispatch_emitted[name] = true;
}
}
} else if (event === "PostToolUse") {
} else if (event === "PostToolUse" || event === "PostToolUseFailure") {
const tool = input.tool_name || "unknown";
const argsHash = djb2(JSON.stringify(input.tool_input || {}));
const file = (input.tool_input && (input.tool_input.file_path || input.tool_input.path)) || null;
const toolResponse = input.tool_response;
// Claude Code sends failed tool calls to PostToolUseFailure with the error
// text in `error`. Shape it like an is_error response so every PostToolUse
// detector sees real failures. An interrupt is an abort, not a tool error.
const toolResponse = event === "PostToolUseFailure"
? { is_error: input.is_interrupt !== true, content: String(input.error || "") }
: input.tool_response;
const respExcerpt = (() => {
if (!toolResponse) return null;
const text = typeof toolResponse === "string" ? toolResponse
@@ -541,11 +546,11 @@ function main() {
}
}
if (input.tool_response && typeof input.tool_response === "object") {
if (toolResponse && typeof toolResponse === "object") {
bumpUsage("payload:tool_response_seen");
}
const fp = errorFingerprint(input.tool_response);
const fp = errorFingerprint(toolResponse);
if (fp) {
bumpUsage("payload:tool_response_error_seen");
if (state.recentRecoveries.length) {
@@ -590,7 +595,7 @@ function main() {
if (tool === "Bash") {
const cmd = (input.tool_input && input.tool_input.command) || "";
const isBuildCmd = BUILD_RE.test(cmd);
const hasError = (input.tool_response && input.tool_response.is_error === true) || fp !== null;
const hasError = (toolResponse && toolResponse.is_error === true) || fp !== null;
if (isBuildCmd && hasError) {
state.build_failure_count += 1;
if (state.build_failure_count >= BUILD_LOOP_THRESHOLD && !state.build_loop_emitted) {